Difficulty: Intermediate
How do you use TypeScript with Node.js and Express? Show patterns for typed middleware, routes, and error handling.
TypeScript with Node.js/Express provides type safety across your entire backend. Key patterns:
- Install @types/node and @types/express for base types - Type Request with generics: Request<Params, ResBody, ReqBody, Query> - Augment Express types to add custom properties (user, requestId) - Create typed middleware with proper next() signatures - Use generic response wrappers for consistent API responses
The main challenge is typing middleware chains where each middleware adds properties to the request. Module augmentation and generic request types solve this.
import { Request, Response, Router } from 'express';
interface User {
id: number;
name: string;
email: string;
}
// Typed request generics: Request<Params, ResBody, ReqBody, Query>
interface CreateUserBody {
name: string;
email: string;
password: string;
}
interface UserParams {
id: string;
}
interface UserQuery {
include?: 'posts' | 'comments';
}
const router = Router();
// GET with typed params and query
router.get('/:id', async (
req: Request<UserParams, User, never, UserQuery>,
res: Response<User>
) => {
const userId = parseInt(req.params.id); // string from params
const include = req.query.include; // 'posts' | 'comments' | undefined
const user = await findUser(userId, include);
res.json(user); // must match User type
});
// POST with typed body
router.post('/', async (
req: Request<never, User, CreateUserBody>,
res: Response<User>
) => {
const { name, email, password } = req.body; // fully typed
const user = await createUser({ name, email, password });
res.status(201).json(user);
});
Express Request accepts generics for Params, Response Body, Request Body, and Query. This provides autocompletion and type checking for all parts of the request/response cycle.
import { Request, Response, NextFunction } from 'express';
// Augment Express Request (in a .d.ts file)
declare module 'express-serve-static-core' {
interface Request {
user?: { id: number; role: 'admin' | 'user' };
startTime: number;
}
}
// Auth middleware
function authenticate(req: Request, res: Response, next: NextFunction): void {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.status(401).json({ error: 'No token provided' });
return;
}
try {
req.user = verifyJWT(token); // typed as { id: number; role: string }
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based authorization
function authorize(...roles: ('admin' | 'user')[]) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user || !roles.includes(req.user.role)) {
res.status(403).json({ error: 'Forbidden' });
return;
}
next();
};
}
// Timing middleware
function timing(req: Request, res: Response, next: NextFunction): void {
req.startTime = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.path} - ${Date.now() - req.startTime}ms`);
});
next();
}
// Usage in routes
router.get('/admin/users', authenticate, authorize('admin'), getUsers);
Module augmentation adds custom properties to Request. Middleware functions use the standard (req, res, next) signature. Factory middleware (authorize) returns a closure.
// Custom error class
class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public code: string,
public isOperational = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
}
static badRequest(message: string): AppError {
return new AppError(400, message, 'BAD_REQUEST');
}
static notFound(resource: string): AppError {
return new AppError(404, `${resource} not found`, 'NOT_FOUND');
}
static unauthorized(): AppError {
return new AppError(401, 'Unauthorized', 'UNAUTHORIZED');
}
}
// Typed API response wrapper
interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: { message: string; code: string };
}
// Global error handler middleware
function errorHandler(
err: Error,
req: Request,
res: Response<ApiResponse>,
next: NextFunction
): void {
if (err instanceof AppError) {
res.status(err.statusCode).json({
success: false,
error: { message: err.message, code: err.code },
});
return;
}
console.error('Unhandled error:', err);
res.status(500).json({
success: false,
error: { message: 'Internal server error', code: 'INTERNAL' },
});
}
// Usage in routes
router.get('/:id', async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) throw AppError.notFound('User');
res.json({ success: true, data: user });
} catch (err) {
next(err); // passes to errorHandler
}
});
Custom error classes with static factory methods provide clean, typed error creation. The global error handler uses the 4-parameter Express signature for centralized error handling.
Express Typing, Request/Response Types, Middleware Typing, Error Handling, Typed Routes