
Koa review
Small async middleware framework for Node.js from the team behind Express, with no bundled middleware
Verdict
Koa is a good fit for developers who want a very small core and async middleware that can act before and after the rest of the stack, with Koa 3 adding Web response bodies and access to the current context. The catch is that almost everything else, from routing and body parsing to validation and types, comes from separate packages, and some of the official koajs ones, such as the joi router and JWT middleware, have not had a release in years.
Best for
Developers who want a minimal async core and pick every other piece
Not for
Teams that want validation, OpenAPI docs or project structure in the framework
Pros
- Middleware can run code before and after downstream handlers
- Small core that is quick to read in full
- Koa 3 accepts Web Response bodies and exposes the current context
- Official router, body parser, CORS, session and static file middleware
Cons
- Routing and body parsing are separate packages
- TypeScript types come from DefinitelyTyped, not the project
- Official validation and JWT middleware have not been released in years
Is Koa right for your project?
Describe what you need and AI will check the fit against this review.
Koa features and capabilities
Async middleware stack
Each middleware awaits the next one and can change the response on the way back, which makes timing, error handling and response rewrites simple to write.
One context object
Request and response helpers sit on a single ctx object, so handlers read and set everything in one place.
Current context in Koa 3
Code deep in a call stack can read the active request context through app.currentContext without passing ctx around.
Web standard response bodies
Handlers can set a Web Response or a custom stream as the body, which helps when sharing code with fetch-based libraries.
- TypeScript firstWritten in TypeScript or ships its own types, with typed routes or handlers
- PartlyTypes from @types/koa
- Request validationValidates bodies, params or queries against a schema, in the core or an official package
- PartlyOfficial koa-joi-router, last released 2021
- OpenAPI docsGenerates an OpenAPI spec or Swagger UI from routes
- PartlyCommunity packages only
- Beyond Node.jsOfficially runs on Bun, Deno or edge runtimes such as Cloudflare Workers
- NoNode.js only in the docs
- WebSocketsBuilt in or through an official plugin
- PartlyCommunity packages only
- Project structureModules, dependency injection or a fixed layout, not only routing
- No
- Database layerAn official ORM, query builder or data layer
- No
- AuthenticationAn official auth module, plugin or strategy system
- Yeskoa-jwt and koa-session in the koajs org
At a glance
- Type
- Minimal middleware framework
- Stack
- JavaScript / Node.js 18+
Free vs paid editions
What you get when you self-host, and what needs a paid plan.
Koa and its official middleware are free and have no paid edition or official hosting.
Running Koa
Koa 3 needs Node.js 18 or later, and the official @koa/router needs Node.js 20 or later.
New cloud server to ready
5 s
Memory in use at idle
82 MB of 4 GB
Reads per second
13,497
Writes per second
8,260
Startup
0.44 s
Version tested
3.2.1 · Oct 31, 2026
- Koa's core has no router or body parser, so the app adds @koa/router and @koa/bodyparser from the Koa organisation, plus Zod: 78 packages and 17 MB, the most of the light frameworks.
- Reads had a median latency of 3.5 ms and 6.0 ms at the 99th percentile.
- Malformed JSON returned the plain text Bad Request rather than JSON.
import Koa from "koa";
import Router from "@koa/router";
import { bodyParser } from "@koa/bodyparser";
import { z } from "zod";
// 100 items kept in memory, so the test measures the framework, not a database.
const items = new Map();
for (let i = 1; i <= 100; i++) items.set(i, { id: i, name: `Item ${i}`, price: i * 1.5, tags: ["sample"] });
let nextId = 101;
const Item = z.object({ name: z.string().min(1).max(100), price: z.number().min(0), tags: z.array(z.string()).max(10).optional() });
const app = new Koa();
const router = new Router();
router.get("/health", (ctx) => { ctx.body = { status: "ok" }; });
router.get("/items/:id", (ctx) => {
const item = items.get(Number(ctx.params.id));
if (!item) { ctx.status = 404; ctx.body = { error: "Not found" }; return; }
ctx.body = item;
});
router.post("/items", (ctx) => {
const parsed = Item.safeParse(ctx.request.body);
if (!parsed.success) { ctx.status = 400; ctx.body = { error: "Invalid item", issues: parsed.error.issues }; return; }
const item = { id: nextId++, ...parsed.data };
items.set(item.id, item);
ctx.status = 201;
ctx.body = item;
});
app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());
app.listen(Number(process.env.PORT ?? 3000), "127.0.0.1");{
"error": "Invalid item",
"issues": [
{
"origin": "string",
"code": "too_small",
"minimum": 1,
"inclusive": true,
"path": [
"name"
],
"message": "Too small: expected string to have >=1 characters"
},
{
"origin": "number",
"code": "too_small",
"minimum": 0,
"inclusive": true,
"path": [
"price"
],
"message": "Too small: expected number to be >=0"
}
]
}What you can do with Koa
Lightweight API services
Build a small REST service from Koa, @koa/router and @koa/bodyparser with only the parts it needs.
Proxies and response rewriting
Use middleware that runs after downstream handlers to add headers, cache or reshape responses.
Custom in-house frameworks
Use Koa as the base layer for a company framework that adds its own conventions and plugins.