Tips & Tricks

Concevoir, implémenter et tester des APIs REST à toute vitesse avec Claude Code | De la spec OpenAPI à la production

Découvrez comment développer des APIs REST de bout en bout avec Claude Code : génération de spec OpenAPI, implémentation TypeScript type-safe avec Hono, validation zod et tests vitest. Exemples de code fonctionnels inclus.

Pourquoi Claude Code excelle dans la conception d’APIs REST

Le développement d’APIs REST est truffé de patterns répétitifs : définitions d’endpoints, validation, gestion d’erreurs et tests — tout écrit à la main. Il est difficile de rester concentré sur la logique métier en gérant ce boilerplate.

Je suis Masa, ingénieur DX en charge de la modernisation des APIs de systèmes internes. Avant Claude Code, mon coût standard par endpoint était : « 30 min de conception → 2 h d’implémentation → 1 h de tests ». Après l’intégration complète de Claude Code : 10 min de conception → 30 min d’implémentation → 15 min de tests.

3 raisons pour lesquelles Claude Code excelle dans le développement d’APIs

  1. Comprend les spécifications OpenAPI comme contexte — collez un YAML et il génère du code conforme à la spec instantanément
  2. Maîtrise le système de types TypeScript — produit du code type-safe dès le départ
  3. Connaît un large éventail de patterns de test — génère des tests happy-path, error-path et edge cases en une seule passe

1. Générer automatiquement une spec OpenAPI avec Claude Code

Exemple de prompt

Génère une spécification OpenAPI 3.1 (YAML) pour les exigences suivantes.

Ressource : Task (gestion de tâches)
Champs :
  - id : UUID
  - title : string (1–200 caractères)
  - description : string (optionnel)
  - status : "todo" | "in_progress" | "done"
  - priority : "low" | "medium" | "high" (défaut : medium)
  - assignee_id : UUID (optionnel)
  - due_date : format ISO8601 (optionnel)
  - created_at : format ISO8601
  - updated_at : format ISO8601

Endpoints :
  - GET /tasks (liste avec pagination)
  - GET /tasks/:id
  - POST /tasks
  - PUT /tasks/:id (mise à jour complète)
  - PATCH /tasks/:id (mise à jour partielle)
  - DELETE /tasks/:id

Format de réponse : { data, meta, errors }
Codes d'erreur : 400, 401, 403, 404, 422, 500
Authentification : Bearer Token (en-tête Authorization)

YAML OpenAPI généré (extrait)

openapi: 3.1.0
info:
  title: Task Management API
  version: 1.0.0
  description: Spécification OpenAPI pour l'API de gestion des tâches

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: http://localhost:3000/v1
    description: Développement

components:
  schemas:
    Task:
      type: object
      required: [id, title, status, priority, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 200
        status:
          type: string
          enum: [todo, in_progress, done]
        priority:
          type: string
          enum: [low, medium, high]
          default: medium

2. Implémentation type-safe des handlers avec Hono

// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { taskRouter } from "./routes/tasks";

const app = new Hono();

// Middleware
app.use("*", cors());
app.use("*", logger());
app.use("*", prettyJSON());

// Enregistrement du routeur
app.route("/v1/tasks", taskRouter);

// Gestionnaire d'erreurs global
app.onError((err, c) => {
  console.error(err);
  return c.json(
    { data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "Une erreur interne du serveur s'est produite" }] },
    500
  );
});

export default app;

Implémentation du routeur de tâches

// src/routes/tasks.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { TaskCreateSchema, TaskUpdateSchema, TaskPatchSchema, TaskQuerySchema } from "../schemas/task";
import { TaskService } from "../services/taskService";

export const taskRouter = new Hono();
const taskService = new TaskService();

// Lister les tâches
taskRouter.get("/", zValidator("query", TaskQuerySchema), async (c) => {
  const query = c.req.valid("query");
  const result = await taskService.findAll(query);
  return c.json({
    data: result.tasks,
    meta: { total: result.total, page: query.page, per_page: query.per_page },
    errors: null,
  });
});

