Machine coding
You get a small product spec and a laptop. There is no algorithm in this round — the entire test is whether code you write under pressure looks like code from someone who has shipped. Your background should make this your strongest round, and it is where unprepared candidates with better resumes lose.
| Weight | What is scored | What it means in the room |
|---|---|---|
| 30% | It works | The happy path runs end to end. A beautiful non-working submission scores below an ugly working one, every time. |
| 25% | Separation of concerns | Components, hooks, services, types in separate files with clear boundaries. No business logic inside a JSX return. |
| 20% | Extensibility | They will say "now add X" in the last ten minutes. If that takes thirty seconds, you pass. |
| 15% | States & edges | Loading, empty, error, disabled-while-submitting. Most candidates ship only the success state. |
| 10% | Naming & types | No any. No data2. Types that describe the domain, not the shape of a fetch response. |
Do not open the editor. Spend six to eight minutes out loud: restate the requirements in your own words, ask two clarifying questions, say what you will build and what you are explicitly leaving out, then sketch the file structure. Interviewers score this. Candidates who start typing at second thirty almost always finish with a mess.
Skip a drag library unless it is explicitly allowed — the HTML5 dragstart / dragover / drop trio is enough and shows you know the platform. The decision they are watching for is normalised state:
type Task = { id: string; title: string; createdAt: number }
type Board = {
tasks: Record<string, Task> // id → task, one source of truth
columns: Record<string, string[]> // columnId → ordered ids
order: string[] // column order
}
// moving a card is two array operations, not a tree walk
function move(b: Board, id: string, from: string, to: string, at: number): Board {
const src = b.columns[from].filter(x => x !== id)
const dst = from === to ? src : [...b.columns[to]]
dst.splice(at, 0, id)
return { ...b, columns: { ...b.columns, [from]: src, [to]: dst } }
}Nested arrays of task objects force a deep clone on every drag and make "which column is this card in" an O(n·m) search. Say that out loud as you choose — the reasoning scores higher than the result.
- Now persist it and reload without losing order.
- Add an undo.
- What happens if two tabs are open?
Derive the visible rows in one pipeline, memoised, and reset the page index whenever the filter or query changes. That reset is the bug they are looking for: filter down to three results while on page 4 and an unprepared implementation renders an empty table.
const rows = useMemo(() => {
const q = query.trim().toLowerCase()
return data
.filter(r => !q || r.name.toLowerCase().includes(q))
.filter(r => !status || r.status === status)
.sort(BY[sortKey])
}, [data, query, status, sortKey])
const pageRows = rows.slice(page * SIZE, page * SIZE + SIZE)
useEffect(() => { setPage(0) }, [query, status]) // the bit everyone forgetsDebounce the search input at ~300ms and say why. Put the fetch in a custom hook (useTasks), never inline in the component body — that single move is most of the "separation of concerns" score.
- Now make sorting server-side.
- The dataset is 50,000 rows — what changes?
- Make the filter state survive a page refresh.
Drive the steps from a config array, never a switch. Adding a step then means adding one object.
const STEPS = [
{ id: 'account', title: 'Account', fields: ['email','password'], schema: accountSchema },
{ id: 'profile', title: 'Profile', fields: ['name','phone'], schema: profileSchema },
{ id: 'review', title: 'Review', fields: [], schema: null },
]
const step = STEPS[i]
const errors = step.schema ? validate(step.schema, form) : {}
const canNext = Object.keys(errors).length === 0Keep every step's data in one parent object rather than per-step state, so the review step is a read and going back does not lose input. Mention Zod and derive the TypeScript types from the schema with z.infer, so the runtime check and the compile-time type cannot drift apart.
- Add a fourth step.
- Persist a half-finished form and resume it.
- The last step submits — handle the failure.
Say up front that you have shipped a booking marketplace — it buys you credibility for the whole round. Then model slots as half-open intervals [start, end), which makes back-to-back slots not overlap, and write the predicate as a named pure function:
// half-open: [aStart, aEnd) and [bStart, bEnd)
const overlaps = (a: Slot, b: Slot) =>
a.start < b.end && b.start < a.end
// 10:00–11:00 and 11:00–12:00 → false. Correct.
// Using <= here is the classic off-by-one that breaks adjacency.Keep everything in UTC internally and format only at the edge. If they ask about recurring slots, the answer is: store the rule, expand to concrete instances for the visible window, and store exceptions separately — never store a thousand rows for a weekly repeat.
- Two people book the last seat at the same moment.
- Handle a user in a different timezone.
- Now support recurring weekly slots.
Given your stack this may replace the frontend problem. Scaffold with the CLI, then make the layering visible: controller does HTTP only, service holds logic, repository touches data, DTOs validate at the boundary, one module per domain, a global exception filter.
src/
bookings/
bookings.controller.ts // HTTP only — no logic
bookings.service.ts // the actual rules
bookings.repository.ts // data access
dto/create-booking.dto.ts
entities/booking.entity.ts
bookings.module.ts
common/
filters/http-exception.filter.ts
interceptors/logging.interceptor.ts- Add pagination.
- Make POST idempotent.
- Where would you put a transaction?
- "I am building the data model first, because everything else falls out of it."
- "I am storing this normalised so a reorder is a splice rather than a deep clone."
- "This goes in a hook so the component stays presentational and the logic is testable on its own."
- "Debouncing here at 300ms — without it we fire a request per keystroke."
- "I am doing the empty state now rather than at the end, because it is the state people actually see first."
- "I would use Zod here in real code; for time I am hand-rolling it, but I would not ship this."
- "This is the seam where optimistic updates would go."
- "If this list went past a few thousand rows I would virtualise it. I am not doing that now because it is not the bottleneck at this size."
- "That is the happy path working. With the remaining twenty minutes I will add the error state, then tidy naming."
The last one matters most. Announcing your plan for the time you have left is the single clearest signal of a senior candidate rather than a fast one.
Rehearse this until you can produce it without thinking:
src/
components/ // presentational only: props in, events out
features/<name>/ // the feature: its hooks, components, types
hooks/ // useDebounce, useLocalStorage, useFetch
lib/ // api client, formatters, pure helpers
types/ // domain typesTwo rules that survive contact with a timer: a component that fetches is a feature, not a component; and anything you would unit-test goes in lib/ or a hook, never inside JSX.