5 TypeScript Patterns I Reach for in Every Node.js API
May 10, 2026
Five practical TypeScript patterns for Node.js and Express APIs — request validation, discriminated unions for responses, and typing async error handling properly.
TypeScript's value in a Node.js API isn't really about catching typos — it's about making illegal states unrepresentable and pushing errors to compile time instead of a 2am production incident. Here are five patterns that show up in almost every API I build, regardless of the domain.
1. Validate at the boundary, trust the type after that
Request bodies come in as `unknown`, no matter what your route handler's type signature claims. Using a schema validator (Zod is the one I reach for) at the very edge of the request — parsing the body once, immediately — means everything past that point can be fully typed and trusted. Skipping this and just casting `req.body as MyType` is the single most common way a 'type-safe' API still crashes on bad input.
2. Discriminated unions for API responses
Modeling a response as `{ success: true, data: T } | { success: false, error: string }` instead of a single object with optional fields forces every caller to narrow the type before accessing `data`, which means the compiler catches the 'forgot to check for the error case' bug that would otherwise only show up at runtime.
3. Branded types for IDs that look alike
A `userId: string` and an `orgId: string` are trivially easy to swap by accident, and plain TypeScript won't catch it because they're structurally identical. Branding them — `type UserId = string & { readonly __brand: unique symbol }` — makes them incompatible at compile time even though they're both strings at runtime, which catches an entire category of 'passed the wrong ID' bugs for free.
4. Typing async errors instead of throwing anything
`try/catch` in TypeScript types the caught error as `unknown` by default, which is correct but easy to ignore by immediately casting it to `Error`. A small `Result<T, E>` wrapper around functions that can fail — returning `{ ok: true, value } | { ok: false, error }` instead of throwing — keeps error types explicit through the call chain and avoids losing information across `catch` boundaries.
5. Exhaustiveness checks on switch statements
Adding a `default` case that assigns to a variable typed as `never` means that if someone adds a new variant to a union later and forgets to handle it in a switch statement, the build fails instead of silently falling through. It's a small amount of ceremony that pays for itself the first time a new status or event type gets added to a growing codebase.
None of these are exotic — they're mostly about being deliberate at the boundaries (input validation, error handling, response shapes) where untyped chaos tends to sneak in, and letting the compiler do the rest.