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";
| Position | Parameter | Types | Default |
|---|---|---|---|
| 1 | Params | req.params | ParamsDictionary |
| 2 | ResBody | body passed to res.json/res.send | any |
| 3 | ReqBody | req.body | any |
| 4 | ReqQuery | req.query | ParsedQs |
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.queryas{ 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
stringvalues and convert numbers explicitly. - Constrain the response with
Response<ResBody>so wrong payloads fail to compile. - Treat typed
req.bodyas intent only; validate untrusted input at runtime. - Leave
req.queryvalues as strings in types; parse them inside the handler. - Use
RequestHandler<...>or a shared alias to avoid repeating four-parameter generics.