Tips & Tricks (Mis à jour: 03/06/2026)

Créer sa première REST API avec Claude Code: CRUD, validation et tests

Créez une REST API Express avec Claude Code: CRUD, validation, erreurs, tests et code exécutable.

Créer sa première REST API avec Claude Code: CRUD, validation et tests

Quand on crée sa première REST API, le plus difficile n’est pas d’écrire des routes Express. Le vrai problème est de savoir ce qu’une API utilisable doit contenir: des URL claires, les bonnes méthodes HTTP, une validation fiable, des erreurs prévisibles et quelques tests qui prouvent que le code fonctionne.

Dans ce guide, vous allez construire avec Claude Code une petite API Todo en Express. Vous créerez le projet, ajouterez des endpoints CRUD, validerez l’entrée JSON, renverrez des codes de statut utiles et vérifierez le tout avec curl et le test runner intégré de Node.js. Le code vise Node.js 22/24 LTS ou une version plus récente.

Si Claude Code est encore nouveau pour vous, commencez par le guide de démarrage Claude Code. Pour approfondir les tests, lisez ensuite Claude Code API testing et la validation Zod avec Claude Code.

Les notions à connaître

Une REST API est une façon de manipuler des ressources via HTTP. Ici, la ressource est un todo. En termes simples, une autre application appelle une URL, envoie ou reçoit du JSON, puis lit le code de statut pour comprendre le résultat.

TermeSens simpleExemple
EndpointURL d’entrée de l’APIGET /todos
MéthodeVerbe HTTP qui décrit l’actionGET, POST, PUT, DELETE
Code de statutNombre qui indique le résultat200, 201, 400, 404
JSONFormat texte léger pour échanger des données{ "title": "Learn REST" }
ValidationVérification de la validité d’une entréeRefuser un title vide
IdempotenceLa même requête répétée garde le même état finalEnvoyer le même PUT deux fois

Les références officielles utiles sont les méthodes HTTP sur MDN, les codes de statut HTTP sur MDN, Express routing, Express error handling et le Node.js test runner.

Ce que nous allons construire

L’API stocke les Todos en mémoire. Après un redémarrage, elle revient à l’état initial. C’est volontaire: on apprend la forme REST avant d’ajouter une base de données.

ActionMéthode et URLStatut de succès
SantéGET /health200 OK
Lister les TodosGET /todos200 OK
Lire un TodoGET /todos/:id200 OK
Créer un TodoPOST /todos201 Created
Remplacer un TodoPUT /todos/:id200 OK
Supprimer un TodoDELETE /todos/:id204 No Content

Ce petit exemple correspond à des cas réels. Premier cas: une API interne de tâches, tickets ou demandes de revue. Deuxième cas: un backend mock pour développer une interface React, Vue ou Astro avant que le vrai backend soit prêt. Troisième cas: une petite API de contact ou de newsletter, en remplaçant title par email et message.

Donner des critères clairs à Claude Code

Indiquez la stack, les endpoints, le comportement d’erreur et la vérification. Un prompt vague comme “crée une API” produit souvent du code qui démarre, mais dont le design reste difficile à relire.

Create a beginner Todo REST API with Express 5 and Node.js ES Modules.

Requirements:
- Provide package.json and server.js
- Add GET /health, GET /todos, GET /todos/:id, POST /todos, PUT /todos/:id, DELETE /todos/:id
- Accept JSON input
- title must be 1 to 120 characters; completed must be boolean
- Return JSON errors for 400, 404, and 500
- Keep PUT idempotent: repeating the same request should not change the final resource state
- Include curl requests and a node:test example

La ligne sur l’idempotence compte. PUT représente un remplacement. Si le même corps est envoyé deux fois, le Todo doit finir dans le même état. Si updatedAt change à chaque appel identique, le code contredit l’explication REST.

Créer le projet

mkdir claude-rest-api
cd claude-rest-api
npm init -y
npm install express

Remplacez package.json:

