Toute application web ou mobile communique avec un backend via une API. REST reste le pattern dominant pour de bonnes raisons : il se mappe proprement sur HTTP, il est facile à cacher, et tous les développeurs le comprennent déjà. Mais l’écart entre une API qui fonctionne et une API prête pour la production est considérable. Validation des entrées, réponses d’erreur cohérentes, middleware d’authentification et design de routes réfléchi sont ce qui sépare une API qui casse sous la charge d’une qui scale.
Design des routes et nommage des ressources
Les API REST modélisent des ressources, pas des actions. L’URL identifie ce sur quoi on travaille, la méthode HTTP dit ce qu’on fait. Garder ça propre évite l’antipattern POST /getUserById qui gangrène les API mal conçues.
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);
Les ressources imbriquées comme /projects/:id/tasks expriment les relations sans obliger les clients à connaître les IDs internes. Noms au pluriel, pas de verbes dans les URLs, et utilisation cohérente des méthodes HTTP rendent l’API prévisible pour n’importe quel consommateur.
Validation des entrées à la frontière
Toute donnée venant de l’extérieur de l’application est non fiable. La valider au niveau du route handler empêche les données malformées d’atteindre la logique métier ou la base de données. Zod rend ça type-safe en 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 renvoie une union discriminée. Les données validées sont entièrement typées, donc la couche service reçoit exactement ce qu’elle attend. Pas de any, pas de cast manuel, pas de surprises au runtime.
Gestion d’erreurs cohérente
Des blocs try/catch éparpillés dans chaque route handler mènent à des réponses d’erreur inconsistantes. Un error handler centralisé normalise la sortie et garde les handlers concentrés sur le 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' });
}
Les route handlers lancent des instances AppError avec des status codes spécifiques. Le middleware attrape tout, log les erreurs inattendues, et renvoie toujours une shape JSON cohérente. Les consommateurs ne voient jamais de stack traces ni de messages d’erreur non structurés.
Middleware d’authentification
L’authentification JWT protège les routes sans état de session côté serveur. Un middleware vérifie le token avant que la requête n’atteigne le 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);
Le middleware attache le user décodé à l’objet request. Les routes protégées déclarent leur exigence d’authentification explicitement dans la définition de route, rendant la frontière de sécurité visible d’un coup d’oeil.
Conclusion
Une API REST bien structurée est prévisible pour les consommateurs, maintenable pour les développeurs, et résiliente sous la charge. Routing basé sur les ressources, validation par schéma à la frontière, gestion d’erreurs centralisée et middleware d’authentification explicite sont les patterns qui font la différence. Ils ne sont pas complexes, mais en sauter un seul crée de la dette technique qui se cumule avec chaque nouvel endpoint.