-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
65: Typing Node.js APIs and Express Handlers
Up until now, we've been playing in a relatively controlled environment. But the real world is messy, and Node.js—specifically Express—is a great example of that. Express was written in JavaScript, and its types are provided via @types/express. Because Express is so flexible, it relies heavily on generics to let you define what's coming in and what's going out.
Let's build a small "Digital Library" API. We just need two endpoints: one to get a book by its ID and one to add a new book to our collection. I'll walk you through how I type these so we don't end up with any bleeding all over the codebase.
Defining our Domain Model
Before touching the server, I always start with the data. If I don't know what a "Book" looks like, I can't possibly type the request body that creates one. I'll keep it simple.
interface Book {
id: string;
title: string;
author: string;
publishedYear: number;
}
// A mock database for our example
const books: Book[] = [
{ id: '1', title: 'The TypeScript Guide', author: 'Jane Doe', publishedYear: 2021 },
];
Typing the Request Parameters
For our GET /books/:id route, the most important part is the id in the URL. In Express, the Request type takes four generics: Request<Params, ResBody, ReqBody, ReqQuery>. Most people ignore these and just use Request, but that leaves req.params as a collection of strings (or any in older versions).
Here is how I'd handle the fetch route:
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
app.get('/books/:id', (req: Request<{ id: string }>, res: Response) => {
const { id } = req.params;
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).send('Book not found');
}
res.json(book);
});
The Generic Order Trap
Now we want to add a POST /books route. This is where I usually trip up when I'm moving fast. I want the request body to be typed as a Book (well, a version of a Book without the ID, since the server generates that).
I'll start by defining the payload type:
type CreateBookDto = Omit<Book, 'id'>;
Now, here is the mistake I just made. I wrote this:
// WRONG: I'm putting the body type in the first generic slot
app.post('/books', (req: Request<CreateBookDto>, res: Response) => {
const { title } = req.body; // TS thinks title is part of the URL params, not the body!
// ...
});
If you look at the Request generic definition again, the first slot is for Params. By putting CreateBookDto there, I've told TypeScript that my URL looks like /books/:title/:author/:publishedYear. That's not what I want. I need to skip the Params and ResBody slots to get to the ReqBody.
Here is the corrected version:
app.post('/books', (req: Request<{}, {}, CreateBookDto>, res: Response) => {
const { title, author, publishedYear } = req.body;
const newBook: Book = {
id: Date.now().toString(),
title,
author,
publishedYear
};
books.push(newBook);
res.status(201).json(newBook);
});
Note the {}, {}. We're explicitly telling TypeScript: "There are no route params, and I'm not strictly typing the response body here (letting it be default)."
Typing Middleware and the NextFunction
Finally, let's add a simple validation middleware. This is where the NextFunction type comes into play. If you don't type the next callback, you lose the ability to properly handle error-passing in TypeScript.
import { Request, Response, NextFunction } from 'express';
const validateBookData = (req: Request, res: Response, next: NextFunction) => {
const { title } = req.body;
if (!title) {
// We can call next() or send a response
return res.status(400).json({ error: 'Title is required' });
}
next();
};
app.post('/books', validateBookData, (req: Request<{}, {}, CreateBookDto>, res: Response) => {
// ... implementation same as above
});
By being explicit with these generics, we've turned our Express handlers from "black boxes" into type-safe functions. You get autocomplete on req.body and req.params, and you'll catch errors before you even hit npm start.
📋 Practical Task
Exercise: Implementing a User Profile Update Route
You are building a user management system. You need to create a PUT /users/:id endpoint that allows a user to update their profile information.
Requirements:
- Define a
Userinterface withid (string),username (string), andemail (string). - Define an
UpdateUserDtotype that makesusernameandemailoptional (since a user might only update one of them). - Create the Express handler using the
Requestgeneric to ensure:- The
idis captured from the URL parameters. - The
UpdateUserDtois captured from the request body.
- The
- Implement a basic check: if the
idis "0", return a403 Forbiddenresponse. Otherwise, return a200 OKwith a message saying "User updated".
Starter Code:
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
// Your code here:
// 1. Define User and UpdateUserDto
// 2. Implement the PUT /users/:id handler
There are no comments for now.