Skip to content
TypeScript ts node 3 min read

Request & Response Types

Express’s Request and Response types are generic, which is the key to typing the data flowing through a route. By default req.params, req.body, and req.query are loosely typed, so any property access compiles. By supplying type arguments you tell the compiler the exact shape of each part of the request, turning silent runtime bugs into compile-time errors. This page explains every slot in the generic Request, how to type the response body, and patterns for keeping handlers readable.

The shape of generic Request

Request accepts four type parameters in a fixed order. Knowing the order matters because you often want to specify a later one (like the body) while leaving earlier ones at their defaults.

// Request<Params, ResBody, ReqBody, ReqQuery>
import type { Request, Response } from "express";
PositionParameterTypesDefault
1Paramsreq.paramsParamsDictionary
2ResBodybody passed to res.json/res.sendany
3ReqBodyreq.bodyany
4ReqQueryreq.queryParsedQs

Note the slightly surprising ordering: the response body type sits in position two, between the route params and the request body. The matching Response<ResBody> takes only the body type.

Typing route parameters

Path parameters arrive as strings, so model Params as an object of string-valued keys. The compiler then flags any param name you did not declare in the route.

import type { Request, Response } from "express";

app.get("/users/:id", (req: Request<{ id: string }>, res: Response) => {
  const id = Number(req.params.id); // req.params.id: string
  // req.params.name; // ❌ Error: Property 'name' does not exist
  res.json({ id });
});

Route params are always strings even for numeric-looking segments, so convert them explicitly. Declaring { id: string } documents the route’s contract and prevents typos against req.params.

Typing the request body

To type req.body, fill positions two and three. Use an underscore-style placeholder type for the response body when you only care about the request body, or specify both for full coverage.

interface CreateUser {
  name: string;
  email: string;
}

interface UserResponse {
  id: number;
  name: string;
}

app.post(
  "/users",
  (req: Request<{}, UserResponse, CreateUser>, res: Response<UserResponse>) => {
    const { name, email } = req.body; // fully typed
    const user = { id: 1, name };
    res.json(user); // ✅ must match UserResponse
    // res.json({ wrong: true }); // ❌ Error: not assignable to UserResponse
  },
);

Because Response<UserResponse> constrains res.json, returning the wrong shape is a compile error. Remember that typing req.body reflects your intent, not validated reality — the body is still whatever the client sent, so validate it (see the environment and middleware pages) before trusting it.

Typing the query string

Query values are typed as ParsedQs by default, meaning each value is string | string[] | ParsedQs | undefined. You can narrow it, but values are still strings at runtime, so a parsing step is unavoidable.

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

app.get(
  "/search",
  (req: Request<{}, any, any, SearchQuery>, res: Response) => {
    const term = req.query.q ?? "";
    const page = Number(req.query.page ?? "1");
    res.json({ term, page });
  },
);

Typing req.query as { page: number } is tempting but wrong — Express never converts query values to numbers. Keep the declared types as strings and convert (or validate with a schema) inside the handler.

A reusable handler alias

Repeating the four-parameter Request on every handler is noisy. Capture the combination in a type alias, or use the dedicated RequestHandler generic, to keep signatures clean and consistent.

import type { RequestHandler } from "express";

// RequestHandler<Params, ResBody, ReqBody, ReqQuery>
const createUser: RequestHandler<{}, UserResponse, CreateUser> = (req, res) => {
  res.status(201).json({ id: 1, name: req.body.name });
};

app.post("/users", createUser);

With RequestHandler, req and res are inferred from the type arguments, so you write the generics once at the declaration instead of annotating both parameters inline.

Best Practices

  • Remember the order: Request<Params, ResBody, ReqBody, ReqQuery>.
  • Type route params as objects of string values and convert numbers explicitly.
  • Constrain the response with Response<ResBody> so wrong payloads fail to compile.
  • Treat typed req.body as intent only; validate untrusted input at runtime.
  • Leave req.query values as strings in types; parse them inside the handler.
  • Use RequestHandler<...> or a shared alias to avoid repeating four-parameter generics.
Last updated June 29, 2026
Was this helpful?