Was ist Koa?
Koa ist ein minimales Web-Framework für Node.js, das vom selben Team entwickelt wurde wie Express. Während Express ein riesiges middleware-Ökosystem und die vertraute req/res-API aufgebaut hat, startete Koa mit zwei Zielen neu: ein winziger Core und erstklassige async/await-Unterstützung. Es ist weniger ein Konkurrent zu Express als vielmehr ein bewusstes Überdenken desselben.
Der ursprüngliche Express-Autor, TJ Holowaychuk, entwickelte Koa, um die Dinge zu beheben, die sich in Express zu dieser Zeit unpraktisch anfühlten – verschachtelte Callbacks, Ad-hoc-Fehlerbehandlung und ein Core, der mehr Funktionen angesammelt hatte, als die Autoren eigentlich wollten. Koa 1 nutzte Generator-Funktionen. Koa 2 erschien, nachdem Node 7.6 native async/await eingeführt hatte, und das ist die Version, die Sie heute verwenden werden.
Wenn Express Ihnen die middleware-Kette lehrt, dann zeigt Ihnen Koa, was passiert, wenn diese Kette eine Anfrage umschließen darf, anstatt sie nur zu durchlaufen.
Das Context-Objekt
Koa fasst den Request und die Response in einem einzigen Objekt zusammen, dem sogenannten Context, das konventionell ctx genannt wird. Anstatt req zu lesen und in res zu schreiben, lesen und schreiben Sie Eigenschaften an einem einzigen Objekt.
app.use(async (ctx) => {
ctx.status = 200; // response status
ctx.type = "application/json"; // response content type
ctx.body = { ok: true }; // response body
});
Der Request und die Response sind dennoch vollständig verfügbar, wenn Sie diese benötigen:
ctx.request— die gekapselte eingehende Nachricht (ctx.request.body,ctx.request.header).ctx.response— die gekapselte ausgehende Nachricht (ctx.response.status).ctx.params,ctx.queryundctx.request.body— Routing und geparste Eingaben.ctx.state— ein einfaches Objekt zur Übergabe von Werten zwischen middleware, wie zum Beispiel dem authentifizierten Benutzer.ctx.throw(status, message)— löst einen HTTP-Fehler aus, den die Error-middleware abfangen kann.
ctx.state ist der idiomatische Ort, um gemeinsam genutzte Daten zu hinterlegen. Die Authentication-middleware setzt ctx.state.user, und spätere middleware oder die Route lesen diesen Wert aus, ohne das Request-Objekt selbst zu verunreinigen.
Die Zwiebel: Middleware, die umschließt
Eine Koa middleware ist eine einzige async Funktion mit der Signatur (ctx, next). Der Aufruf von await next() gibt die Kontrolle an die nächste Ebene weiter; alles, was Sie nach dieser Zeile schreiben, wird ausgeführt, sobald die inneren Ebenen abgeschlossen sind.
request ──▶ mw1 before ──▶ mw2 before ──▶ route
│
response ◀── mw1 after ◀── mw2 after ◀────────┘
Dies ist das Zwiebel-Modell (Onion Model) – die zentrale Idee, die Koa so besonders macht. Eine flache Kette kann Code nur vor der Antwort ausführen; die Zwiebel kann Code auf beiden Seiten ausführen.
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // everything downstream runs here
ctx.set("X-Response-Time", `${Date.now() - start}ms`);
});
Diese eine middleware misst die Zeit der gesamten Anfrage, einschließlich jeder Route und jeder anderen darunter registrierten middleware. Dasselbe Muster wird für Logging, Datenbanktransaktionen und Cleanup verwendet.
Eine middleware kann die Kette auch unterbrechen (Short-Circuiting): Wenn sie ctx.body setzt und next() niemals aufruft, endet die Anfrage an dieser Stelle. Auf diese Weise kann eine Authentifizierung eine Anfrage ablehnen, bevor sie eine Route erreicht.
Routing mit @koa/router
Koa besitzt keinen integrierten Router im Core, daher wird das Routing über @koa/router bereitgestellt, ein von der Community gepflegtes Paket.
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() mountet die passenden Handler, und router.allowedMethods() antwortet mit dem korrekten 405, wenn der Pfad existiert, aber die Methode nicht. Router können verschachtelt und mit Präfixen versehen werden, wodurch eine große API modular bleibt – ähnlich wie bei express.Router().
Fehlerbehandlung und app.on(“error”)
Da Koa middleware gewöhnliche async-Funktionen sind, sind Fehler gewöhnliche Exceptions. Fangen Sie diese an einer zentralen Stelle ab, indem Sie den downstream-Aufruf umschließen.
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);
}
});
Innerhalb eines Handlers erstellt ctx.throw(404, "not found") einen Fehler mit einem status, einer Nachricht und einem expose-Flag, das kennzeichnet, ob der Fehler sicher für Clients angezeigt werden kann. Fehler ohne Status werden zu 500, und ihre Nachricht wird in der Antwort ausgeblendet.
Nicht jeder Fehler kann in eine Antwort umgewandelt werden. Wenn ein Fehler auftritt, nachdem die Header bereits gesendet wurden, löst Koa stattdessen ein error-Event an der App aus:
app.on("error", (err, ctx) => {
console.error(`${ctx.method} ${ctx.url}`, err);
});
Abonnieren Sie dieses Event in jedem Fall, damit unerwartete Fehler immer protokolliert werden.
Warum Koa keinen integrierten Router oder Body Parser besitzt
Der Kern von Koa ist bewusst fast leer gehalten. Er stellt dir den Context, die middleware-Pipeline und das HTTP-Grundgerüst zur Verfügung; Routing, Body Parsing, Cookies, Sessions, statische Dateien und Security-Header befinden sich hingegen in separaten Paketen.
Dies ist ein bewusster Trade-off. Ein minimaler Kern ist einfach zu prüfen, hat wenige Abhängigkeiten und entwickelt sich langsam, sodass es für Koa selbst schwierig ist, deine App zu beeinträchtigen. Der Preis dafür ist, dass du die Komposition selbst übernimmst: Du entscheidest, welchen Body Parser du hinzufügst, wie er parst und wo er registriert wird.
import bodyParser from "koa-bodyparser";
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
Die Reihenfolge ist hier genauso entscheidend wie in Express. Ein Parser, der nach dem Router registriert wird, steht den Routes nicht zur Verfügung.
Koa oder Express?
Beide Frameworks verfolgen die gleichen Ansätze bei Request und Response, daher ist die Entscheidung primär eine Frage der Philosophie.
Wähle Express, wenn Bekanntheit am wichtigsten ist: Es ist das am weitesten verbreitete Node-Framework, bringt ein integriertes Routing mit und verfügt über die größte Sammlung an middleware. Für Teams, die schnell vorankommen wollen, ist es die sicherste Standardwahl.
Wähle Koa, wenn du einen kleineren Core und das Onion-Modell bevorzugst. Die asynchrone Fehlerbehandlung ist sauberer, das Context-Objekt erspart viel Gefrickel mit req/res, und du zahlst nur für die middleware, die du tatsächlich hinzufügst. Der Kompromiss ist ein kleineres Ökosystem und ein höherer Konfigurationsaufwand deinerseits.
Eine Koa-App testen
Da eine Koa-App eine middleware-Pipeline ist, lässt sie sich problemlos mit supertest testen. Exportieren Sie die App und übergeben Sie app.callback() an den Request-Helper, damit kein Port geöffnet werden muss.
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);
});
Wie bei Express sollten Sie die App-Definition von app.listen() trennen, damit Tests sie importieren können, ohne einen Server zu starten.
Best Practices
- Platzieren Sie die Error-Handling middleware an erster Stelle, damit sie alle anderen Layer umschließt.
- Verwenden Sie
ctx.statefür Request-Scoped Values, wie zum Beispiel den authentifizierten Benutzer. - Setzen Sie
ctx.bodyundctx.status, anstatt die raw response direkt zu manipulieren. - Registrieren Sie
bodyParservor dem Router, damitctx.request.bodybefüllt wird. - Nutzen Sie
ctx.throwfür erwartete HTTP-Fehler und einen einzelnen Handler für alle anderen Fälle. - Abonnieren Sie immer
app.on("error"), um Fehler zu loggen, die nach dem Senden der Header auftreten. - Exportieren Sie die App separat vom Server, damit die Tests schnell bleiben.
Häufige Fehler
next()zweimal innerhalb einer middleware aufrufen und dadurch den nachfolgenden Code erneut ausführen.awaitvornext()vergessen, wodurch die “Rückweg”-Hälfte der Onion-Struktur übersprungen wird.- Den Router vor dem Body Parser registrieren und feststellen, dass
ctx.request.bodyleer ist. - Davon ausgehen, dass Koa einen integrierten Router oder Body Parser besitzt, und das falsche Paket importieren.
- Fehler verschlucken, ohne sie zu emittieren, sodass nichts in den Logs erscheint.
ctx.resdirekt mutieren und damit die Status- und Header-Verarbeitung von Koa umgehen.
Wie geht es weiter?
Koa ist die sauberste Demonstration von async middleware in Node, und sein Onion-Modell findet sich in Frameworks über alle Sprachen hinweg wieder. Wenn Sie einen umfangreicheren Funktionsumfang und mehr Geschwindigkeit suchen, lesen Sie den Fastify-Guide. Wenn Sie das Framework bevorzugen, aus dem Koa hervorgegangen ist, schauen Sie sich Express noch einmal an. Um die darunterliegende HTTP-Schicht von ctx zu verstehen, beginnen Sie mit dem HTTP-Guide und halten Sie die Node.js basics griffbereit.