// Obtenir une tâche par ID
taskRouter.get("/:id", async (c) => {
  const id = c.req.param("id");
  const task = await taskService.findById(id);
  if (!task) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Tâche introuvable", field: "id" }] },
      404
    );
  }
  return c.json({ data: task, meta: null, errors: null });
});

// Créer une tâche
taskRouter.post("/", zValidator("json", TaskCreateSchema), async (c) => {
  const body = c.req.valid("json");
  const task = await taskService.create(body);
  return c.json({ data: task, meta: null, errors: null }, 201);
});

// Supprimer une tâche
taskRouter.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const deleted = await taskService.delete(id);
  if (!deleted) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Tâche introuvable", field: "id" }] },
      404
    );
  }
  return c.json({ data: null, meta: null, errors: null }, 204);
});

3. Génération automatique des schémas de validation Zod

// src/schemas/task.ts
import { z } from "zod";

const TaskStatus = z.enum(["todo", "in_progress", "done"]);
const TaskPriority = z.enum(["low", "medium", "high"]);

export const TaskSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1, "Le titre doit contenir au moins 1 caractère").max(200, "Le titre ne peut pas dépasser 200 caractères"),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

export const TaskCreateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().optional(),
  status: TaskStatus.default("todo"),
  priority: TaskPriority.default("medium"),
  assignee_id: z.string().uuid().optional(),
  due_date: z.string().datetime().optional(),
});

export const TaskUpdateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
});

export const TaskPatchSchema = TaskUpdateSchema.partial();

export const TaskQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  per_page: z.coerce.number().int().min(1).max(100).default(20),
  status: TaskStatus.optional(),
  priority: TaskPriority.optional(),
});

export type Task = z.infer<typeof TaskSchema>;
export type TaskCreate = z.infer<typeof TaskCreateSchema>;
export type TaskQuery = z.infer<typeof TaskQuerySchema>;

Comparaison avant/après

MétriqueAvant Claude CodeAprès Claude Code
Temps de définition des schémas30–45 min par endpoint5–10 min (prompt inclus)
Taux d’erreurs de typesFréquent en fin de projetQuasi nul dès le départ
Validations manquéesSignalées à chaque revue de codeCas limites couverts automatiquement
Dérive spec/codeObsolète en 2–3 semainesToujours synchronisé (généré depuis la spec)

4. Génération automatique de tests avec Vitest

// src/routes/tasks.test.ts
import { describe, it, expect, vi } from "vitest";
import app from "../index";
import { TaskService } from "../services/taskService";

vi.mock("../services/taskService");

const mockTask = {
  id: "550e8400-e29b-41d4-a716-446655440000",
  title: "Tâche de test",
  description: "Une tâche pour les tests",
  status: "todo" as const,
  priority: "medium" as const,
  assignee_id: null,
  due_date: null,
  created_at: "2026-04-25T00:00:00.000Z",
  updated_at: "2026-04-25T00:00:00.000Z",
};

describe("GET /v1/tasks", () => {
  it("happy path : retourne la liste des tâches", async () => {
    vi.mocked(TaskService.prototype.findAll).mockResolvedValue({ tasks: [mockTask], total: 1 });
    const res = await app.request("/v1/tasks", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.data).toHaveLength(1);
  });

  it("erreur : per_page dépasse le maximum", async () => {
    const res = await app.request("/v1/tasks?per_page=101", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(400);
  });
});

describe("POST /v1/tasks", () => {
  it("happy path : crée une tâche", async () => {
    vi.mocked(TaskService.prototype.create).mockResolvedValue(mockTask);
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "Nouvelle tâche" }),
    });
    expect(res.status).toBe(201);
  });

  it("erreur : titre vide", async () => {
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "" }),
    });
    expect(res.status).toBe(400);
  });
});

5. Patterns de gestion des erreurs

// src/lib/errors.ts
export class AppError extends Error {
  constructor(
    public readonly code: string,
    public readonly message: string,
    public readonly statusCode: number,
    public readonly field?: string
  ) {
    super(message);
    this.name = "AppError";
  }
}

