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
- Comprend les spécifications OpenAPI comme contexte — collez un YAML et il génère du code conforme à la spec instantanément
- Maîtrise le système de types TypeScript — produit du code type-safe dès le départ
- 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étrique | Avant Claude Code | Après Claude Code |
|---|---|---|
| Temps de définition des schémas | 30–45 min par endpoint | 5–10 min (prompt inclus) |
| Taux d’erreurs de types | Fréquent en fin de projet | Quasi nul dès le départ |
| Validations manquées | Signalées à chaque revue de code | Cas limites couverts automatiquement |
| Dérive spec/code | Obsolète en 2–3 semaines | Toujours 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 :
- Générer la spec OpenAPI en premier — elle devient la base de toutes les étapes suivantes
- Générer les schémas Zod depuis la spec — les écrire à la main provoque des divergences
- Demander explicitement des tests happy-path, error-path et edge cases — sans instruction, seuls les happy paths sont écrits
- 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 :
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.
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.
À 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.
Articles similaires
Guide complet pour débuter avec Claude Code 2026 | 7 étapes pour passer de zéro à une utilisation professionnelle
Le guide de démarrage complet pour les nouveaux utilisateurs de Claude Code. De l'installation à l'intégration dans un vrai workflow de développement — avec tous les pièges que Masa a rencontrés au début.
Créer une REST API avec Claude Code | Guide pratique pour débutants
Apprenez les bases des REST API avec Claude Code. Un guide pratique couvrant la conception d'endpoints, la validation et la gestion des erreurs — avec du code prêt à copier.
Claude Code vs Gemini CLI 2026 Comparaison Approfondie | En quoi l'IA de Google est-elle différente ?
Comparaison pratique de Claude Code et Gemini CLI par l'ingénieur DX Masa. Prix, autonomie, fenêtre de contexte et écosystème analysés. Avec un diagramme de décision pour choisir le bon outil.