Use Cases (Mis à jour: 02/06/2026)

Conception d'API avec Claude Code : OpenAPI, tests et changements cassants

Concevez des API REST fiables avec Claude Code : OpenAPI, mock, tests, versioning, sécurité et pièges.

Conception d'API avec Claude Code : OpenAPI, tests et changements cassants

Concevoir une API ne consiste pas à trouver de jolies URL. Il s’agit de définir un contrat : ce que le client peut envoyer, ce que le serveur renvoie et comment l’échec est expliqué.

Quand ce contrat reste flou, le frontend, les applications mobiles, les partenaires, les tests et l’observabilité inventent chacun leur propre interprétation. Le coût de correction augmente fortement quand l’API est déjà consommée.

Claude Code est très utile, à condition de l’utiliser comme relecteur de conception, pas seulement comme générateur de code. Demandez-lui de rédiger un contrat OpenAPI, de revoir les endpoints, de générer des mocks et des tests, puis de vérifier les changements cassants.

Appuyez-vous sur les références officielles : OpenAPI Specification, RFC 9110 HTTP Semantics, JSON Schema docs et OWASP API Security Top 10. Pour la mise en œuvre, consultez aussi développement d’API de production, automatisation des tests d’API et versioning d’API.

Ce que signifie concevoir une API

Une API est une interface pour un autre programme. Une interface humaine peut se présenter avec des libellés et une mise en page; une API se présente avec des chemins, des méthodes HTTP, des codes de statut, des noms de champs JSON, des schémas, des exemples et des erreurs.

Pour commencer, décidez cinq éléments.

ÉlémentExplication simpleExemple
RessourceLe nom exposé par l’APIorders, customers, invoices
OpérationL’action sur cette ressourceGET, POST, PATCH, DELETE
SchémaLa forme et les règles du JSONitems contient au moins un élément
ErreurLa manière de signaler l’échec400, 401, 403, 404, 422 avec détails
CompatibilitéComment éviter de casser les clientsUn champ requis ajouté est cassant

REST peut paraître théorique, mais l’habitude pratique est claire : utilisez des noms dans les URL et laissez l’action aux méthodes HTTP. POST /orders est plus lisible que POST /orders/create, et GET /orders/ord_123 est plus testable que GET /getOrder?id=ord_123.

Workflow avec Claude Code

Ne demandez pas à Claude Code de tout faire dans une seule consigne. Séparez le brouillon, la revue, les exemples et la compatibilité.

flowchart TD
  A["Résumer les règles métier"] --> B["Rédiger le contrat OpenAPI"]
  B --> C["Revoir HTTP, schema et sécurité"]
  C --> D["Générer mock et tests API"]
  D --> E["Bloquer les changements cassants en CI"]
  E --> F["Implémenter, documenter et publier"]

OpenAPI est un contrat lisible par machine pour les API HTTP. JSON Schema décrit les formes et contraintes JSON. Les codes HTTP apportent une sémantique commune pour succès et échec. Claude Code peut relier ces pièces, mais la référence reste la spécification officielle et vos tests.

Dans un petit projet de vérification, Masa a constaté qu’une simple liste d’endpoints semblait correcte au départ. Les problèmes sont apparus en ajoutant ensuite authentification, pagination, idempotency key et détails d’erreur. Un bon prompt demande dès le départ comment le client récupère après un échec.

Exemple à copier et exécuter

Cet exemple n’utilise aucun paquet externe. Il permet de tester OpenAPI, un serveur mock et un contrôle de changement cassant.

mkdir api-design-lab
cd api-design-lab
mkdir docs examples
node --version

Créez docs/openapi.yaml. La page officielle OpenAPI indique la version publiée la plus récente; ici nous utilisons 3.1 pour rester compatible avec beaucoup d’outils.

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /v1/orders:
    post:
      summary: Create an order
      operationId: createOrder
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Problem"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    CreateOrderRequest:
      type: object
      required: [customerId, items]
      properties:
        customerId:
          type: string
          minLength: 3
        items:
          type: array
          minItems: 1
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku:
                type: string
                minLength: 3
              quantity:
                type: integer
                minimum: 1
    Order:
      type: object
      required: [id, status, customerId, total]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [accepted, cancelled]
        customerId:
          type: string
        total:
          type: integer
    Problem:
      type: object
      required: [type, title, status, detail]
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        errors:
          type: array
          items:
            type: object

Créez examples/mock-server.mjs.

import { createServer } from "node:http";
import { randomUUID } from "node:crypto";

function readJson(req) {
  return new Promise((resolve, reject) => {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk;
      if (body.length > 1_000_000) req.destroy(new Error("Body too large"));
    });
    req.on("end", () => {
      if (!body) return resolve({});
      try {
        resolve(JSON.parse(body));
      } catch (error) {
        reject(error);
      }
    });
    req.on("error", reject);
  });
}