export const Errors = {
  notFound: (resource: string) => new AppError("NOT_FOUND", `${resource} introuvable`, 404),
  unauthorized: () => new AppError("UNAUTHORIZED", "Authentification requise", 401),
  forbidden: () => new AppError("FORBIDDEN", "Accès refusé", 403),
  conflict: (message: string) => new AppError("CONFLICT", message, 409),
  internal: () => new AppError("INTERNAL_ERROR", "Une erreur interne du serveur s'est produite", 500),
};

6. Générer la documentation avec Swagger UI

// src/swagger.ts
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";

const openApiSpec = YAML.parse(fs.readFileSync(path.resolve("./openapi.yaml"), "utf8"));

export function setupSwagger(app: OpenAPIHono) {
  app.get("/docs", swaggerUI({ url: "/openapi.json" }));
  app.get("/openapi.json", (c) => c.json(openApiSpec));
  console.log("Swagger UI : http://localhost:3000/docs");
}

3 pièges courants à éviter

Piège 1 : Validation UUID manquante

Claude Code traite parfois le paramètre de chemin :id comme une simple chaîne. Sans validation du format UUID, des requêtes malformées peuvent atteindre la base de données.

// MAL : chaîne brute envoyée directement à la BD
const task = await db.findById(c.req.param("id"));

// BIEN : valider le format UUID en premier
const idSchema = z.string().uuid("UUID invalide");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
  return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "Format d'ID invalide" }] }, 400);
}
const task = await db.findById(parsed.data);

Piège 2 : Validation du Content-Type manquante

Pour les requêtes PUT/POST sans Content-Type: application/json, Hono traite le corps comme un objet vide. La validation zod passe et des données vides sont enregistrées.

// src/middleware/contentType.ts
import { createMiddleware } from "hono/factory";

export const requireJson = createMiddleware(async (c, next) => {
  if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
    const contentType = c.req.header("Content-Type");
    if (!contentType?.includes("application/json")) {
      return c.json(
        { data: null, meta: null, errors: [{ code: "UNSUPPORTED_MEDIA_TYPE", message: "Content-Type : application/json est requis" }] },
        415
      );
    }
  }
  await next();
});

Piège 3 : Problème de fuseau horaire pour les champs date

z.string().datetime() n’accepte que le format UTC. Si le frontend envoie 2026-04-25T09:00:00+02:00, c’est rejeté.

// MAL
due_date: z.string().datetime()

// BIEN : autoriser le décalage de fuseau horaire
due_date: z.string().datetime({ offset: true }).optional()

Conclusion

Intégrer Claude Code dans le développement d’APIs REST réduit considérablement le temps jusqu’à la mise en production :

  1. Générer la spec OpenAPI en premier — elle devient la base de toutes les étapes suivantes
  2. Générer les schémas Zod depuis la spec — les écrire à la main provoque des divergences
  3. Demander explicitement des tests happy-path, error-path et edge cases — sans instruction, seuls les happy paths sont écrits
  4. Anticiper les pièges dans le prompt — validation UUID, Content-Type, gestion des fuseaux horaires

Depuis l’adoption de ce workflow, le temps de développement par endpoint a diminué d’environ 70 %.

Cela dit, ne déployez jamais le code de Claude Code sans le relire. La sécurité et la logique métier ont toujours besoin d’yeux humains. Claude Code est un générateur de premiers jets rapides ; la responsabilité finale de la qualité incombe au développeur.


Articles connexes :

#claude-code #rest-api #openapi #typescript #backend

Passez votre flux Claude Code au niveau supérieur

50 modèles de prompts éprouvés, prêts à être copiés-collés dans Claude Code.

Gratuit

PDF gratuit : aide-mémoire Claude Code en 5 minutes

Laissez simplement votre e-mail et nous vous enverrons immédiatement l'aide-mémoire A4 en PDF.

Nous traitons vos données avec soin et n'envoyons jamais de spam.

Masa

À propos de l'auteur

Masa

Ingénieur passionné par Claude Code. Il gère claudecode-lab.com, un média tech en 10 langues avec plus de 2 000 pages.