Typing Node.js & Express APIs
📖 Concept
TypeScript transforms Node.js/Express development — catching request/response bugs, typing middleware, and ensuring API contracts at compile time.
Key packages:
@types/node— Type definitions for Node.js APIs@types/express— Type definitions for Express
Request/Response typing:
Express generics let you type the request body, params, query, and response body: Request<Params, ResBody, ReqBody, Query>.
Middleware typing: Properly typed middleware passes type information through the chain — each middleware can augment the Request type.
DTO pattern (Data Transfer Objects): Define interfaces for request/response shapes and validate at runtime (with Zod, io-ts, or manual guards). TypeScript types alone don't validate runtime data — you need both.
Module augmentation extends Express types — adding user to Request for auth middleware.
🏠 Real-world analogy: Typing Express APIs is like having a contract for every API endpoint. The contract specifies exactly what must be sent (request body), what will be returned (response), and what can go wrong (error types). Both the client and server agree on the contract before any code runs.
💻 Code Example
1// Setup: npm i express && npm i -D @types/express @types/node typescript2// tsconfig: "types": ["node"]34import express, { Request, Response, NextFunction } from "express";56// Define DTOs7interface CreateUserDTO {8 name: string;9 email: string;10 password: string;11}1213interface UserResponse {14 id: string;15 name: string;16 email: string;17 createdAt: string;18}1920interface ApiError {21 message: string;22 code: string;23 details?: Record<string, string[]>;24}2526// Typed request handler27// Request<Params, ResBody, ReqBody, Query>28const createUser = async (29 req: Request<{}, UserResponse | ApiError, CreateUserDTO>,30 res: Response<UserResponse | ApiError>31) => {32 const { name, email, password } = req.body;3334 if (!name || !email) {35 return res.status(400).json({36 message: "Validation failed",37 code: "VALIDATION_ERROR",38 details: {39 name: !name ? ["Name is required"] : [],40 email: !email ? ["Email is required"] : []41 }42 });43 }4445 const user: UserResponse = {46 id: crypto.randomUUID(),47 name,48 email,49 createdAt: new Date().toISOString()50 };5152 return res.status(201).json(user);53};5455// Typed params56interface GetUserParams {57 id: string;58}59const getUser = async (60 req: Request<GetUserParams>,61 res: Response<UserResponse | ApiError>62) => {63 const { id } = req.params; // Type: string (typed!)64 // ... fetch user65};6667// Typed query parameters68interface ListUsersQuery {69 page?: string;70 limit?: string;71 sort?: "name" | "createdAt";72}73const listUsers = async (74 req: Request<{}, UserResponse[], {}, ListUsersQuery>,75 res: Response<UserResponse[]>76) => {77 const page = parseInt(req.query.page ?? "1");78 const limit = parseInt(req.query.limit ?? "10");79 // ...80};8182// Module augmentation — add user to Request83declare global {84 namespace Express {85 interface Request {86 user?: { id: string; role: "admin" | "user" };87 }88 }89}9091// Typed middleware92const authMiddleware = (93 req: Request,94 res: Response,95 next: NextFunction96) => {97 const token = req.headers.authorization?.replace("Bearer ", "");98 if (!token) {99 return res.status(401).json({ message: "Unauthorized", code: "AUTH_ERROR" });100 }101 req.user = { id: "user-123", role: "admin" }; // Type-safe!102 next();103};104105// Error handling middleware (4 params = error handler)106const errorHandler = (107 err: Error,108 req: Request,109 res: Response,110 next: NextFunction111) => {112 console.error(err.stack);113 res.status(500).json({114 message: "Internal server error",115 code: "INTERNAL_ERROR"116 });117};118119// App setup120const app = express();121app.use(express.json());122app.post("/users", createUser);123app.get("/users/:id", authMiddleware, getUser);124app.get("/users", listUsers);125app.use(errorHandler);126127console.log("Express app with TypeScript types");
🏋️ Practice Exercise
Mini Exercise:
- Type a full CRUD Express router for a "Post" resource with proper DTOs
- Create typed middleware for authentication that augments the Request type
- Use Zod to validate request bodies and infer TypeScript types from schemas
- Type an error-handling middleware that handles different error subclasses
- Create a generic typed controller factory:
createCRUD<TEntity, TCreate, TUpdate>()
⚠️ Common Mistakes
Not validating request bodies at runtime — TypeScript types are compile-time only; req.body is
anyat runtime. Use Zod/joi for validationUsing
anyfor request/response types — defeats the purpose; always type the generics:Request<Params, ResBody, ReqBody, Query>Forgetting that Express query params are always strings —
req.query.pageisstring | undefined, notnumberNot augmenting Request type for middleware-added properties — without declaration merging,
req.userwon't be typedTyping the response but not enforcing it —
res.json()doesn't validate the shape; it trusts your types. Bugs can still occur
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Typing Node.js & Express APIs