Skip to Content
Course content

65: Typing Node.js APIs and Express Handlers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 User interface with id (string), username (string), and email (string).
  • Define an UpdateUserDto type that makes username and email optional (since a user might only update one of them).
  • Create the Express handler using the Request generic to ensure:
    • The id is captured from the URL parameters.
    • The UpdateUserDto is captured from the request body.
  • Implement a basic check: if the id is "0", return a 403 Forbidden response. Otherwise, return a 200 OK with 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
Rating
0 0

There are no comments for now.

to be the first to leave a comment.