Coding drills

Take-home and pairing

How to score well on take-home assignments and pairing sessions — scoping and time-boxing, the README that explains decisions, tests that matter, commits that tell a story, avoiding over-engineering, the reviewer's rubric, code-review drills for spotting bugs in Vue and Nuxt snippets, refactoring under observation, and pairing etiquette.

Take-homes and pairing sessions test the everyday job more directly than any whiteboard: can you read a brief, make sensible decisions, deliver something that works, and explain it. They are also where seniors most often lose points by doing too much. The reviewer has a rubric; this page is that rubric turned into a method.

Know

  • Read the brief twice: list the explicit requirements, the implicit ones (it must run with one command; it must not crash on empty data), and the ambiguities you will resolve with stated assumptions. Ask questions if a channel exists; otherwise write the assumptions in the README.
  • Time-box: a "4–6 hours" assignment means a working core in the first half, then polish in priority order: correctness on edges, tests on the core logic, accessibility basics, README, then extras. Log the time honestly; reviewers prefer a clean small solution to a sprawling one.
  • Architecture proportional to the problem: a todo app does not need Pinia, i18n and a design system; a data-heavy dashboard does need a state strategy. Use the framework's conventions (Nuxt file structure, useFetch, server/api) so the reviewer sees idioms, not inventions. No premature abstractions; one duplication beats a wrong abstraction.
  • What reviewers score: does it run (pnpm install && pnpm dev) and match the brief; code clarity and naming; correctness on edge cases; typed code; tests on logic that can break; accessibility and semantics of the UI; error, loading and empty states; the README's reasoning; commit history; and the honesty of "what I would do next". Over-engineering, ignored requirements, any everywhere, no error handling and a broken install are the common fails.
  • The README: how to run and test in three lines; the assumptions made; the architecture in a paragraph (where state lives, how data flows); decisions and trade-offs with alternatives considered; what is intentionally left out and what you would do with more time; known limitations. Screenshots or a deployed preview help.
  • Tests that matter: unit tests for pure logic (parsers, reducers, computations), one or two component tests for the key interaction, an API test if there is a server route, no snapshot tests of whole pages. Tests must pass from a clean install with pnpm test.
  • Commits: small, ordered, with messages that explain why; feat: list todos with filter in URL, fix: keep focus on the input after adding, test: cover empty and error states. A reviewer reads the history to see how you work; squashing everything into "final" hides that.
  • Pairing sessions: think aloud, restate the goal, ask about constraints and conventions, propose a plan before typing, write a failing test when the setup allows it, take feedback gracefully and act on it visibly, keep an eye on the clock, and say what you would do next when time runs out. Silence and defensiveness are the two failure modes; so is ignoring the interviewer's hints.
  • Code-review drills: you are shown a snippet and asked what is wrong. Vue and Nuxt classics: mutating a prop; index as key; watch without cleanup; async setup without await of a composable (context loss); fetching in onMounted in a Nuxt page (no SSR data); useFetch without a key inside a loop; reading window at module scope; a store created at module scope on the server; v-html with user content; missing aria-labels; a promise not awaited; Array.shift in a loop; == comparisons; catching and swallowing errors; secrets in runtimeConfig.public. Say what breaks, when, and how to fix it.
  • Refactoring under observation: characterise the current behaviour with a test first, make one small change at a time with the tests green, name the smell you are removing (long method, feature envy, duplicated conditionals), and stop when the goal is met rather than gold-plating.
  • Explaining afterwards: be ready to defend every decision and to say which you would change; reviewers ask "why not X" to see reasoning, not to catch you.

How it works

A README skeleton that hits every rubric item:

README.md
# Orders dashboard (take-home)

## Run
pnpm install && pnpm dev          # http://localhost:3000
pnpm test                         # Vitest: unit + component tests
pnpm typecheck && pnpm lint

## What it does
Lists orders from the provided JSON API with filtering by status (kept in the URL), sorting, and a detail drawer.
Loading, empty and error states are handled; the list is keyboard-navigable and announced to screen readers.

## Assumptions
- The API is read-only for this exercise; mutations are out of scope.
- "Recent" means the last 30 days by `createdAt` in the viewer's time zone.
- The dataset fits in memory (< 5k rows); virtualisation would be the next step beyond that.

## Architecture
Nuxt 4 with a thin server route (`server/api/orders.get.ts`) proxying and validating the upstream API, so the page uses
`useFetch` with a key and gets SSR data. Filter state lives in the URL; UI state in component refs; no global store
was needed. Formatting goes through `Intl`. Components: `OrdersTable`, `OrderDrawer`, `StatusFilter`.

