How to Explicitly Specify Types for Express Request and Response Objects in TypeScript

Use the generic parameters provided by @types/express to type Request<Params, ResBody, ReqBody, ReqQuery> and Response<ResBody>, enabling compile-time validation of route data, request bodies, and JSON responses.

The expressjs/express repository provides the runtime implementation, while the companion @types/express package supplies the TypeScript declarations that describe the framework's public API. Explicitly specifying types for Express Request and Response objects transforms loosely typed JavaScript handlers into self-documenting, refactor-safe code that catches mismatches between your route contracts and implementation at compile time.

Understanding the Generic Type Signatures

The Express type definitions expose four generic parameters on the Request interface and one on Response. These map directly to the data extraction logic found in the source code.

  • Request<Params, ResBody, ReqBody, ReqQuery> corresponds to req.params, req.res, req.body, and req.query as implemented in lib/request.js.
  • Response<ResBody> describes the payload sent by res.json(), which the runtime serializes using JSON.stringify (see lib/response.js lines 31-46).

When you import { Request, Response, NextFunction } from express, you gain access to these generics to lock down the shape of data flowing through your application.

Typing Route Parameters and Query Strings

Route parameters are parsed from the URL pattern by the router logic in lib/router/index.js and attached to req.params. Use the first generic parameter to enforce their types.

import { Request, Response, NextFunction } from 'express';

interface UserParams {
  id: string;  // /users/:id
}

export const getUser = (
  req: Request<UserParams>,
  res: Response,
  next: NextFunction
) => {
  const userId = req.params.id;  // ✅ TypeScript knows this is a string
  res.json({ id: userId, name: 'Alice' });
};

For query strings, which Express parses using the qs library (or Node's built-in url.parse depending on configuration), use the fourth generic parameter.

interface SearchQuery {
  q: string;
  page?: number;
}

export const search = (
  req: Request<{}, any, any, SearchQuery>,
  res: Response,
  next: NextFunction
) => {
  const term = req.query.q;         // ✅ string
  const page = req.query.page ?? 1; // ✅ number | undefined
  res.json({ term, page });
};

Typing Request Bodies and JSON Responses

Middleware like express.json() processes the raw body and assigns it to req.body. Type this using the third generic parameter, and enforce response contracts with Response<ResBody>.

interface CreateUserBody {
  name: string;
  email: string;
  age?: number;
}

interface UserResponse {
  id: string;
  name: string;
  email: string;
  age?: number;
}

export const createUser = (
  req: Request<{}, UserResponse, CreateUserBody>,
  res: Response<UserResponse>,
  next: NextFunction
) => {
  const { name, email, age } = req.body;  // ✅ name and email are required strings
  const newUser: UserResponse = { id: '123', name, email, age };
  res.status(201).json(newUser);          // ✅ Must match UserResponse shape
};

The second generic parameter on Request (shown as UserResponse above) types req.res, which is useful when accessing the response object from within middleware chains.

Typing Middleware and res.locals

Middleware functions often augment the request or response objects. Type res.locals by passing an interface to the Response generic, and always type next as NextFunction to prevent misuse in error-handling flows.

interface MyLocals {
  user?: { id: string; role: string };
}

export const authMiddleware = (
  req: Request,
  res: Response<MyLocals>,
  next: NextFunction
) => {
  res.locals.user = { id: '123', role: 'admin' };
  next();
};

Centralizing Types and Enabling Strict Checks

Maintain a dedicated types/ directory for interfaces shared across handlers. This centralizes your API contract and simplifies refactoring when the underlying data model changes.

// types/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

// handlers/user.ts
import { Request, Response, NextFunction } from 'express';
import { User } from '../types/user';

export const getUser = (
  req: Request<{ id: string }>,
  res: Response<User>,
  next: NextFunction
) => {
  const user: User = { id: req.params.id, name: 'Bob', email: 'bob@example.com' };
  res.json(user);
};

Enable strict compiler options in tsconfig.json to ensure these explicit typings enforce compile-time validation:

  • strict: true
  • noImplicitAny: true
  • strictNullChecks: true

Summary

  • Import Request, Response, and NextFunction from express to access the official type definitions that mirror the runtime implementation in lib/request.js and lib/response.js.
  • Use Request<Params, ResBody, ReqBody, ReqQuery> to explicitly type route parameters, response bodies, request bodies, and query strings.
  • Apply Response<ResBody> to enforce the shape of JSON payloads sent via res.json().
  • Type res.locals through the Response generic when building middleware that attaches data to the response cycle.
  • Store shared interfaces in a central location and enable strict TypeScript options to maximize compile-time safety.

Frequently Asked Questions

How do I type a request body when using express.json() middleware?

Define an interface describing the expected JSON structure and pass it as the third generic argument to Request. For example, req: Request<{}, any, CreateUserBody> ensures req.body matches your CreateUserBody interface after the middleware in lib/request.js processes the payload.

Can I override the default types for req.params without affecting other handlers?

Yes. Express's Request type is generic, allowing you to specify route-local parameter shapes like Request<{ id: string }> for one handler and Request<{ orgId: string; userId: string }> for another. These declarations are scoped to the individual function signature and do not pollute the global Express namespace.

What is the purpose of the second generic parameter on Request?

The second parameter types the response body that req.res will eventually send (ResBody). While optional, it is useful when your handler logic inspects req.res or when you want to ensure consistency between the request handler and the eventual JSON payload shape defined in lib/response.js.

Should I install @types/express separately or is it included?

You must install @types/express as a dev dependency when working with TypeScript, as the expressjs/express repository itself contains only JavaScript source code (including package.json which lists Node >=18 as the engine). The type definitions reside in the DefinitelyTyped repository and provide the generic signatures necessary for explicitly specifying types in your application.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →