Aller au contenu principal

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)

1/5

#Diagrammes REST : création puis lecture

Client
API
appel async/retour activation fragments
1. POST /users

#Mise à jour conditionnelle (ETag + If‑Match)

Client
API
DB
1. GET /posts/:id
2. 200 ETag: "v1"
3. PATCH /posts/:id If-Match: "v1"
4. BEGIN + UPDATE
5. OK
6. 200 ETag: "v2"

#Idempotence clé (Idempotency‑Key)

Client
API
DB
appel async/retour activation fragments
1. POST /payments (Idempotency‑Key: K)

#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)

tsts
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)
Précision 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)

tsts
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)

sqlsql
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;
tsts
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 })
Ordre total

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)

yamlyaml
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:

#Quiz rapide

Quelle réponse renvoyer si le client PATCH sans envoyer If‑Match sur une ressource protégée par ETag ?
Quelle réponse renvoyer si le client PATCH sans envoyer If‑Match sur une ressource protégée par ETag ?
Quel curseur garantit une pagination stable sur posts triés par created_at DESC ?
Quel curseur garantit une pagination stable sur posts triés par created_at DESC ?