Every web and mobile application talks to a backend through an API. REST remains the dominant pattern for good reason: it maps cleanly to HTTP, it’s easy to cache, and every developer already understands it. But the gap between a working API and a production-ready one is significant. Proper input validation, consistent error responses, authentication middleware, and thoughtful route design are what separate an API that breaks under pressure from one that scales.
Route design and resource naming
REST APIs model resources, not actions. The URL identifies what you’re working with, the HTTP method says what you’re doing. Keeping this clean avoids the POST /getUserById antipattern that plagues poorly designed APIs.
import { Router } from 'express';
const router = Router();
router.get('/projects', listProjects);
router.get('/projects/:id', getProject);
router.post('/projects', createProject);
router.patch('/projects/:id', updateProject);
router.delete('/projects/:id', deleteProject);
router.get('/projects/:id/tasks', listProjectTasks);
Nested resources like /projects/:id/tasks express relationships without requiring clients to know internal IDs. Plural nouns, no verbs in URLs, and consistent use of HTTP methods make the API predictable for any consumer.
Input validation at the boundary
Every piece of data coming from outside the application is untrusted. Validating it at the route handler level prevents malformed data from reaching business logic or the database. Zod makes this type-safe in TypeScript.
import { z } from 'zod';
const createProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
status: z.enum(['draft', 'active', 'archived']).default('draft'),
});
router.post('/projects', async (req, res, next) => {
const result = createProjectSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten().fieldErrors });
}
const project = await projectService.create(result.data);
res.status(201).json(project);
});
safeParse returns a discriminated union. The validated data is fully typed, so the service layer receives exactly what it expects. No any, no manual type casting, no runtime surprises.
Consistent error handling
Scattered try/catch blocks in every route handler lead to inconsistent error responses. A centralized error handler normalizes the output and keeps route handlers focused on the happy path.
class AppError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
}
}
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message });
}
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
Route handlers throw AppError instances with specific status codes. The middleware catches everything, logs unexpected errors, and always returns a consistent JSON shape. Consumers never see stack traces or unstructured error messages.
Authentication middleware
JWT-based authentication protects routes without server-side session state. A middleware function verifies the token before the request reaches the handler.
import jwt from 'jsonwebtoken';
function authenticate(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
throw new AppError(401, 'Missing authentication token');
}
const token = header.slice(7);
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload as AuthUser;
next();
}
router.get('/projects', authenticate, listProjects);
router.post('/projects', authenticate, createProject);
The middleware attaches the decoded user to the request object. Protected routes declare their authentication requirement explicitly in the route definition, making the security boundary visible at a glance.
Conclusion
A well-structured REST API is predictable for consumers, maintainable for developers, and resilient under load. Resource-based routing, schema validation at the boundary, centralized error handling, and explicit authentication middleware are the patterns that make the difference. They’re not complex, but skipping any one of them creates technical debt that compounds with every new endpoint.