DEV Community

Uroš Štok
Uroš Štok

Posted on • Originally published at urosstok.com

Typed routes in Express

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);
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
        ...
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)