← lakshya.me / notes / express-drill
Machine coding · Node + Express 5 + TypeScript + Zod

Express Server Drill

The skeleton of your node-backend repo, cleaned up and laid out in the order you'd type it in an interview. Read the blueprint, study the files, then drill until you can write the whole thing from a blank folder in 15 minutes.

How a request travels

A request goes in from the outside. You build a module from the inside out: model, schema, repository, service, controller, routes. Each layer has one job and one thing it must never do.

task.routes.ts

Route

Maps verb + path to a handler and attaches middleware such as authenticate.

Never holds logic.

task.controller.ts

Controller

Reads req, parses input with Zod, calls the service, sends the status code and JSON.

Never touches the database.

task.service.ts

Service

Holds the business rules: ownership checks and invariants. Throws NotFoundError and the other AppErrors.

Never sees req or res.

task.repository.ts

Repository

Data access behind an interface: an in-memory Map today, Prisma tomorrow.

Never throws HTTP errors.

error.middleware.ts

Error handler

Every throw lands here: Zod → 400, AppError → its own status, anything else → 500.

Always registered last.

The 45-minute clock

  1. 00–05

    Server answering. Run npm init, install, write tsconfig, then app.ts + index.ts with /health. Hit it once in the terminal.

  2. 05–12

    Safety net. Write errors.ts, the error middleware and the 404 handler. Every later bug now comes back as clean JSON.

  3. 12–30

    One entity, inside out. Model → Zod schema → repository interface + in-memory class → service → controller → routes → wire up in app.ts.

  4. 30–38

    The extras they asked for. Pagination, filters, a role check with authorize("ADMIN"), a second entity.

  5. 38–45

    Demo and talk. Run curl through the happy path and one error, then explain how you'd swap in Prisma, handle concurrency and add tests.

Folder layout

src/
  config/env.ts            # zod-validated process.env
  common/
    errors.ts              # AppError + subclasses, one file
    http.ts                # ok() response helper
    error.middleware.ts
    auth.middleware.ts     # authenticate + authorize
  modules/tasks/
    task.model.ts
    task.schema.ts         # zod schemas + inferred types
    task.repository.ts     # interface + InMemory impl
    task.service.ts
    task.controller.ts
    task.routes.ts
  types/express.d.ts       # req.user typing
  app.ts                   # createApp(): wiring only
  index.ts                 # listen + shutdown

Pocket syntax

The lines that go blank under pressure.

Express request & response

req.params.id
/tasks/:id. Always a string, so coerce it.
req.query.page
?page=2. String or array, so coerce it.
req.body
Needs app.use(express.json()). Undefined without it.
req.headers.authorization
Header names are lowercase.
req.cookies
Needs app.use(cookieParser()).
res.status(201).json(x)
Chainable; one response per request.
res.status(204).end()
Success with no body.

Zod v4

z.coerce.number().int()
Turns "42" into 42 for params and query values.
z.stringbool()
Turns "false" into false. z.coerce.boolean() gives true.
z.email()
Top-level in v4. z.string().email() is the old form.
.default(1) / .optional()
A default fills in a missing key.
.refine(fn, "msg")
Cross-field rules.
schema.parse(x)
Throws ZodError, which the error handler turns into a 400.
z.infer<typeof s>
The TS type, for free.

Order & gotchas

app.use order
json → routes → 404 → error handler.
(err, req, res, next)
Exactly 4 params, or Express won't treat it as an error handler.
Express 5 async
Thrown errors and rejected promises reach next(err) automatically. No try/catch needed.
Arrow methods
create = async (req, res) => {} keeps this bound when you pass c.create.
".js" imports
ESM + NodeNext: import ./x.js even though the file is x.ts.
"types": ["node"]
Required in tsconfig on TS 6+. Without it, process and Buffer show red lines.
req.query is read-only
In Express 5 it's a getter, so parse it into a local variable.