{
  "name": "claude-rest-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}

server.js

import express from "express";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

const PORT = Number(process.env.PORT ?? 3000);

function createHttpError(status, message, details) {
  const error = new Error(message);
  error.status = status;
  error.details = details;
  return error;
}

function hasOwn(object, key) {
  return Object.prototype.hasOwnProperty.call(object, key);
}

function parseId(rawId) {
  const id = Number(rawId);
  return Number.isInteger(id) && id > 0 ? id : null;
}

function validateTodoInput(body, { partial = false } = {}) {
  const errors = [];

  if (!partial || hasOwn(body, "title")) {
    if (typeof body.title !== "string" || body.title.trim().length === 0) {
      errors.push({ field: "title", message: "title is required" });
    } else if (body.title.trim().length > 120) {
      errors.push({ field: "title", message: "title must be 120 characters or fewer" });
    }
  }

  if (!partial || hasOwn(body, "completed")) {
    if (typeof body.completed !== "boolean") {
      errors.push({ field: "completed", message: "completed must be a boolean" });
    }
  }

  return errors;
}

export function createApp() {
  const app = express();

  let nextId = 3;
  const todos = [
    {
      id: 1,
      title: "Read MDN HTTP status docs",
      completed: false,
      updatedAt: "2026-06-03T00:00:00.000Z"
    },
    {
      id: 2,
      title: "Ask Claude Code to review API errors",
      completed: true,
      updatedAt: "2026-06-03T00:00:00.000Z"
    }
  ];

  app.use(express.json({ limit: "32kb" }));

  app.get("/health", (req, res) => {
    res.json({ status: "ok", uptime: process.uptime() });
  });

  app.get("/todos", (req, res) => {
    res.json({ data: todos, count: todos.length });
  });

  app.get("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const todo = todos.find((item) => item.id === id);
    if (!todo) return next(createHttpError(404, "todo not found"));

    res.json({ data: todo });
  });

  app.post("/todos", (req, res, next) => {
    const errors = validateTodoInput(req.body);
    if (errors.length > 0) {
      return next(createHttpError(400, "invalid request body", errors));
    }

    const todo = {
      id: nextId,
      title: req.body.title.trim(),
      completed: req.body.completed,
      updatedAt: new Date().toISOString()
    };
    nextId += 1;
    todos.push(todo);

    res.status(201).location(`/todos/${todo.id}`).json({ data: todo });
  });

  app.put("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const errors = validateTodoInput(req.body);
    if (errors.length > 0) {
      return next(createHttpError(400, "invalid request body", errors));
    }

    const index = todos.findIndex((item) => item.id === id);
    if (index === -1) return next(createHttpError(404, "todo not found"));

    const previous = todos[index];
    const nextTodo = {
      ...previous,
      title: req.body.title.trim(),
      completed: req.body.completed
    };

    const changed =
      previous.title !== nextTodo.title || previous.completed !== nextTodo.completed;

    todos[index] = {
      ...nextTodo,
      updatedAt: changed ? new Date().toISOString() : previous.updatedAt
    };

    res.json({ data: todos[index] });
  });

  app.delete("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const index = todos.findIndex((item) => item.id === id);
    if (index === -1) return next(createHttpError(404, "todo not found"));

    todos.splice(index, 1);
    res.status(204).send();
  });

  app.use((req, res, next) => {
    next(createHttpError(404, `route not found: ${req.method} ${req.originalUrl}`));
  });

  app.use((err, req, res, next) => {
    const status = Number.isInteger(err.status) && err.status >= 400 ? err.status : 500;
    const body = {
      error: {
        status,
        message: status === 500 ? "Internal Server Error" : err.message
      }
    };

    if (err.details) {
      body.error.details = err.details;
    }

    res.status(status).json(body);
  });

  return app;
}

const currentFile = fileURLToPath(import.meta.url);
const startedFile = process.argv[1] ? resolve(process.argv[1]) : "";

if (startedFile === currentFile) {
  createApp().listen(PORT, () => {
    console.log(`REST API listening on http://localhost:${PORT}`);
  });
}

Le point essentiel est la forme stable des réponses: { data: ... } pour les succès, { error: ... } pour les échecs, et un code de statut adapté à chaque situation.

Tester avec curl

