Qu’est-ce que Koa ?
Koa est un framework web minimaliste pour Node.js conçu par la même équipe qu’Express. Alors qu’Express a développé un vaste écosystème de middleware et l’API req/res bien connue, Koa a repris les bases avec deux objectifs : un cœur léger et un support de premier ordre pour async/await. Plus qu’un concurrent d’Express, Koa est une réflexion délibérée sur sa conception.
L’auteur original d’Express, TJ Holowaychuk, a créé Koa pour corriger ce qui semblait maladroit dans Express à l’époque : les callbacks imbriqués, la gestion d’erreurs ad hoc et un cœur devenu trop volumineux. Koa 1 utilisait des fonctions génératrices. Koa 2 est arrivé après que Node 7.6 a introduit nativement async/await, et c’est cette version que vous utiliserez aujourd’hui.
Si Express vous enseigne la chaîne de middleware, Koa vous montre ce qui se passe lorsque cette chaîne peut envelopper une requête au lieu de simplement la traverser.
L’objet context
Koa fusionne la requête et la réponse en un seul objet appelé le context, conventionnellement nommé ctx. Au lieu de lire req et d’écrire dans res, vous lisez et écrivez des propriétés sur un seul et même objet.
app.use(async (ctx) => {
ctx.status = 200; // response status
ctx.type = "application/json"; // response content type
ctx.body = { ok: true }; // response body
});
La requête et la réponse restent intégralement disponibles lorsque vous en avez besoin :
ctx.request— le message entrant encapsulé (ctx.request.body,ctx.request.header).ctx.response— le message sortant encapsulé (ctx.response.status).ctx.params,ctx.queryetctx.request.body— le routage et les entrées analysées.ctx.state— un objet simple pour transmettre des valeurs entre middleware, comme l’utilisateur authentifié.ctx.throw(status, message)— pour lever une erreur HTTP que le middleware d’erreur pourra intercepter.
ctx.state est l’endroit idiomatique pour attacher des données partagées. Le middleware d’authentification définit ctx.state.user, puis les middleware suivants ou la route le lisent, sans polluer l’objet de requête lui-même.
L’oignon : le middleware qui enveloppe
Un middleware Koa est une simple fonction async avec la signature (ctx, next). Appeler await next() transfère le contrôle vers l’intérieur ; tout ce que vous écrivez après cette ligne sera exécuté une fois que les couches internes auront terminé leur traitement.
request ──▶ mw1 before ──▶ mw2 before ──▶ route
│
response ◀── mw1 after ◀── mw2 after ◀────────┘
C’est le modèle en oignon, et c’est l’idée fondamentale qui rend Koa unique. Une chaîne plate ne peut exécuter du code qu’avant la réponse ; l’oignon peut exécuter du code des deux côtés.
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // everything downstream runs here
ctx.set("X-Response-Time", `${Date.now() - start}ms`);
});
Ce middleware unique chronomètre l’intégralité de la requête, y compris chaque route et chaque autre middleware enregistré en dessous de lui. Ce même modèle est utilisé pour la journalisation (logging), les transactions de base de données et le nettoyage.
Un middleware peut également court-circuiter la chaîne : s’il définit ctx.body et n’appelle jamais next(), la requête s’arrête là. C’est ainsi que l’authentification rejette une requête avant qu’elle n’atteigne une route.
Le routage avec @koa/router
Koa ne possède pas de routeur intégré, le routage est donc assuré par @koa/router, le package maintenu par la communauté.
import Koa from "koa";
import Router from "@koa/router";
const app = new Koa();
const router = new Router();
router.get("/posts", async (ctx) => {
ctx.body = await Post.find().limit(20);
});
router.get("/posts/:id", async (ctx) => {
const post = await Post.findById(ctx.params.id);
if (!post) ctx.throw(404, "post not found");
ctx.body = post;
});
app.use(router.routes());
app.use(router.allowedMethods());
router.routes() monte les gestionnaires correspondants, et router.allowedMethods() répond avec le 405 approprié lorsque le chemin existe mais que la méthode ne correspond pas. Les routeurs peuvent être imbriqués et préfixés, ce qui permet de garder une API volumineuse modulaire, tout comme le fait express.Router().
Gestion des erreurs et app.on(“error”)
Comme les middleware Koa sont des fonctions async ordinaires, les erreurs sont des exceptions classiques. Interceptez-les en un seul endroit en encapsulant l’appel descendant.
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status ?? 500;
ctx.body = { error: err.expose ? err.message : "internal_error" };
ctx.app.emit("error", err, ctx);
}
});
À l’intérieur d’un handler, ctx.throw(404, "not found") crée une erreur avec un status, un message et un flag expose qui indique s’il est sûr de l’afficher aux clients. Les erreurs sans statut deviennent des 500, et leur message est masqué dans la réponse.
Toutes les erreurs ne peuvent pas être transformées en réponse. Si une panne survient après l’envoi des headers, Koa émet alors un événement error sur l’app :
app.on("error", (err, ctx) => {
console.error(`${ctx.method} ${ctx.url}`, err);
});
Abonnez-vous systématiquement à cet événement afin que les pannes imprévues soient toujours journalisées.
Pourquoi Koa n’a pas de routeur ou de body parser intégré
Le cœur de Koa est intentionnellement presque vide. Il vous fournit le contexte, le pipeline de middleware et la plomberie HTTP ; le routage, le parsing du corps de requête (body parsing), les cookies, les sessions, les fichiers statiques et les headers de sécurité résident tous dans des packages séparés.
C’est un compromis délibéré. Un cœur minimaliste est facile à auditer, possède peu de dépendances et évolue lentement, ce qui limite les risques que Koa lui-même ne casse votre application. La contrepartie est que vous êtes responsable de la composition : c’est à vous de décider quel body parser ajouter, comment le configurer et où l’enregistrer.
import bodyParser from "koa-bodyparser";
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
L’ordre est ici tout aussi important que dans Express. Un parser enregistré après le routeur ne sera pas disponible pour les routes.
Koa ou Express ?
Les deux frameworks partagent les mêmes concepts de requête/réponse, le choix repose donc principalement sur une question de philosophie.
Choisissez Express lorsque la familiarité est prioritaire : c’est le framework Node.js le plus utilisé, il inclut nativement le routage et possède la plus vaste collection de middleware. C’est l’option par défaut la plus sûre pour une équipe qui souhaite avancer rapidement.
Choisissez Koa si vous préférez un cœur plus léger et le modèle en “oignon” (onion model). La gestion des erreurs asynchrones y est plus propre, l’objet de contexte évite bien des manipulations entre req/res, et vous n’installez que les middleware dont vous avez réellement besoin. En contrepartie, l’écosystème est plus restreint et demande plus de configuration manuelle de votre part.
Tester une application Koa
Comme une application Koa est un pipeline de middleware, elle est facile à tester avec supertest. Exportez l’application et passez app.callback() à l’assistant de requête afin qu’aucun port ne soit ouvert.
import request from "supertest";
import app from "../app.js";
test("GET /posts returns a list", async () => {
const res = await request(app.callback()).get("/posts").expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
Comme pour Express, gardez la définition de l’application séparée de app.listen() pour que les tests puissent l’importer sans démarrer de serveur.
Bonnes pratiques
- Placez le middleware de gestion des erreurs en premier afin qu’il englobe toutes les autres couches.
- Utilisez
ctx.statepour les valeurs liées à la requête, comme l’utilisateur authentifié. - Définissez
ctx.bodyetctx.statusplutôt que de manipuler directement la réponse brute. - Enregistrez
bodyParseravant le routeur pour quectx.request.bodysoit renseigné. - Utilisez
ctx.throwpour les erreurs HTTP attendues et un gestionnaire unique pour toutes les autres. - Abonnez-vous toujours à
app.on("error")pour journaliser les échecs qui surviennent après l’envoi des headers. - Exportez l’application séparément du serveur pour garantir la rapidité des tests.
Erreurs courantes
- Appeler
next()deux fois dans un même middleware, ce qui exécute à nouveau le code en aval. - Oublier
awaitavantnext(), ce qui court-circuite la phase de “retour” de l’oignon. - Enregistrer le routeur avant le body parser et constater que
ctx.request.bodyest vide. - Supposer que Koa possède un routeur ou un body parser intégré et importer le mauvais package.
- Intercepter des erreurs sans les propager, ne laissant aucune trace dans les logs.
- Muter
ctx.resdirectement, contournant ainsi la gestion des statuts et des headers de Koa.
Et après ?
Koa est la démonstration la plus pure du fonctionnement des middleware asynchrones dans Node.js, et son modèle en “oignon” se retrouve dans des frameworks de tous les langages. Si vous recherchez un ensemble de fonctionnalités intégrées plus large et plus de performance, consultez le guide Fastify. Si vous préférez le framework dont Koa est issu, redécouvrez Express. Pour comprendre la couche HTTP sous-jacente à ctx, commencez par le guide HTTP, tout en gardant les bases de Node.js à portée de main.