Sept leçons à lire le menu. Aujourd'hui, on le cuisine.
Depuis la leçon 2, on interroge la même API : la bibliothèque. GET /livres, POST /livres, un 404 sur un livre qui n'existe pas. À chaque fois, tu envoyais des requêtes à un serveur imaginaire qui répondait comme par magie. Aujourd'hui, on enlève la magie. On l'écrit, ce serveur.
Pas de framework. Pas de Symfony, pas de Laravel. Du PHP natif, avec ce que tu sais déjà : PDO pour la base, json_encode pour le contrat. Le but n'est pas de faire « pro », c'est de voir le squelette nu de toute API : une requête entre, on regarde sa méthode et son URI, on décide quoi faire, on renvoie une réponse. C'est tout. Chaque framework rajoute du confort par-dessus ce squelette. Le comprendre, c'est comprendre tous les autres.
À la fin de cette leçon, l'API que tu interrogeais depuis sept leçons existera, ligne par ligne, sortie de tes mains.
On reste volontairement court : une soixantaine de lignes, réparties en blocs. Chaque bloc renvoie à la leçon qui l'a enseigné. Si un détail t'échappe, le numéro de leçon est là pour y retourner.
Le plan : un seul point d'entrée
Tout passe par un seul fichier : api/index.php. Une requête arrive, peu importe l'URL exacte, c'est ce fichier qui la reçoit. Il regarde deux choses : la méthode HTTP (GET, POST, PUT, DELETE) et l'URI (/livres ou /livres/88). Avec ce couple, il décide quoi faire.
C'est exactement le squelette de tout micro-framework, en version lisible. Un routeur, ce n'est rien d'autre que ça : un grand aiguillage sur [méthode, ressource]. On va le construire pièce par pièce.
Le code, bloc par bloc
Bloc 1 : lire la requête. On récupère la méthode, et on découpe l'URI pour en extraire la ressource (livres) et l'identifiant éventuel (88). C'est ce que la leçon 3 appelait « le verbe et la ressource ».
<?php
// api/index.php : le point d'entrée unique
$methode = $_SERVER['REQUEST_METHOD']; // GET, POST, PUT, DELETE (leçon 3)
$chemin = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$bouts = explode('/', trim($chemin, '/')); // ['api', 'livres', '88']
$ressource = $bouts[1] ?? ''; // 'livres'
$id = isset($bouts[2]) ? (int) $bouts[2] : null; // 88, ou null
Bloc 2 : le contrat JSON et les réponses. Le corps d'une requête (le JSON envoyé par le client en POST) se lit dans php://input, puis se décode (leçon 5). Et comme on va répondre partout, on s'écrit un helper repondre() : il pose le status code, le bon Content-Type, et encode le JSON.
// Le corps JSON entrant (leçon 5)
$corps = json_decode(file_get_contents('php://input'), true);
// Helper de réponse : status code + en-tête + JSON (leçon 4 et 5)
function repondre(int $code, array $data): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
Bloc 3 : les erreurs structurées. Quand quelque chose cloche, on ne renvoie pas du texte au hasard : on renvoie un application/problem+json (leçon 6, la RFC 9457). Un second helper s'en charge.
// Erreur normalisée problem+json (leçon 6, RFC 9457)
function probleme(int $status, string $title, string $detail): void {
http_response_code($status);
header('Content-Type: application/problem+json; charset=utf-8');
echo json_encode(['type' => 'about:blank', 'title' => $title,
'status' => $status, 'detail' => $detail], JSON_UNESCAPED_UNICODE);
}
Bloc 4 : le token sur les écritures. Lire un livre est public, mais en créer ou en supprimer un exige un token (leçon 7). On le lit dans l'en-tête Authorization, et on refuse en 401 s'il manque sur une méthode d'écriture.
// Le Bearer token, exigé sur les écritures (leçon 7)
$entetes = getallheaders();
$token = $entetes['Authorization'] ?? '';
$ecriture = in_array($methode, ['POST', 'PUT', 'DELETE'], true);
if ($ecriture && $token === '') {
probleme(401, 'Non authentifié', 'Un Bearer token est requis pour écrire.');
exit;
}
Bloc 5 : le routage. Le cœur. Un match de PHP 8 (vu au cours PHP) sur le couple [méthode, ressource a un id ?]. Chaque branche fait une chose, renvoie un status code de la leçon 4, et s'appuie sur des requêtes PDO que tu sais déjà écrire (esquissées en une ligne).
// L'aiguillage central (match PHP 8, cours PHP)
match (true) {
// GET /livres → la collection paginée (leçon 6)
$methode === 'GET' && $ressource === 'livres' && $id === null => (function () use ($pdo) {
$limit = min(max((int) ($_GET['limit'] ?? 20), 1), 100); // borné 1..100 (leçon 6)
$page = max((int) ($_GET['page'] ?? 1), 1);
$total = (int) $pdo->query('SELECT COUNT(*) FROM livres')->fetchColumn();
$rows = $pdo->query('SELECT * FROM livres LIMIT ' . $limit . ' OFFSET ' . (($page - 1) * $limit))->fetchAll();
repondre(200, ['data' => $rows, 'meta' => ['page' => $page, 'limit' => $limit, 'total' => $total]]);
})(),
// GET /livres/88 → un livre, ou 404 (leçon 4)
$methode === 'GET' && $ressource === 'livres' && $id !== null => (function () use ($pdo, $id) {
$stmt = $pdo->prepare('SELECT * FROM livres WHERE id = ?');
$stmt->execute([$id]);
$livre = $stmt->fetch();
$livre ? repondre(200, $livre) : probleme(404, 'Introuvable', 'Aucun livre nº ' . $id . '.');
})(),
// POST /livres → validation puis création (leçon 4 : 422, 201)
$methode === 'POST' && $ressource === 'livres' => (function () use ($pdo, $corps) {
if (empty($corps['titre'])) {
probleme(422, 'Validation', 'Le champ titre est obligatoire.'); // 422, pas 400 (leçon 4)
return;
}
$stmt = $pdo->prepare('INSERT INTO livres (titre, auteur) VALUES (?, ?)');
$stmt->execute([$corps['titre'], $corps['auteur'] ?? null]);
$nouvelId = (int) $pdo->lastInsertId();
header('Location: /api/livres/' . $nouvelId); // le 201 pointe la ressource créée
repondre(201, ['id' => $nouvelId, 'titre' => $corps['titre']]);
})(),
// PUT /livres/88 → remplace tout l'objet
$methode === 'PUT' && $ressource === 'livres' && $id !== null => (function () use ($pdo, $corps, $id) {
$stmt = $pdo->prepare('UPDATE livres SET titre = ?, auteur = ? WHERE id = ?');
$stmt->execute([$corps['titre'] ?? '', $corps['auteur'] ?? null, $id]);
repondre(200, ['id' => $id, 'titre' => $corps['titre'] ?? '']);
})(),
// DELETE /livres/88 → 204, pas de corps (leçon 4)
$methode === 'DELETE' && $ressource === 'livres' && $id !== null => (function () use ($pdo, $id) {
$pdo->prepare('DELETE FROM livres WHERE id = ?')->execute([$id]);
http_response_code(204); // 204 = succès sans contenu
})(),
// tout le reste → 404 propre
default => probleme(404, 'Route inconnue', 'Cette ressource n\'existe pas.'),
};
Relis ce match : il est ton API. Chaque ligne renvoie à une leçon du cours. Tu n'apprends rien de neuf ici, tu assembles. C'est ça, construire.
En vrai projet, on séparerait chaque branche dans sa propre fonction (un « contrôleur »), et les requêtes SQL seraient toutes des requêtes préparées (jamais de concaténation de variable utilisateur). Ici $limit et $page sont castés en entier et bornés avant la requête : c'est ce qui les rend sûrs malgré la concaténation. Le titre, lui, passe par ? : c'est la règle d'or contre l'injection SQL.
À toi : la séquence CRUD de la victoire
Un terminal simulé, branché sur ton API qui tourne enfin. Tu vas la dérouler en entier : lister, créer (avec ton token), relire ce que tu viens de créer, supprimer, et vérifier que c'est bien parti. Toute la boucle, de tes mains.
Tu fais un POST /api/livres avec un token valide et {"titre":"Fondation"}. Quel status code, et qu'est-ce que le serveur met dans l'en-tête de réponse pour te dire où trouver le nouveau livre ? Réfléchis avant de dérouler.
Voir la réponse
201 Created (leçon 4 : une ressource vient d'être créée), accompagné d'un en-tête Location: /api/livres/88 qui pointe l'URL du livre tout neuf. Le client n'a plus qu'à suivre ce Location pour relire sa création. C'est précisément ce que fait le helper repondre(201, ...) juste après le header('Location: ...').
Documenter : le menu lisible par les machines
Souviens-toi de la leçon 1 : une API, c'est un menu. Le client lit ce qui est disponible et commande. Mais jusqu'ici, ton menu n'existe que dans ta tête et dans ton code. Comment un autre développeur (ou une machine) sait-il que POST /livres attend un titre ?
La réponse standard s'appelle OpenAPI (anciennement Swagger). C'est un fichier qui décrit ton API dans un format que les humains et les machines lisent. Le menu, écrit noir sur blanc.
openapi: 3.1.0
info: { title: API Bibliotheque, version: 1.0.0 }
paths:
/livres:
get: { summary: Liste paginee des livres }
post: { summary: Cree un livre (titre requis), renvoie 201 + Location }
À partir de ce fichier, l'outil Swagger UI génère une page web interactive : la liste de tes endpoints, leurs paramètres, et même un bouton « Try it out » pour tester en direct. La doc n'est plus un document mort à maintenir à la main : elle est générée depuis le contrat. Le menu se met à jour tout seul.
Écris l'OpenAPI avant de coder (« design-first ») : front et back se mettent d'accord sur le contrat, puis chacun travaille de son côté sans s'attendre. Le menu d'abord, la cuisine ensuite.
Situer REST en 2026
Tu as appris REST. Mais ce n'est pas le seul style d'API. Pour ne pas être perdu en entretien ou en réunion d'archi, voici la carte, sans guerre de religion :
- REST : le défaut. Simple, universel, basé sur HTTP que tout le monde connaît. C'est le bon choix pour la grande majorité des API publiques et internes. Quand tu hésites, c'est REST.
- GraphQL : utile quand le front a des besoins de données très variables. Au lieu de subir les réponses figées du serveur, le client demande exactement les champs qu'il veut, en une seule requête. Pratique pour des apps mobiles aux écrans très différents.
- gRPC : le service-à-service interne, haute performance. Binaire, rapide, typé. On le trouve entre microservices d'un même système, là où la vitesse prime sur la lisibilité humaine.
Ce ne sont pas des concurrents qui s'excluent, mais des outils pour des besoins différents. Une même entreprise expose souvent du REST au public, du GraphQL à ses apps mobiles, et du gRPC entre ses services internes. Apprendre REST d'abord, c'est apprendre les fondations HTTP que les deux autres réutilisent.
Le menu, tu sais le lire ET l'écrire
On boucle là où on a commencé. Leçon 1 : une API, c'est un contrat, un menu entre deux logiciels. À l'époque, tu étais le client : tu lisais le menu et tu commandais. Huit leçons plus tard, tu es passé de l'autre côté du comptoir. Tu sais maintenant lire un menu d'API (le consommer) et l'écrire (le construire). C'est exactement ce qui sépare celui qui « utilise des API » de celui qui « fait des API ».
Tes 8 réflexes, un par leçon :
- Leçon 1 : une API est un contrat ; REST l'organise au-dessus de HTTP.
- Leçon 2 : des noms, pas des verbes.
/livres/42, jamais/getLivre. - Leçon 3 : le verbe HTTP dit l'action. GET lit, POST crée, PUT remplace, DELETE supprime.
- Leçon 4 : le bon status code. 201 + Location, 204 sans corps, 422 pour la validation.
- Leçon 5 : le JSON est le contrat de données. Content-Type des deux côtés.
- Leçon 6 : paginer, filtrer, et structurer les erreurs en problem+json.
- Leçon 7 : protéger les écritures par token. 401 si absent, 403 si interdit.
- Leçon 8 : tout assembler. Une requête entre, le routeur aiguille, le JSON repart.
Garde ce match comme modèle mental. Le jour où tu ouvriras un vrai routeur Symfony ou Express, tu reconnaîtras le même squelette dessous : méthode + URI → action → réponse. Tous les frameworks habillent cette idée. Toi, tu l'as vue nue.
Seven lessons reading the menu. Today, we cook it.
Since lesson 2, we've queried the same API: the library. GET /books, POST /books, a 404 on a book that doesn't exist. Each time, you sent requests to an imaginary server that answered as if by magic. Today, we remove the magic. We write that server.
No framework. No Symfony, no Laravel. Plain PHP, with what you already know: PDO for the database, json_encode for the contract. The goal isn't to be "pro", it's to see the bare skeleton of every API: a request comes in, we look at its method and URI, we decide what to do, we send a response. That's it. Every framework adds comfort on top of this skeleton. Understand it, and you understand all of them.
By the end of this lesson, the API you've been querying for seven lessons will exist, line by line, built by your own hands.
We deliberately keep it short: about sixty lines, split into blocks. Each block points back to the lesson that taught it. If a detail slips past you, the lesson number is there to go back.
The plan: a single entry point
Everything goes through one file: api/index.php. A request arrives, whatever the exact URL, and this file receives it. It looks at two things: the HTTP method (GET, POST, PUT, DELETE) and the URI (/books or /books/88). With that pair, it decides what to do.
This is exactly the skeleton of every micro-framework, in readable form. A router is nothing more than that: a big switch on [method, resource]. We'll build it piece by piece.
The code, block by block
Block 1: read the request. We grab the method, and split the URI to extract the resource (books) and the optional id (88). This is what lesson 3 called "the verb and the resource".
<?php
// api/index.php : the single entry point
$method = $_SERVER['REQUEST_METHOD']; // GET, POST, PUT, DELETE (lesson 3)
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$parts = explode('/', trim($path, '/')); // ['api', 'books', '88']
$resource = $parts[1] ?? ''; // 'books'
$id = isset($parts[2]) ? (int) $parts[2] : null; // 88, or null
Block 2: the JSON contract and responses. A request body (the JSON a client sends on POST) is read from php://input, then decoded (lesson 5). And since we'll respond everywhere, we write ourselves a respond() helper: it sets the status code, the right Content-Type, and encodes the JSON.
// The incoming JSON body (lesson 5)
$body = json_decode(file_get_contents('php://input'), true);
// Response helper: status code + header + JSON (lessons 4 and 5)
function respond(int $code, array $data): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
Block 3: structured errors. When something goes wrong, we don't return random text: we return application/problem+json (lesson 6, RFC 9457). A second helper handles it.
// Normalised problem+json error (lesson 6, RFC 9457)
function problem(int $status, string $title, string $detail): void {
http_response_code($status);
header('Content-Type: application/problem+json; charset=utf-8');
echo json_encode(['type' => 'about:blank', 'title' => $title,
'status' => $status, 'detail' => $detail], JSON_UNESCAPED_UNICODE);
}
Block 4: the token on writes. Reading a book is public, but creating or deleting one requires a token (lesson 7). We read it from the Authorization header, and reject with 401 if it's missing on a write method.
// The Bearer token, required on writes (lesson 7)
$headers = getallheaders();
$token = $headers['Authorization'] ?? '';
$write = in_array($method, ['POST', 'PUT', 'DELETE'], true);
if ($write && $token === '') {
problem(401, 'Unauthenticated', 'A Bearer token is required to write.');
exit;
}
Block 5: routing. The heart. A PHP 8 match (covered in the PHP course) on the pair [method, does it have an id?]. Each branch does one thing, returns a status code from lesson 4, and leans on PDO queries you already know how to write (sketched in one line).
// The central switch (PHP 8 match, PHP course)
match (true) {
// GET /books → the paginated collection (lesson 6)
$method === 'GET' && $resource === 'books' && $id === null => (function () use ($pdo) {
$limit = min(max((int) ($_GET['limit'] ?? 20), 1), 100); // clamped 1..100 (lesson 6)
$page = max((int) ($_GET['page'] ?? 1), 1);
$total = (int) $pdo->query('SELECT COUNT(*) FROM books')->fetchColumn();
$rows = $pdo->query('SELECT * FROM books LIMIT ' . $limit . ' OFFSET ' . (($page - 1) * $limit))->fetchAll();
respond(200, ['data' => $rows, 'meta' => ['page' => $page, 'limit' => $limit, 'total' => $total]]);
})(),
// GET /books/88 → one book, or 404 (lesson 4)
$method === 'GET' && $resource === 'books' && $id !== null => (function () use ($pdo, $id) {
$stmt = $pdo->prepare('SELECT * FROM books WHERE id = ?');
$stmt->execute([$id]);
$book = $stmt->fetch();
$book ? respond(200, $book) : problem(404, 'Not found', 'No book #' . $id . '.');
})(),
// POST /books → validation then creation (lesson 4: 422, 201)
$method === 'POST' && $resource === 'books' => (function () use ($pdo, $body) {
if (empty($body['title'])) {
problem(422, 'Validation', 'The title field is required.'); // 422, not 400 (lesson 4)
return;
}
$stmt = $pdo->prepare('INSERT INTO books (title, author) VALUES (?, ?)');
$stmt->execute([$body['title'], $body['author'] ?? null]);
$newId = (int) $pdo->lastInsertId();
header('Location: /api/books/' . $newId); // the 201 points to the created resource
respond(201, ['id' => $newId, 'title' => $body['title']]);
})(),
// PUT /books/88 → replace the whole object
$method === 'PUT' && $resource === 'books' && $id !== null => (function () use ($pdo, $body, $id) {
$stmt = $pdo->prepare('UPDATE books SET title = ?, author = ? WHERE id = ?');
$stmt->execute([$body['title'] ?? '', $body['author'] ?? null, $id]);
respond(200, ['id' => $id, 'title' => $body['title'] ?? '']);
})(),
// DELETE /books/88 → 204, no body (lesson 4)
$method === 'DELETE' && $resource === 'books' && $id !== null => (function () use ($pdo, $id) {
$pdo->prepare('DELETE FROM books WHERE id = ?')->execute([$id]);
http_response_code(204); // 204 = success with no content
})(),
// everything else → a clean 404
default => problem(404, 'Unknown route', 'This resource does not exist.'),
};
Read that match again: it is your API. Every line points back to a lesson. You learn nothing new here, you assemble. That's what building is.
In a real project, each branch would live in its own function (a "controller"), and all SQL queries would be prepared statements (never concatenating user input). Here $limit and $page are cast to integers and clamped before the query: that's what makes them safe despite the concatenation. The title goes through ?: that's the golden rule against SQL injection.
Your turn: the victory CRUD sequence
A simulated terminal, wired to your API now finally running. You'll run the whole loop: list, create (with your token), re-read what you just created, delete, and check it's truly gone. The full cycle, by your own hands.
You do a POST /api/books with a valid token and {"title":"Foundation"}. What status code, and what does the server put in the response header to tell you where to find the new book? Think before expanding.
Show the answer
201 Created (lesson 4: a resource was just created), along with a Location: /api/books/88 header pointing to the brand-new book's URL. The client just follows that Location to re-read its creation. That's exactly what the respond(201, ...) helper does right after header('Location: ...').
Documenting: the menu machines can read
Remember lesson 1: an API is a menu. The client reads what's available and orders. But so far, your menu only exists in your head and your code. How does another developer (or a machine) know that POST /books expects a title?
The standard answer is OpenAPI (formerly Swagger). It's a file that describes your API in a format humans and machines read. The menu, in black and white.
openapi: 3.1.0
info: { title: Library API, version: 1.0.0 }
paths:
/books:
get: { summary: Paginated list of books }
post: { summary: Create a book (title required), returns 201 + Location }
From this file, the Swagger UI tool generates an interactive web page: the list of your endpoints, their parameters, even a "Try it out" button to test live. The docs are no longer a dead document to maintain by hand: they're generated from the contract. The menu updates itself.
Write the OpenAPI before coding ("design-first"): front and back agree on the contract, then each works independently without waiting on the other. The menu first, the kitchen after.
Placing REST in 2026
You've learned REST. But it isn't the only API style. So you're not lost in an interview or an architecture meeting, here's the map, no holy war:
- REST: the default. Simple, universal, built on the HTTP everyone knows. The right choice for the vast majority of public and internal APIs. When in doubt, it's REST.
- GraphQL: useful when the front end has highly variable data needs. Instead of taking the server's fixed responses, the client asks for exactly the fields it wants, in a single request. Handy for mobile apps with very different screens.
- gRPC: internal service-to-service, high performance. Binary, fast, typed. Found between microservices of one system, where speed beats human readability.
These aren't mutually exclusive rivals, but tools for different needs. One company often exposes REST to the public, GraphQL to its mobile apps, and gRPC between its internal services. Learning REST first means learning the HTTP foundations the other two reuse.
The menu, you can read it AND write it
We close where we started. Lesson 1: an API is a contract, a menu between two programs. Back then, you were the client: you read the menu and ordered. Eight lessons later, you've moved to the other side of the counter. You can now read an API menu (consume it) and write it (build it). That's exactly what separates someone who "uses APIs" from someone who "makes APIs".
Your 8 reflexes, one per lesson:
- Lesson 1: an API is a contract; REST organises it on top of HTTP.
- Lesson 2: nouns, not verbs.
/books/42, never/getBook. - Lesson 3: the HTTP verb states the action. GET reads, POST creates, PUT replaces, DELETE removes.
- Lesson 4: the right status code. 201 + Location, 204 with no body, 422 for validation.
- Lesson 5: JSON is the data contract. Content-Type on both sides.
- Lesson 6: paginate, filter, and structure errors with problem+json.
- Lesson 7: protect writes with a token. 401 if absent, 403 if forbidden.
- Lesson 8: assemble it all. A request comes in, the router dispatches, the JSON goes back out.
Keep this match as a mental model. The day you open a real Symfony or Express router, you'll recognise the same skeleton underneath: method + URI → action → response. Every framework dresses up this idea. You've seen it bare.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Décris le squelette d'une mini-API : que se passe-t-il, du moment où une requête entre dans index.php jusqu'à la réponse ?
🧠 Rappel libre
Sans remonter : pour un POST qui crée une ressource, quel status code et quel en-tête renvoie-t-on ? Et pour un DELETE réussi ?
/api/livres/88). Un DELETE réussi renvoie 204 No Content : succès, mais aucun corps à renvoyer (la ressource n'existe plus). Bonus : si le titre manque au POST, c'est 422 (validation), pas 400.⚖️ Juge le code de l'IA
« Certains proxys d'entreprise bloquent le verbe DELETE. Pour contourner ça, je t'ajoute une route POST /api/livres/88/delete qui supprime le livre. » L'IA te propose ça. Tu acceptes, ou tu rejettes ?
/livres/88/delete) casse la règle des leçons 2 et 3 : des noms dans l'URI, l'action vient du verbe HTTP. Le besoin (un proxy qui bloque DELETE) est réel et le method override existe, mais le standard est l'en-tête X-HTTP-Method-Override: DELETE sur un POST, pas un verbe planqué dans le chemin. L'en-tête garde l'URI propre et reste réversible ; l'URL-verbe pollue durablement le contrat. Donc : refuser la route proposée, accepter éventuellement l'en-tête.POST /api/livres avec un Bearer token valide et {"titre":"Dune"}. Déroule TOUT ce que fait l'API, dans l'ordre.Your API works. But do you prove it on every change? The Testing your code course teaches you to armour it: every endpoint, every error code, checked automatically.
Course: Testing your code →