Typed routes in Express

by Uroš Štok DEC 18, 2021, 6 min read

While Express wasn't built with Typescript, there are type definitions available - @types/express. This adds typings for routes (specifically for this post, Request and Response).

I've looked around for ways of properly doing Request and Response types, and haven't found anything that works without breaking something else or being complicated. So here's how I usually implement typesafety into express routes.

Let's say we had an endpoint for adding a new user:

import express from "express";

const app = express();

app.post("/user", (req, res) => {
    req.body.name; // autocomplete doesn't work
});

app.listen(3000);

This is pretty standard javascript, besides using ESM imports, there's no reason we need typescript for this. So let's add some types:

import express, {Request, Response} from "express";
...
app.post("/user", (req: Request, res: Response) => {
    req.body.name; // autocomplete doesn't work
});

Note that this is what happens normally even if we don't specify the types, typescript infers the Request and Response type from the function automatically. So we didn't really do much here.

Request.body type

What if this endpoint needs some input body data? Currently when we type req.body autocomplete doesn't offer anything special. Let's change that.

We can pass an interface to the Request type parameter list so that Typescript knows what variables are available in req.body. It would look something like this:

type UserRequestBody = { name: string };
app.post("/user", (req: Request<{}, {}, UserRequestBody>, res: Response) => {
    req.body.name; // autocomplete works
});

We need to put {} for the first two parameters as the thing we want (body) is actually the third type parameter. As we can see in the Request definition:

interface Request<
        P = core.ParamsDictionary,
        ResBody = any,
        ReqBody = any, // this is the Request.body
        ...

Now this is quite chunky code for simply passing an interface for the request body. Luckily there's a better way, we simply define a helper type:

type RequestBody<T> = Request<{}, {}, T>;

With our cleaner definition we can simply use:

type RequestBody<T> = Request<{}, {}, T>;

type UserRequestBody = { name: string };
app.post("/user", (req: RequestBody<UserRequestBody>, res: Response) => {
    req.body.name; // autocomplete works
});

Other defintions

Now with our new found knowledge of how to write clean route typed code we can declare helper types for all our use cases!

// for .body
type RequestBody<T> = Request<{}, {}, T>;
// for .params
type RequestParams<T> = Request<T>;
// for .query
type RequestQuery<T> = Request<{}, {}, {}, T>;
// and so on... similarly for Response

Multiple types

To cover everything, we need to be able to specify multiple types, for example .body and .params. We can do so by simply adding a new type:

type RequestBodyParams<TBody, TParams> = Request<TParams, {}, TBody>

Typed example

Here's the full example from the start, now with typed routes:

import express, {Request, Resposne} from "express";

const app = express();

type RequestBody<T> = Request<{}, {}, T>;
type UserRequestBody = { name: string };
app.post("/user", (req: RequestBody<UserRequestBody>, res: Response) => {
    req.body.name; // autocomplete works
});

app.listen(3000);

Closing notes

That's it! This should allow you to create proper typed routes. The next step would be to add schema validation for these routes.