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.