function send(res, status, body, headers = {}) {
  res.writeHead(status, {
    "content-type": "application/json; charset=utf-8",
    "x-content-type-options": "nosniff",
    ...headers,
  });
  res.end(JSON.stringify(body, null, 2));
}

function problem(status, title, detail, errors = []) {
  return {
    type: "https://example.com/problems/request",
    title,
    status,
    detail,
    errors,
  };
}

function validateOrder(input) {
  const errors = [];
  if (typeof input.customerId !== "string" || input.customerId.length < 3) {
    errors.push({
      path: "customerId",
      message: "customerId must be a string with 3+ characters",
    });
  }
  if (!Array.isArray(input.items) || input.items.length === 0) {
    errors.push({ path: "items", message: "items must contain at least one item" });
  }
  for (const [index, item] of (input.items ?? []).entries()) {
    if (typeof item.sku !== "string" || item.sku.length < 3) {
      errors.push({
        path: `items.${index}.sku`,
        message: "sku must be a string with 3+ characters",
      });
    }
    if (!Number.isInteger(item.quantity) || item.quantity < 1) {
      errors.push({
        path: `items.${index}.quantity`,
        message: "quantity must be a positive integer",
      });
    }
  }
  return errors;
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");

  if (req.method === "GET" && url.pathname === "/health") {
    return send(res, 200, { ok: true });
  }

  const customerMatch = url.pathname.match(/^\/v1\/customers\/([a-z0-9-]+)$/);
  if (req.method === "GET" && customerMatch) {
    return send(res, 200, {
      id: customerMatch[1],
      name: "Aki Tanaka",
      plan: "pro",
    });
  }

  if (req.method === "POST" && url.pathname === "/v1/orders") {
    const idempotencyKey = req.headers["idempotency-key"];
    if (!idempotencyKey) {
      return send(
        res,
        400,
        problem(400, "Missing Idempotency-Key", "POST /v1/orders requires the header.")
      );
    }

    try {
      const body = await readJson(req);
      const errors = validateOrder(body);
      if (errors.length > 0) {
        return send(res, 422, problem(422, "Invalid request body", "Fix errors.", errors));
      }
      return send(
        res,
        201,
        {
          id: `ord_${randomUUID()}`,
          status: "accepted",
          customerId: body.customerId,
          total: 4200,
        },
        { location: "/v1/orders/example" }
      );
    } catch {
      return send(res, 400, problem(400, "Malformed JSON", "Request body must be JSON."));
    }
  }

  return send(res, 404, problem(404, "Not found", `${req.method} ${url.pathname} is undefined.`));
});

server.listen(3000, () => {
  console.log("Mock API running at http://localhost:3000");
});

Lancez le serveur puis appelez-le depuis un autre terminal.

node examples/mock-server.mjs
curl -i http://localhost:3000/health
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -H "idempotency-key: demo-001" \
  -d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -H "idempotency-key: demo-002" \
  -d '{"customerId":"x","items":[]}'

Créez examples/contract-check.mjs. Il échoue volontairement pour montrer les changements cassants.

import assert from "node:assert/strict";

const previous = {
  paths: {
    "/v1/orders": {
      post: {
        request: {
          required: ["customerId", "items"],
          properties: ["customerId", "items", "couponCode"],
        },
        response: {
          required: ["id", "status", "customerId", "total"],
          properties: ["id", "status", "customerId", "total"],
        },
      },
    },
  },
};

const next = structuredClone(previous);
next.paths["/v1/orders"].post.request.required.push("shippingAddress");
next.paths["/v1/orders"].post.response.properties =
  next.paths["/v1/orders"].post.response.properties.filter((name) => name !== "total");

function diffContract(oldSpec, newSpec) {
  const breaking = [];
  for (const [path, methods] of Object.entries(oldSpec.paths)) {
    for (const [method, oldOperation] of Object.entries(methods)) {
      const newOperation = newSpec.paths[path]?.[method];
      if (!newOperation) {
        breaking.push(`${method.toUpperCase()} ${path} was removed`);
        continue;
      }

      const oldRequired = new Set(oldOperation.request.required);
      for (const field of newOperation.request.required) {
        if (!oldRequired.has(field)) {
          breaking.push(`${method.toUpperCase()} ${path} now requires "${field}"`);
        }
      }

      const newResponseFields = new Set(newOperation.response.properties);
      for (const field of oldOperation.response.properties) {
        if (!newResponseFields.has(field)) {
          breaking.push(`${method.toUpperCase()} ${path} removed response "${field}"`);
        }
      }
    }
  }
  return breaking;
}

const breaking = diffContract(previous, next);
console.log(breaking.join("\n") || "No breaking changes found");
assert.equal(breaking.length, 0, "Breaking API changes detected");
node examples/contract-check.mjs

L’échec est le résultat attendu : le script détecte un nouveau champ requis et un champ de réponse supprimé.

Prompts pour Claude Code

Découpez le travail : brouillon, revue, exemples, compatibilité.

