Skip to content
intermediate Phase 5 · TypeScript with Node.js

Typed Express APIs

Build Express routes, middleware, and controllers with TypeScript.

1h 30m
0 problems
Topic Progress 0%

Express Server Setup

Express Server Setup

Basic Server

import express, { Request, Response, NextFunction } from 'express';
import { env } from './config/env.js';

const app = express();

app.use(express.json());

app.get('/health', (req: Request, res: Response) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.listen(env.PORT, () => {
  console.log(`Server running on port ${env.PORT}`);
});

Typed Route Handlers

interface User {
  id: number;
  name: string;
  email: string;
}

let users: User[] = [];

// GET /api/users
app.get('/api/users', (req: Request, res: Response) => {
  res.json(users);
});

// GET /api/users/:id
app.get('/api/users/:id', (req: Request, res: Response) => {
  const id = parseInt(req.params.id, 10);
  const user = users.find(u => u.id === id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// POST /api/users
app.post('/api/users', (req: Request, res: Response) => {
  const { name, email } = req.body;
  const user: User = {
    id: users.length + 1,
    name,
    email
  };
  users.push(user);
  res.status(201).json(user);
});

Typed Request and Response

Typed Request and Response

Custom Request Types

// Typed request with body
interface CreateUserRequest extends Request {
  body: {
    name: string;
    email: string;
    role?: 'admin' | 'user';
  };
}

// Typed request with params
interface GetUserRequest extends Request {
  params: {
    id: string;
  };
}

// Typed request with query
interface SearchUsersRequest extends Request {
  query: {
    q?: string;
    page?: string;
    limit?: string;
  };
}

app.get('/api/users', (req: SearchUsersRequest, res: Response) => {
  const { q, page = '1', limit = '10' } = req.query;
  // types are safe here
});

Typed Response

interface ApiResponse<T> {
  data: T;
  meta?: {
    page: number;
    total: number;
    limit: number;
  };
}

interface ErrorResponse {
  error: string;
  code: string;
  details?: unknown;
}

app.get('/api/users', (req: Request, res: Response<ApiResponse<User[]>>) => {
  res.json({ data: users });
});

app.get('/api/users/:id', (req: Request, res: Response<ApiResponse<User> | ErrorResponse>) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'Not found', code: 'USER_NOT_FOUND' });
  }
  res.json({ data: user });
});

Typed Middleware

Typed Middleware

Authentication Middleware

import { JwtPayload, verify } from 'jsonwebtoken';

interface AuthenticatedRequest extends Request {
  user?: JwtPayload & { id: string; role: string };
}

function authenticate(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
): void {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    res.status(401).json({ error: 'No token provided' });
    return;
  }

  try {
    const decoded = verify(token, env.JWT_SECRET) as JwtPayload & {
      id: string;
      role: string;
    };
    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

// Usage
app.get('/api/profile', authenticate, (req: AuthenticatedRequest, res: Response) => {
  res.json({ user: req.user });
});

Validation Middleware

import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).optional()
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

function validate(schema: z.ZodSchema) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      res.status(400).json({
        error: 'Validation failed',
        details: result.error.flatten()
      });
      return;
    }
    req.body = result.data;
    next();
  };
}

app.post('/api/users', validate(CreateUserSchema), (req: Request<{}, {}, CreateUserInput>, res: Response) => {
  const user = { id: users.length + 1, ...req.body };
  users.push(user);
  res.status(201).json(user);
});

Error Handling Middleware

Error Handling Middleware

Typed Error Classes

class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: unknown
  ) {
    super(message);
    this.name = 'AppError';
  }
}

function errorHandler(
  err: Error | AppError,
  req: Request,
  res: Response,
  next: NextFunction
): void {
  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
      details: err.details
    });
    return;
  }

  console.error('Unhandled error:', err);
  res.status(500).json({
    error: 'Internal server error',
    code: 'INTERNAL_ERROR'
  });
}

// Usage in routes
app.get('/api/users/:id', (req: Request, res: Response, next: NextFunction) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return next(new AppError(404, 'USER_NOT_FOUND', 'User not found'));
  }
  res.json(user);
});

app.use(errorHandler);