## Decisions and trade-offs
- Sorting client-side: the dataset is small; server sorting would be the choice with pagination.
- No Pinia: shared state is limited to the URL and the fetched data; a store would add indirection without benefit.
- Nuxt UI for primitives: accessible table/drawer out of the box; a custom design system was out of scope.

## Tests
- `orders.filter.spec.ts` — filtering/sorting logic incl. empty input and equal dates.
- `OrdersTable.spec.ts` — renders rows, announces sort, opens the drawer with keyboard.

## Not done / next steps
- Virtualised rows for large datasets; server-side pagination.
- E2E test with Playwright; visual regression for the drawer.
- Optimistic status updates once the API supports writes.

Time spent: ~5 h (1 h reading/planning, 2.5 h build, 1 h tests/a11y, 0.5 h README).

A code-review drill with the answer you should give:

What is wrong here?
<script setup lang="ts">
const props = defineProps<{ items: Item[] }>()
const cart = reactive({ lines: [] as Line[] })         // module-scope store lives in a separate file in the real snippet

onMounted(async () => {
  const data = await $fetch('/api/prices')             // 1
  props.items.forEach(i => (i.price = data[i.id]))     // 2
})

const total = ref(0)
watch(() => cart.lines, () => { total.value = cart.lines.reduce((s, l) => s + l.qty * l.price, 0) }) // 3
</script>

<template>
  <li v-for="(item, index) in items" :key="index">    <!-- 4 -->
    <div v-html="item.description" />                 <!-- 5 -->
    <button @click="cart.lines.push({ ...item, qty: 1 })">Add</button>   <!-- 6 -->
  </li>
</template>
1. Fetching in onMounted in a Nuxt page loses SSR and shows a flash; use useFetch at setup with a key.
2. Mutating props (and the parent's objects) breaks one-way data flow; compute a derived list or emit.
3. watch on `() => cart.lines` is not deep and the derived value should be a computed(); also no cleanup concern here, but derived state as ref+watch is a smell.
4. index as key breaks state when items reorder or are removed; use item.id.
5. v-html with server or user-provided description is an XSS vector; render text or sanitise.
6. No accessible name beyond "Add" repeated N times; add aria-label with the item name; also pushing the item object itself shares references with the list.
Plus: no loading/error state; the store snippet at module scope would leak between users in SSR.

A refactoring drill outline, as you would narrate it:

Goal   "This 300-line component fetches, filters, sorts and renders; make it testable."
Step 0 Add a characterisation test: mount, assert rows and the sort behaviour as they are today.
Step 1 Extract pure logic: filterOrders(), sortOrders() into utils with unit tests (green).
Step 2 Extract a composable useOrders() holding fetch + derived state; component becomes a template with props/slots.
Step 3 Split the template: OrdersTable, OrdersFilters; pass data down, events up.
Step 4 Remove dead code and the duplicate formatting; run typecheck, lint, tests.
Stop   The goal was testability; the visual redesign someone mentions is a separate ticket.
Gotcha· The impressive take-home that does not run

A reviewer who cannot start the app in two minutes stops reading. Test the install from a fresh clone (or a fresh directory) with the pinned Node version before submitting, and make pnpm dev and pnpm test the only commands needed.

Gotcha· Defending instead of listening in a pairing session

When the interviewer suggests a different approach, they are testing collaboration as much as the idea. Consider it out loud, adopt it or explain briefly why not, and move on; ten minutes of debate is a failed session even if you were right.

Exercise

Exercise
  • Do a past take-home again in four hours with a timer; write the README first as a plan, then fill it in as you go.
  • Collect ten Vue/Nuxt snippets with a planted bug each (from your own history or the classics above) and practise the "what is wrong" answer in under a minute each.
  • Record yourself pairing with a friend on a small refactor; watch for silence, unstated plans and unheard hints.

Be able to say

Be able to say· How do you approach a take-home assignment?

"I read the brief twice and list the explicit requirements, the implicit ones and the ambiguities, which I resolve with stated assumptions in the README. Then I time-box: a working, idiomatic core in the first half, then correctness on edges, tests on the logic that can break, accessibility basics, and the README, with extras only if time remains. The architecture stays proportional to the problem, using the framework's conventions so the reviewer sees idioms rather than inventions. Commits are small and explain why. The README covers how to run and test it, the architecture, decisions with the alternatives I considered, what I left out and what I would do next, and the time I spent. Before submitting I install and run it from a clean clone."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.