Skip to content
TypeScript ts node 3 min read

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

SymptomCauseFix
Could not find a declaration file for 'express'@types/express missingInstall it as a dev dependency
req.body is anyBody type is unconstrained by defaultUse Request<…, …, Body> generics
Handler return type errorsReturning res.json(...) from a typed handlerType the handler as RequestHandler, not by return
Async error never caughtExpress 4 ignores rejected promisestry/catch and call next(err)

Returning res.send(...) from a handler is fine, but do not annotate a handler’s return type as Response — Express handlers are expected to return void (or Promise<void>). Use the RequestHandler type instead of inferring from the return.

Best Practices

  • Always install @types/express and a matching @types/node; Express ships no built-in types.
  • Let app.get/router.post infer handler parameter types instead of annotating every one.
  • Use a typed Router per 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 RequestHandler rather than annotating the return value.
  • Keep strict enabled so missing next calls and untyped req.body surface early.
Last updated June 29, 2026
Was this helpful?