Introduction
Les API sont le système nerveux des applications modernes. Chaque SaaS, chaque app mobile, chaque intégration tierce communique via des API — et dans la majorité des cas, c'est REST qui fait le travail. Mais "faire du REST" ne suffit pas : une API mal conçue génère de la dette technique, des bugs d'intégration et des failles de sécurité qui coûtent des semaines de maintenance.
Ce guide couvre les bonnes pratiques éprouvées de conception d'API REST : nommage des ressources, verbes HTTP, gestion d'erreurs, pagination, versioning, authentification et documentation OpenAPI. Avec des exemples concrets en Node.js et les erreurs classiques à éviter. Si vous hésitez entre REST et GraphQL, notre comparatif GraphQL vs REST vous aidera à trancher.

Nommage et Structure des Ressources
Une API REST bien conçue commence par des URLs claires et prévisibles. Le principe : les URLs représentent des ressources (noms), pas des actions (verbes). Un développeur doit comprendre ce qu'un endpoint fait rien qu'en lisant son URL.
Conventions de Nommage
# ✅ Ressources au pluriel, hiérarchie claire
GET /users → Liste des utilisateurs
GET /users/123 → Utilisateur 123
POST /users → Créer un utilisateur
GET /users/123/orders → Commandes de l'utilisateur 123
GET /users/123/orders/456 → Commande 456 de l'utilisateur 123
# ❌ Verbes dans l'URL, nommage incohérent
GET /getUser?id=123
POST /createNewOrder
GET /user/123/orderList
DELETE /removeProduct/456
Règles clés :
- Noms de ressources au pluriel (
/users,/products,/orders) - Hiérarchie pour les relations (
/users/{id}/orders) - kebab-case pour les noms composés (
/order-items, pas/orderItems) - Maximum 2 à 3 niveaux de nesting — au-delà, la lisibilité chute
Utilisation des Verbes HTTP
Chaque verbe HTTP a une sémantique précise. Les respecter rend votre API prévisible :
| Verbe | Action | Idempotent | Corps de requête | Code de retour typique |
|---|---|---|---|---|
GET | Lire une ressource | Oui | Non | 200 OK |
POST | Créer une ressource | Non | Oui | 201 Created |
PUT | Remplacer entièrement | Oui | Oui | 200 OK |
PATCH | Modifier partiellement | Non | Oui | 200 OK |
DELETE | Supprimer | Oui | Non | 204 No Content |
L'idempotence est un concept clé : un GET, PUT ou DELETE exécuté 10 fois doit produire le même résultat qu'une seule exécution. Un POST crée une nouvelle ressource à chaque appel — il n'est pas idempotent.
// Exemple Express.js — CRUD complet sur une ressource
const router = express.Router();
router.get('/products', listProducts); // Liste paginée
router.get('/products/:id', getProduct); // Détail
router.post('/products', createProduct); // Création
router.put('/products/:id', replaceProduct); // Remplacement complet
router.patch('/products/:id', updateProduct); // Mise à jour partielle
router.delete('/products/:id', deleteProduct); // Suppression
Codes de Statut HTTP et Gestion des Erreurs
La manière dont votre API communique les résultats — succès comme échecs — est aussi importante que la logique métier elle-même.
Les Codes de Statut Essentiels
| Code | Signification | Quand l'utiliser |
|---|---|---|
200 | OK | Requête réussie avec contenu |
201 | Created | Ressource créée (POST) |
204 | No Content | Succès sans contenu (DELETE, PUT) |
400 | Bad Request | Requête mal formée (syntaxe) |
401 | Unauthorized | Authentification manquante ou invalide |
403 | Forbidden | Authentifié mais pas autorisé |
404 | Not Found | Ressource inexistante |
409 | Conflict | Conflit (email déjà utilisé, version obsolète) |
422 | Unprocessable Entity | Validation métier échouée |
429 | Too Many Requests | Rate limiting dépassé |
500 | Internal Server Error | Erreur serveur inattendue |
Erreur classique : retourner 200 OK avec { "success": false, "error": "..." } dans le body. Les codes HTTP existent pour ça — utilisez-les.
Format Standardisé des Erreurs
Adoptez un format unique pour toutes vos réponses d'erreur. Le standard RFC 9457 (Problem Details) est une bonne base :
// POST /users — 422 Unprocessable Entity
{
"type": "validation_error",
"title": "Validation failed",
"status": 422,
"errors": [
{
"field": "email",
"message": "Email déjà utilisé par un autre compte",
"code": "DUPLICATE_EMAIL"
},
{
"field": "password",
"message": "Le mot de passe doit contenir au moins 8 caractères",
"code": "PASSWORD_TOO_SHORT"
}
]
}
// GET /products/999 — 404 Not Found
{
"type": "not_found",
"title": "Resource not found",
"status": 404,
"detail": "No product found with ID 999"
}
Implémentation Middleware d'Erreur (Express.js)
class ApiError extends Error {
constructor(
public status: number,
public type: string,
public detail: string,
public errors?: { field: string; message: string; code: string }[]
) {
super(detail);
}
}
// Middleware global de gestion d'erreurs
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof ApiError) {
return res.status(err.status).json({
type: err.type,
title: err.detail,
status: err.status,
errors: err.errors
});
}
// Erreur inattendue — log + réponse générique
Sentry.captureException(err);
res.status(500).json({
type: 'internal_error',
title: 'An unexpected error occurred',
status: 500
});
});
// Usage dans un handler
async function createUser(req: Request, res: Response) {
const existing = await db.findUserByEmail(req.body.email);
if (existing) {
throw new ApiError(409, 'conflict', 'Email already registered');
}
const user = await db.createUser(req.body);
res.status(201).json(user);
}
Pagination : Gérer les Collections Volumineuses
Ne retournez jamais une collection complète sans pagination. Un GET /products qui retourne 50 000 résultats est un déni de service involontaire.
Offset vs Cursor : Quel Modèle Choisir ?
| Critère | Offset (?page=2&limit=20) | Cursor (?cursor=abc&limit=20) |
|---|---|---|
| Implémentation | Simple (OFFSET SQL) | Plus complexe (token opaque) |
| Saut de pages | Oui (?page=15) | Non (séquentiel uniquement) |
| Performance grands datasets | Dégradée (OFFSET scanne les lignes) | Constante |
| Cohérence si données changent | Doublons/trous possibles | Consistant |
| Idéal pour | Back-offices, < 100K lignes | Feeds, listes infinies, > 100K lignes |
Implémentation Offset-Based
// GET /products?page=2&limit=20
async function listProducts(req: Request, res: Response) {
const page = Math.max(1, parseInt(req.query.page as string) || 1);
const limit = Math.min(100, parseInt(req.query.limit as string) || 20);
const offset = (page - 1) * limit;
const [products, total] = await Promise.all([
db.query('SELECT * FROM products ORDER BY id LIMIT $1 OFFSET $2', [limit, offset]),
db.query('SELECT COUNT(*) FROM products')
]);
res.json({
data: products,
pagination: {
page,
limit,
total: parseInt(total[0].count),
pages: Math.ceil(total[0].count / limit)
}
});
}
Implémentation Cursor-Based
// GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
async function listProducts(req: Request, res: Response) {
const limit = Math.min(100, parseInt(req.query.limit as string) || 20);
const cursor = req.query.cursor
? JSON.parse(Buffer.from(req.query.cursor as string, 'base64url').toString())
: null;
const products = await db.query(
`SELECT * FROM products
WHERE ($1::int IS NULL OR id > $1)
ORDER BY id LIMIT $2`,
[cursor?.id ?? null, limit + 1]
);
const hasMore = products.length > limit;
const items = hasMore ? products.slice(0, -1) : products;
const nextCursor = hasMore
? Buffer.from(JSON.stringify({ id: items.at(-1).id })).toString('base64url')
: null;
res.json({
data: items,
pagination: { next_cursor: nextCursor, has_more: hasMore }
});
}
Versioning : Faire Évoluer sans Casser
Votre API va évoluer. Des champs seront renommés, des endpoints ajoutés, des comportements modifiés. Le versioning permet de faire coexister l'ancien et le nouveau sans casser les intégrations existantes.
Stratégies de Versioning
| Stratégie | Exemple | Avantage | Inconvénient |
|---|---|---|---|
| URL path | /api/v1/users | Visible, simple à tester | URL plus longue, couplage fort |
| Header | API-Version: 2 | URL propres | Invisible, complexe à tester |
| Query param | /users?version=2 | Simple à implémenter | Peu standard, cache complexe |
La stratégie URL path (/api/v1/) est la plus répandue et la plus lisible. C'est celle que nous recommandons pour la plupart des projets.
// Routeur versionné
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Ou avec un middleware de routing
app.use('/api/:version', (req, res, next) => {
req.apiVersion = parseInt(req.params.version.replace('v', ''));
next();
});
Règle d'or : ne créez une nouvelle version majeure que pour les breaking changes. Les ajouts de champs, de nouveaux endpoints ou d'options facultatives ne nécessitent pas de bump de version.
Authentification et Sécurité des API
La sécurité d'une API REST repose sur trois piliers : authentification (qui êtes-vous ?), autorisation (avez-vous le droit ?) et protection (contre les abus).
JWT pour l'Authentification Stateless
Les JSON Web Tokens sont le standard pour les API REST stateless. Ils permettent au serveur de vérifier l'identité sans maintenir d'état de session (voir notre guide sur l'authentification SSO et OAuth).
// Middleware d'authentification JWT
import jwt from 'jsonwebtoken';
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ApiError(401, 'unauthorized', 'Missing authentication token');
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload as AuthUser;
next();
} catch {
throw new ApiError(401, 'unauthorized', 'Invalid or expired token');
}
}
// Middleware d'autorisation par rôle
function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role)) {
throw new ApiError(403, 'forbidden', 'Insufficient permissions');
}
next();
};
}
// Usage
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
Rate Limiting
Protégez votre API contre les abus avec un rate limiter basé sur Redis :
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requêtes par fenêtre
standardHeaders: true, // Headers RateLimit-* dans la réponse
message: {
type: 'rate_limit_exceeded',
title: 'Too many requests',
status: 429,
detail: 'Rate limit exceeded. Try again in 60 seconds.'
}
});
app.use('/api/', limiter);
Checklist Sécurité API
- HTTPS obligatoire (pas de HTTP)
- JWT avec expiration courte (15min) + refresh token
- Rate limiting par IP et par utilisateur
- Validation des entrées (zod, joi, class-validator)
- Headers de sécurité (
CORS,Content-Security-Policy) - Pas de données sensibles dans les URLs (tokens, mots de passe)
- Logs d'accès pour le monitoring
Pour une approche complète, consultez notre guide sur la sécurité des applications web.
Documentation avec OpenAPI (Swagger)
Une API sans documentation est une API inutilisable. OpenAPI 3.1 est le standard de facto pour décrire vos API REST de manière machine-readable.
Approche API-First
En 2026, l'approche API-first (ou contract-driven) est devenue le standard : définissez votre contrat OpenAPI avant de coder, puis générez la documentation, les tests et même le code serveur/client.
# openapi.yaml
openapi: 3.1.0
info:
title: Mon SaaS API
version: 1.0.0
paths:
/users:
get:
summary: Liste des utilisateurs
parameters:
- name: page
in: query
schema: { type: integer, default: 1 }
- name: limit
in: query
schema: { type: integer, default: 20, maximum: 100 }
responses:
'200':
description: Liste paginée
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/User' }
pagination:
$ref: '#/components/schemas/Pagination'
post:
summary: Créer un utilisateur
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateUser' }
responses:
'201':
description: Utilisateur créé
'409':
description: Email déjà utilisé
'422':
description: Validation échouée
Des outils comme Swagger UI, Redoc ou Scalar transforment ce fichier en documentation interactive. Vos consommateurs d'API peuvent tester les endpoints directement depuis la doc.
FAQ : Conception d'API REST
FAQ - Questions fréquentes
Conclusion : Une API Bien Conçue, c'est du Temps Gagné
La qualité d'une API REST se mesure sur le long terme : moins de bugs d'intégration, moins de questions des développeurs consommateurs, moins de breaking changes douloureux. Les principes couverts dans ce guide — nommage cohérent, codes HTTP précis, pagination robuste, versioning anticipé, sécurité en profondeur et documentation OpenAPI — sont l'investissement minimum pour une API qui tient en production.
Si vous concevez une application nécessitant des API robustes ou souhaitez auditer vos API existantes, contactez notre équipe. Nous concevons des applications web sur mesure avec des API pensées pour la durabilité et la scalabilité.





