REST API with Express + TypeScript
In this page:
Core Concept
Building a REST API with Express and TypeScript means typing your route handlers' request and response objects, so accessing req.body.email is checked against a declared shape instead of silently being any.
Example: Core Concept
// import { Request, Response } from "express";
interface CreateUserBody { email: string; }
// app.post("/users", (req: Request<{}, {}, CreateUserBody>, res: Response) => {
// console.log(req.body.email);
// });
console.log("req.body is checked against a declared shape, not any");
Basic Setup
A basic setup installs @types/express alongside express itself, and typed handlers use Request<Params, ResBody, ReqBody> generics to describe exactly what's expected on each route.
Example: Basic Setup
// npm install express
// npm install --save-dev @types/express
console.log("@types/express enables Request<Params, ResBody, ReqBody> generics");
Typed Example
A typed example: app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response<UserResponse>) => { ... }) ensures the handler's req.body is checked against CreateUserBody and its res.json(...) calls are checked against UserResponse.
Example: Typed Example
interface CreateUserBody { email: string; }
interface UserResponse { id: number; email: string; }
// app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response<UserResponse>) => {
// res.json({ id: 1, email: req.body.email });
// });
console.log("Both req.body and res.json() are checked against declared types");
Project Usage
In a real project, sharing these request/response interfaces with a frontend client (via a shared-types package) keeps the API contract enforced on both ends without either side drifting out of sync silently.
Example: Project Usage
interface UserResponse { id: number; email: string; }
// Shared with a frontend client via a shared-types package.
const response: UserResponse = { id: 1, email: "[email protected]" };
console.log(response);
Best Practices
Add runtime validation (with a library like Zod or Joi) alongside your compile-time types, since TypeScript's checks disappear at runtime and can't stop a malformed request body from an untrusted client.
Example: Best Practices
interface CreateUserBody { email: string; }
function isCreateUserBody(value: any): value is CreateUserBody {
return typeof value?.email === "string";
}
// Runtime validation (e.g. Zod/Joi) catches what compile-time types can't.
console.log(isCreateUserBody({ email: "[email protected]" }));
Chapter Quiz — Complete all 14 topics to unlock
0/14 topics done
Complete these topics first:
- Todo App with TypeScript
- REST API with Express + TypeScript
- React Dashboard with TypeScript
- CLI Tool with TypeScript
- Library with TypeScript
- Full Stack TypeScript App
- TypeScript Design System
- TypeScript Monorepo Project
- Authentication System
- Real-time App with Socket.io
- GraphQL API with TypeScript
- Microservices with TypeScript
- TypeScript Best Practices Review
- What to Learn Next