Types for the data: DTOs
A DTO (Data Transfer Object) describes the shape of the data that enters
and leaves your API. You model it with an interface or a type:
// What the client sends to create a user (request body)
interface CreateUserDTO {
name: string;
email: string;
age?: number; // optional
}
// What the API returns
interface UserResponse {
id: number;
name: string;
email: string;
}
interface and type are almost interchangeable for describing objects. One
practical difference: type also names unions and primitives
(type Id = number | string), while interface focuses on the shape
of objects and can be extended.
Typing req and res
Express exposes generic types Request and Response. The signature's
parameters let you type req.params, the body and the response:
import { Request, Response } from "express";
// Request<Params, ResBody, ReqBody>
app.post(
"/users",
(req: Request<{}, UserResponse, CreateUserDTO>, res: Response<UserResponse>) => {
const { name, email } = req.body; // req.body: CreateUserDTO
const user = { id: 1, name, email };
res.status(201).json(user); // res.json expects UserResponse
}
);
Now the editor autocompletes req.body.name and flags an error if
you try res.json({ foo: 1 }), because it doesn't match UserResponse.
Route parameters are typed the same way:
app.get("/users/:id", (req: Request<{ id: string }>, res: Response) => {
const id = req.params.id; // string (params ALWAYS arrive as string)
res.json({ id });
});
Basic generics
A generic is a type with a "hole" that gets filled in when you use it: it lets you write a reusable piece without losing the type information.
interface ApiResponse<T> {
ok: boolean;
data: T;
}
const r1: ApiResponse<UserResponse> = { ok: true, data: { id: 1, name: "Ada", email: "a@x.com" } };
const r2: ApiResponse<number[]> = { ok: true, data: [1, 2, 3] };
ApiResponse<T> is the same wrapper for any data: the <T>
preserves the concrete type at each use. It's exactly what Express does with
Request<Params, ResBody, ReqBody>.