The Case for TypeScript on the Backend
JavaScript's flexibility is a double-edged sword. The same dynamic typing that allows rapid prototyping creates subtle bugs that only manifest at runtime — when a user is actively using your application. A function receives undefined where it expects a string. An API response changes its structure and the consuming code silently breaks. TypeScript's static type system catches these entire categories of bugs at compile time — before any code runs.
Project Setup: The Foundation That Matters
A TypeScript Express project requires careful initial configuration. The decisions you make in tsconfig.json set the quality bar for the entire codebase:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
strict: true enables all strict type-checking options simultaneously. Never disable it — every strict rule exists because it catches real bugs.
Runtime Validation with Zod: The Missing Piece
TypeScript types are erased at compile time. Your type declarations cannot validate the JSON body of an incoming HTTP request at runtime. Zod provides runtime validation AND generates TypeScript types from your schemas, creating a single source of truth for your data shapes:
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(5).max(200).trim(),
content: z.string().min(50).max(50000),
tags: z.array(z.string().max(30)).max(10).optional(),
published: z.boolean().default(false),
});
type CreatePostInput = z.infer<typeof CreatePostSchema>;
const validate = (schema) => (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: 'Validation failed', issues: result.error.issues });
}
req.body = result.data;
next();
};
router.post('/posts', validate(CreatePostSchema), createPostHandler);
Centralized Error Handling with Custom Error Classes
A professional API has a single error handling middleware that processes all errors uniformly. Create an error hierarchy that allows handlers to throw semantically meaningful errors — NotFoundError, UnauthorizedError, ValidationError — that the centralized handler converts to appropriate HTTP responses without leaking internal details.
Type-Safe Request Handlers
Extend Express's Request type to add typed properties that your middleware attaches — authenticated user, validated body, parsed query parameters. This eliminates unsafe type assertions throughout your handler code and makes your route handlers self-documenting about what data they require.
Conclusion
TypeScript with Zod validation and a custom error class hierarchy transforms an Express application from a collection of loosely connected JavaScript functions into a type-safe, validated, professionally structured API. The additional setup cost is 2-3 hours on a new project. The maintenance savings — catching type errors at compile time, self-documenting interfaces, confident refactoring — compound across the entire lifetime of the application.