claude -p "
Crée un brouillon OpenAPI dans docs/openapi.yaml pour une API de commandes e-commerce.
Ressources: customers, orders, invoices.
Ajoute summary, operationId, requestBody, responses, examples et bearerAuth.
Utilise OpenAPI 3.1 et des contraintes de style JSON Schema.
"
claude -p "
Relis docs/openapi.yaml comme relecteur de conception API.
Retourne d'abord les Findings par gravité et ne modifie pas encore les fichiers.
Vérifie la sémantique method/status de RFC 9110, les schemas ambigus,
pagination, idempotency, authentication et les risques OWASP API Security.
"
claude -p "
Génère un serveur mock Node.js et des exemples de tests API depuis docs/openapi.yaml.
Couvre succès, échec d'authentification, échec de validation et ressource absente.
Garde les lignes sous 150 caractères et ajoute les commandes dans README.
"
claude -p "
Compare le docs/openapi.yaml actuel avec la version dans HEAD.
Liste d'abord les changements cassants avant de proposer des modifications.
Vérifie paths supprimés, nouveaux champs requis, champs de réponse supprimés,
changements de status code et changements d'auth scope.
"

Cas d’usage réalistes

Premier cas : une API de commandes SaaS. L’admin, la facturation, les emails et l’export comptable lisent tous Order. Si total, devise, taxes et annulation ne sont pas définis, chaque intégration invente sa règle.

Deuxième cas : une API de profil mobile. D’anciennes versions d’application peuvent rester actives pendant des mois. Supprimer un champ ou changer le sens d’un enum peut casser un client qui ne sera pas mis à jour immédiatement.

Troisième cas : une API partenaire B2B. Les développeurs externes ne connaissent pas vos conventions. Ils ont besoin de codes d’erreur stables, de rate limits, de règles de retry, d’un sandbox et d’exemples.

Quatrième cas : une API d’administration interne. Interne ne veut pas dire sans risque. L’autorisation par objet reste nécessaire; connaître un ID ne doit pas permettre de lire les commandes d’un autre tenant.

Pièges et échecs fréquents

Un piège courant consiste à mettre des verbes dans les routes : /cancelOrder, /getUserOrders, /updateOrderStatus. Les noms deviennent vite incohérents. Modélisez les ressources, puis ajoutez des sous-ressources si nécessaire.

Autre échec : retourner 200 pour toutes les erreurs métier. Cela complique monitoring, SDK, retry et gestion d’erreurs côté client. Utilisez 400 pour une requête mal formée, 401 sans authentification, 403 interdit, 404 absent et 422 pour une entrée sémantiquement invalide.

Oublier la sécurité des retry sur POST coûte cher. Créer une commande ou lancer un paiement peut être répété après timeout. Prévoir Idempotency-Key dès le départ réduit le risque de double exécution.

Un schéma basé seulement sur des exemples reste fragile. Décidez si null signifie supprimer, inconnu ou non fourni. Définissez champs requis, longueurs minimales, limites de pagination, fuseaux horaires et propriétés supplémentaires.

Versioning, erreurs, schema et sécurité

Versionner ne se limite pas à ajouter /v1. L’équipe doit définir ce qui est cassant. Ajouter un champ optionnel est souvent sûr; ajouter un champ requis, supprimer un champ de réponse, changer un statut ou durcir les permissions peut casser les clients.

Une bonne erreur indique la prochaine action. Invalid request ne suffit pas. Gardez une forme stable avec type, title, status, detail et des errors par champ si utile. Ne renvoyez pas stack traces, SQL ni IDs internes en production.

Le schema doit écrire les contraintes, pas seulement les exemples. Formats d’ID, longueurs minimales, limites de tableau, pagination et dates réduisent les suppositions côté client.

La sécurité sépare authentification et autorisation. Un bearer token ne suffit pas si GET /orders/{id} renvoie la commande d’un autre tenant. Évitez les API keys dans query string, réduisez les données sensibles, appliquez rate limit et gardez des logs d’audit.

CTA monétisation et conseil

Les lecteurs qui cherchent la conception d’API ont souvent un projet réel : publier une intégration, stabiliser un backend mobile ou écrire des règles de revue. L’article doit montrer un workflow appliquable, pas seulement des définitions.

ClaudeCodeLab peut accompagner la revue de conception API avec Claude Code, le nettoyage OpenAPI, l’automatisation des tests et les contrôles de changements cassants. Les équipes peuvent commencer par formation et conseil, tandis que les développeurs individuels peuvent utiliser les ressources gratuites.

Résultat vérifié

J’ai testé le code avec Node v24.14.1. GET /health a renvoyé 200, un POST /v1/orders valide a renvoyé 201 et items vide a renvoyé 422. contract-check.mjs échoue volontairement et affiche le nouveau champ requis ainsi que le champ de réponse supprimé. Le lecteur dispose donc d’un chemin concret entre prompts Claude Code, OpenAPI, mock, erreurs et contrôle CI.

#claude-code #api #rest #openapi #design
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.