Validation at the boundary
There is one validation rule that almost no one teaches but every senior engineer has internalized: validate at the boundary. The controller is where external data enters your system. That is the only place you should be checking that fields exist and types are right. Once data passes the controller, the rest of your code should trust it absolutely.
import { z } from 'zod';
const CreateBookSchema = z.object({
title: z.string().min(1, 'Title is required'),
author: z.string().min(1, 'Author is required'),
pages: z.number().int().positive('Pages must be positive'),
published: z.string(),
});
export function createBookHandler(req: Request, res: Response) {
const validation = CreateBookSchema.safeParse(req.body);
if (!validation.success) {
res.status(400).json({
error: 'Invalid book data',
details: validation.error.format(),
});
return;
}
const newBook = bookService.createBook(validation.data);
res.status(201).json(newBook);
}Zod schema declared right next to the controller that uses it.
If you sprinkle validation throughout the service, every function ends up doing the same checks. Code grows, tests get repetitive, bugs hide in 'I thought you already checked that'. Validate once at the boundary. Trust the data downstream. The TypeScript types pass through without compromise.
Express does not parse JSON request bodies by default. You have to opt in by adding app.use(express.json()) in your main file. Forget that line and req.body is undefined for every POST you ever write. It is a one-time fix that everyone bumps into the first time.
Quiz: Quiz
Loading practice…