Typing Express Apps
Express ships as plain JavaScript, so TypeScript needs the community-maintained @types/express package to understand its Application, Router, Request, and Response shapes. Once installed, every route handler is type-checked: the compiler knows what res.json() accepts, flags typos in status methods, and infers the types of values you pull off the request. This page covers installing the types, typing the app and routers, writing handlers that infer correctly, and the small gotchas that trip up new TypeScript-on-Express projects.
Installing the types
Express does not bundle its own declarations, so add @types/express as a dev dependency alongside Express itself. The types depend transitively on @types/node, so install that too if you have not already.
npm install express
npm install --save-dev @types/express @types/node
With the declarations present, importing express gives you a fully typed module. There is nothing to configure in tsconfig.json beyond having node_modules/@types on the default type roots, which is the default.
Typing the application
The default export of express is a function that returns an Application. You rarely need to annotate it by hand — the type flows from the call — but importing the Express type is useful when you pass the app between functions, such as a createApp() factory.
import express, { type Express, type Router } from "express";
const app: Express = express();
app.use(express.json());
app.get("/health", (_req, res) => {
res.json({ status: "ok" });
});
app.listen(3000, () => console.log("listening on :3000"));
The express.json() body parser is built in, so no extra @types package is needed for it. Because the handler is inferred from app.get, res is typed as Response and autocompletes .json, .status, .send, and the rest of the response API.
Typed routers
Split routes into Router instances for larger apps. A router has the same handler-registration methods as the app, so handlers are typed identically. Annotate the router only when you export it from a module.
import { Router } from "express";
const users: Router = Router();
users.get("/", (_req, res) => {
res.json([{ id: 1, name: "Ada" }]);
});
users.post("/", (req, res) => {
res.status(201).json({ id: 2, ...req.body });
});
export default users;
Mount it back on the app with app.use("/users", users). The compiler verifies that what you pass to app.use is a valid handler or router, catching the common mistake of passing a Promise instead of a function.
Handler typing and async
Express handlers receive (req, res, next). When you register a handler inline, all three are inferred. Async handlers work, but Express (before v5) does not await your function, so a rejected promise will not reach the error middleware unless you forward it.
import type { Request, Response, NextFunction } from "express";
app.get("/report", async (req: Request, res: Response, next: NextFunction) => {
try {
const data = await buildReport(req.query);
res.json(data);
} catch (err) {
next(err); // forward to error-handling middleware
}
});
In Express 4, an async handler that throws will hang the request unless you catch and call next(err). Express 5 forwards rejected promises automatically. The next pages cover the generic Request parameters and dedicated middleware types in detail.
Common type pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Could not find a declaration file for 'express' | @types/express missing | Install it as a dev dependency |
req.body is any | Body type is unconstrained by default | Use Request<…, …, Body> generics |
| Handler return type errors | Returning res.json(...) from a typed handler | Type the handler as RequestHandler, not by return |
| Async error never caught | Express 4 ignores rejected promises | try/catch and call next(err) |
Returning
res.send(...)from a handler is fine, but do not annotate a handler’s return type asResponse— Express handlers are expected to returnvoid(orPromise<void>). Use theRequestHandlertype instead of inferring from the return.
Best Practices
- Always install
@types/expressand a matching@types/node; Express ships no built-in types. - Let
app.get/router.postinfer handler parameter types instead of annotating every one. - Use a typed
Routerper resource and mount routers on the app for scalable structure. - Forward errors with
next(err)in async handlers on Express 4; rely on auto-forwarding only in v5. - Type handlers as
RequestHandlerrather than annotating the return value. - Keep
strictenabled so missingnextcalls and untypedreq.bodysurface early.