TypeScript with Node/Express
Part of the Full Stack Developer Roadmap
In short
On the server, wrong data doesn't just look ugly, it corrupts your database. Typing Express handlers, request bodies and responses brings the same safety you have in React to the backend.
Typing your Express route handlers, request/response objects and middleware — bringing the same safety to your backend that you get on the frontend.

Think of it like this
Petrol and diesel nozzles
At the pump, the diesel and petrol nozzles look different and are kept apart, so nobody fills a scooter with diesel. Types do that on your server: a request body or response of the wrong shape is caught before it reaches the engine.
Watch it happen
A typed POST /api/tasks
1/5req.body arrives as plain JSON. TypeScript can't know what's inside, so treat it as unknown.
The big ideas
What you need to know
Install the types
Add @types/express and @types/node as dev dependencies. Express 5's types come from @types/express v5.
Response generics
Response<Task> types what res.json accepts. Request<Params, ResBody, ReqBody> exists too, but parse bodies rather than trusting them.
Extend Request for req.user
Declare a user property on Express's Request interface once, so auth middleware and handlers agree on its type.
Run it simply
Use tsx in development, and tsc to build for production. Recent Node versions can also strip types and run .ts directly.
See it in code
Walk through the code
1import express, { type Request, type Response, type NextFunction } from "express";2import { z } from "zod";3 4const NewTask = z.object({ title: z.string().min(1), due: z.coerce.date().optional() });5type Task = z.infer<typeof NewTask> & { id: number; done: boolean };6type ApiError = { error: string };7 8const app = express();9app.use(express.json());10app.post("/api/tasks", (req: Request, res: Response<Task | ApiError>) => {11 const parsed = NewTask.safeParse(req.body); // req.body is untrusted until parsed12 if (!parsed.success) return res.status(400).json({ error: "Title is required" });13 res.status(201).json({ id: Date.now(), done: false, ...parsed.data });14});15app.use((err: Error, req: Request, res: Response<ApiError>, next: NextFunction) => {16 res.status(500).json({ error: "Something broke on our side" }); });1/4The schema defines valid input. The Task type is built from it plus server-added fields.
One schema gives you both runtime validation and the compile-time Task type.
Save yourself the pain
Common mistakes
Oops
Typing the body as Request<{}, {}, Task> and using req.body.title directly.
Do this instead
That only tells TS to trust the client. Parse req.body with a schema, then use the parsed result.
Oops
Using req.user everywhere with // @ts-ignore.
Do this instead
Add a declaration file that extends Express's Request type with user, once for the whole app.
Your mission
Type an existing API
Quick check
You type a handler as Request<{}, {}, { title: string }>. A client sends { title: 42 }. What happens?
Go deeper
- 1Set up a Node + Express + TypeScript project from scratch.
- 2Type your route handlers' request body and response shapes using your shared types.
- 3Use `ts-node` or `tsx` for a fast local development loop.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.