Concevoir une API REST
Progression
#Concevoir une API REST
Ressources, représentations, verbes HTTP. Alignez URIs et actions: GET /users/:id
, POST /users
, PATCH /users/:id
. Documentez avec une spec vivante (OpenAPI), versionnez avec parcimonie et restez tolérant côté lecture, strict côté écriture.
Une bonne API décrit clairement son contrat: formats attendus, champs obligatoires et erreurs standardisées. Les codes de statut ne sont pas décoratifs: 201 Created
signale une création réussie avec un Location
vers la ressource, 204 No Content
confirme une action sans corps de réponse, 409 Conflict
exprime un conflit métier explicite.
Les écritures concurrentes exigent une protection pour éviter les mises à jour perdues. Répondez avec un ETag
sur lecture et exigez If-Match
sur écriture; rejetez avec 412 Precondition Failed
si la version a changé. Cette stratégie garde le serveur simple tout en donnant au client la main sur la résolution de conflit.
La pagination et le filtrage doivent être prévisibles. Préférez des curseurs stables à des offsets fragiles quand les collections bougent, et exposez des liens de navigation (next
, prev
) dans la réponse. Les champs triables et filtrables se déclarent pour éviter les surprises et les surcharges côté serveur.
Mini‑exercice: dessinez les endpoints d’un mini‑blog (posts, comments), précisez les statuts et les erreurs, et ajoutez des préconditions If-Match
sur les mises à jour d’articles pour éviter les écrasements inattendus.
#Flow d’écriture robuste (idempotent)
#Diagrammes REST : création puis lecture
#Mise à jour conditionnelle (ETag + If‑Match)
#Idempotence clé (Idempotency‑Key)
#Checklist REST (animée)
- URIs stables et orientées ressources (noms pluriels, relations claires).
- Statuts cohérents (201 avec
Location
sur création, 409/422 explicites). - Préconditions pour la concurrence (ETag + If‑Match → 412 si conflit).
- Pagination par curseur, filtres/tri documentés, liens de navigation.
- Erreurs JSON stables (
code
,message
,correlationId
).
#Exemples de code pratiques
#ETag + If‑Match (Express)
1import type { Request, Response } from 'express'2import crypto from 'node:crypto'3 4// Exemple: calcul d’ETag déterministe à partir des champs pertinents5function computeEtag(obj: any) {6 const json = JSON.stringify(obj)7 return `"${crypto.createHash('sha1').update(json).digest('base64').slice(0, 16)}"`8}9 10app.get('/posts/:id', async (req: Request, res: Response) => {11 const post = await db.posts.findById(Number(req.params.id))12 if (!post) return res.status(404).end()13 const etag = computeEtag({ id: post.id, updated_at: post.updated_at })14 res.setHeader('ETag', etag)
Un ETag faible (W/
) convient à des variations mineures; un ETag fort convient si chaque octet compte. Ici, on utilise un ETag fort dérivé de updated_at
et de l’identifiant.
#Idempotency‑Key (création)
1// Store minimal (en prod: table idem_keys avec TTL + résultat)2const idemStore = new Map<string, any>()3 4app.post('/payments', async (req, res) => {5 const key = req.get('Idempotency-Key')6 if (!key) return res.status(400).json({ type: 'about:blank', title: 'Missing Idempotency-Key', status: 400 })7 8 const cached = idemStore.get(key)9 if (cached) return res.status(201).json(cached)10 11 // ... traitement transactionnel: INSERT payment ...12 const payment = await createPayment(req.body)13 const result = { id: payment.id, status: 'created' }14 idemStore.set(key, result)
#Pagination par curseur stable (SQL + API)
1-- Suppose une clé composite (created_at DESC, id DESC) pour l’ordre2-- Récupération après un curseur (created_at_c, id_c)3SELECT id, title, created_at4FROM posts5WHERE (created_at, id) < (TIMESTAMPTZ :created_at_c, BIGINT :id_c)6ORDER BY created_at DESC, id DESC7LIMIT 20;
1// Encodage d’un curseur compact {created_at, id} en base642function encodeCursor(c: { created_at: string; id: number }) {3 return Buffer.from(JSON.stringify(c)).toString('base64url')4}5function decodeCursor(s?: string | null) {6 if (!s) return null7 try { return JSON.parse(Buffer.from(s, 'base64url').toString('utf8')) } catch { return null }8}9 10app.get('/posts', async (req, res) => {11 const cursor = decodeCursor(req.query.cursor as string | undefined)12 const rows = await db.posts.list({ cursor, limit: 20 })13 const next = rows.length === 20 ? encodeCursor({ created_at: rows[19].created_at, id: rows[19].id }) : null14 res.json({ items: rows, next })
Pour une pagination stable, combinez un champ d’ordre (ex. created_at
) et une clé unique (id
) pour obtenir un ordre total et éviter les doublons/manques.
#OpenAPI 3.1 (extrait)
1openapi: 3.1.02info: { title: Mini API, version: 1.0.0 }3paths:4 /users:5 post:6 summary: Créer un utilisateur7 requestBody:8 required: true9 content:10 application/json:11 schema:12 type: object13 required: [email, password]14 properties: