Typing Node.js & Express APIs

0/5 in this phase0/21 across the roadmap

📖 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

codeTap to expand ⛶
1// Setup: npm i express && npm i -D @types/express @types/node typescript
2// tsconfig: "types": ["node"]
3
4import express, { Request, Response, NextFunction } from "express";
5
6// Define DTOs
7interface CreateUserDTO {
8 name: string;
9 email: string;
10 password: string;
11}
12
13interface UserResponse {
14 id: string;
15 name: string;
16 email: string;
17 createdAt: string;
18}
19
20interface ApiError {
21 message: string;
22 code: string;
23 details?: Record<string, string[]>;
24}
25
26// Typed request handler
27// 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;
33
34 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 }
44
45 const user: UserResponse = {
46 id: crypto.randomUUID(),
47 name,
48 email,
49 createdAt: new Date().toISOString()
50 };
51
52 return res.status(201).json(user);
53};
54
55// Typed params
56interface 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 user
65};
66
67// Typed query parameters
68interface 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};
81
82// Module augmentation — add user to Request
83declare global {
84 namespace Express {
85 interface Request {
86 user?: { id: string; role: "admin" | "user" };
87 }
88 }
89}
90
91// Typed middleware
92const authMiddleware = (
93 req: Request,
94 res: Response,
95 next: NextFunction
96) => {
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};
104
105// Error handling middleware (4 params = error handler)
106const errorHandler = (
107 err: Error,
108 req: Request,
109 res: Response,
110 next: NextFunction
111) => {
112 console.error(err.stack);
113 res.status(500).json({
114 message: "Internal server error",
115 code: "INTERNAL_ERROR"
116 });
117};
118
119// App setup
120const app = express();
121app.use(express.json());
122app.post("/users", createUser);
123app.get("/users/:id", authMiddleware, getUser);
124app.get("/users", listUsers);
125app.use(errorHandler);
126
127console.log("Express app with TypeScript types");

🏋️ Practice Exercise

Mini Exercise:

  1. Type a full CRUD Express router for a "Post" resource with proper DTOs
  2. Create typed middleware for authentication that augments the Request type
  3. Use Zod to validate request bodies and infer TypeScript types from schemas
  4. Type an error-handling middleware that handles different error subclasses
  5. 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 any at runtime. Use Zod/joi for validation

  • Using any for 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.page is string | undefined, not number

  • Not augmenting Request type for middleware-added properties — without declaration merging, req.user won't be typed

  • Typing 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