npm run dev
curl -i http://localhost:3000/health

curl -i http://localhost:3000/todos

curl -i -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Write API tests","completed":false}'

curl -i -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"","completed":false}'

curl -i -X PUT http://localhost:3000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Read MDN HTTP status docs","completed":true}'

curl -i -X DELETE http://localhost:3000/todos/2

Regardez les succès et les erreurs. Une API qui ne fonctionne que dans le cas idéal n’est pas prête pour une interface réelle.

Ajouter node:test

Créez server.test.js:

import assert from "node:assert/strict";
import test from "node:test";
import { createApp } from "./server.js";

function listen(app) {
  return new Promise((resolve) => {
    const server = app.listen(0, () => resolve(server));
  });
}

async function request(baseUrl, path, options = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    headers: { "Content-Type": "application/json", ...options.headers },
    ...options
  });
  const text = await response.text();
  return {
    response,
    body: text ? JSON.parse(text) : null
  };
}

test("creates, updates, and deletes a todo", async (t) => {
  const server = await listen(createApp());
  t.after(() => server.close());
  const baseUrl = `http://127.0.0.1:${server.address().port}`;

  const created = await request(baseUrl, "/todos", {
    method: "POST",
    body: JSON.stringify({ title: "Test the API", completed: false })
  });
  assert.equal(created.response.status, 201);
  assert.equal(created.body.data.title, "Test the API");

  const id = created.body.data.id;
  const updated = await request(baseUrl, `/todos/${id}`, {
    method: "PUT",
    body: JSON.stringify({ title: "Test the API", completed: true })
  });
  assert.equal(updated.response.status, 200);
  assert.equal(updated.body.data.completed, true);

  const deleted = await request(baseUrl, `/todos/${id}`, { method: "DELETE" });
  assert.equal(deleted.response.status, 204);
});

test("rejects invalid todo input", async (t) => {
  const server = await listen(createApp());
  t.after(() => server.close());
  const baseUrl = `http://127.0.0.1:${server.address().port}`;

  const result = await request(baseUrl, "/todos", {
    method: "POST",
    body: JSON.stringify({ title: "", completed: false })
  });

  assert.equal(result.response.status, 400);
  assert.equal(result.body.error.details[0].field, "title");
});
npm test

Ces tests sont modestes, mais ils donnent une base. Quand Claude Code ajoutera une base de données ou l’authentification, demandez-lui de garder cette suite au vert.

Pièges fréquents

N’utilisez pas des endpoints comme /getTodos. Laissez la méthode porter le verbe: GET /todos, POST /todos, PUT /todos/:id.

Ne renvoyez pas toujours 200. Une mauvaise entrée mérite 400, une ressource absente 404, une création 201, une suppression sans corps 204.

Ne faites pas confiance uniquement à la validation côté frontend. L’API peut être appelée directement, donc le serveur doit valider aussi.

N’exposez pas les stack traces aux clients. Gardez les détails dans les logs serveur et renvoyez un JSON contrôlé.

Ne cassez pas l’idempotence. Si un PUT identique modifie des timestamps ou des valeurs aléatoires à chaque appel, l’état final n’est plus stable.

Suite et CTA

Ajoutez ensuite une seule chose à la fois: base de données, validation de schéma, documentation API, puis authentification. Pour la conception des erreurs, consultez aussi Claude Code error handling patterns.

Pour pratiquer sur votre propre projet, gardez la fiche mémo gratuite sous la main. Pour des prompts réutilisables, des checklists de revue et des modèles CLAUDE.md, utilisez la bibliothèque de produits Claude Code. Pour l’onboarding d’équipe, les règles de revue d’API et le design des permissions, passez par la formation et le conseil Claude Code.

Après avoir testé le contenu de cet article en pratique, le déclic principal pour les débutants n’a pas été le CRUD lui-même. C’était de voir succès et erreurs côte à côte: un title vide renvoie 400, un ID inconnu renvoie 404, et le même PUT répété garde le même état final. REST devient alors un contrat entre frontend et backend, pas seulement une liste d’URL.

#claude-code #rest-api #beginner #typescript #backend #tutorial
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.