← Back to TypeScript Course | Chapter 28: Real World Projects | Lesson 2 of 14

REST API with Express + TypeScript

Express can be combined with TypeScript to build typed HTTP APIs. Interfaces and request types help keep route logic predictable.

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

typescript
// 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

typescript
// 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

typescript
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

typescript
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

typescript
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]" }));

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.