Koa vs Fastify in 2026: Middleware Architecture Compared
TL;DR
Fastify is the better choice for new projects in 2026. Koa (~2.8M weekly downloads) is the elegant, minimal successor to Express with async/await support. Fastify (~3.5M) is faster, has better TypeScript, and is more actively developed. Koa's main advantage is its onion-layer middleware model — elegant for complex middleware composition. If you're starting fresh, use Fastify. If you have an existing Koa codebase, there's no urgent reason to migrate.
Key Takeaways
- Koa: ~2.8M weekly downloads — Fastify: ~3.5M (npm, March 2026)
- Fastify is ~3x faster than Koa in benchmarks
- Koa's onion model is genuinely elegant for middleware composition
- Fastify has better TypeScript — generics for request/response typing
- Koa is unmaintained compared to Fastify — fewer releases, smaller core team
Koa's Core Idea: The Onion Model
Koa's middleware model is its claim to fame. Unlike Express's linear middleware (next()), Koa uses async generators that "wrap" the request:
// Koa's onion model — middleware runs in and out
const app = new Koa();
// This middleware wraps ALL subsequent middleware
app.use(async (ctx, next) => {
const start = Date.now();
console.log(`→ ${ctx.method} ${ctx.url}`);
await next(); // Runs all downstream middleware
const ms = Date.now() - start;
console.log(`← ${ctx.status} ${ms}ms`);
// ctx.body is now set from the route handler below
});
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: err.message };
ctx.app.emit('error', err, ctx);
}
});
// Route
app.use(async (ctx) => {
ctx.body = { hello: 'world' };
});
The execution order is:
- Middleware 1 (start) → 2. Middleware 2 (start) → 3. Route handler → 2. Middleware 2 (end) → 1. Middleware 1 (end)
This makes timing, error handling, and response transformation much cleaner than Express-style linear middleware.
Fastify's Plugin Model
// Fastify uses a plugin/hook model
import Fastify from 'fastify';
const app = Fastify({ logger: true });
// Hooks are the equivalent of Koa middleware
app.addHook('onRequest', async (request, reply) => {
request.startTime = Date.now();
});
app.addHook('onSend', async (request, reply, payload) => {
const ms = Date.now() - request.startTime;
reply.header('X-Response-Time', `${ms}ms`);
return payload;
});
// Error handler
app.setErrorHandler((error, request, reply) => {
reply.status(error.statusCode || 500).send({
error: error.message
});
});
Fastify's hooks are more explicit than Koa's middleware but serve the same purpose. The difference: Fastify separates "plugin registration" (encapsulated scope) from "hooks" (cross-cutting concerns).
TypeScript Comparison
// Koa with TypeScript — needs @types/koa, still loose
import Koa, { Context } from 'koa';
const app = new Koa();
app.use(async (ctx: Context) => {
// ctx.request.body is any — needs koa-bodyparser + @types
const body = ctx.request.body; // any
ctx.body = { received: body };
});
// Fastify — generics for full type safety
import Fastify from 'fastify';
import { Type } from '@sinclair/typebox';
const app = Fastify();
app.post<{
Body: { name: string; email: string }
Reply: { id: string }
}>(
'/users',
{
schema: {
body: Type.Object({
name: Type.String(),
email: Type.String({ format: 'email' }),
}),
},
},
async (request, reply) => {
// request.body is typed as { name: string; email: string }
const user = await db.user.create({ data: request.body });
return reply.status(201).send({ id: user.id }); // typed
}
);
Fastify's generics are more ergonomic than Koa's type system, especially for request/response body typing.
Performance
| Framework | Req/s | Notes |
|---|---|---|
| Fastify | ~230K | JSON schema validation via ajv |
| Koa | ~85K | No built-in validation |
| Express | ~80K | Baseline |
The Fastify advantage comes largely from:
- JSON Schema validation compiled to ajv (not evaluated at runtime)
- Fast-json-stringify for response serialization
- Optimized routing algorithm (radix trie)
Router Comparison
Both support basic routing, but neither has built-in routing as sophisticated as Express Router. Common solutions:
// Koa: use koa-router
import Router from '@koa/router';
const router = new Router();
router.get('/users/:id', async (ctx) => {
ctx.body = await getUser(ctx.params.id);
});
app.use(router.routes()).use(router.allowedMethods());
// Fastify: built-in routing with full TypeScript
app.get<{ Params: { id: string } }>('/users/:id', async (req) => {
return getUser(req.params.id);
});
Fastify's routing is built-in and typed. Koa requires @koa/router as a dependency.
When to Choose
Choose Koa when:
- Maintaining an existing Koa codebase
- You love the onion middleware model and want it explicitly
- Your team knows Koa and learning Fastify isn't worth the switch
- You're building something where the minimal surface area of Koa is appealing
Choose Fastify when:
- Starting a new Node.js API project
- TypeScript type safety matters
- Performance matters (microservices, high-throughput)
- You want a more active community and ecosystem
Migrate Koa → Fastify when:
- Performance is a blocking issue
- TypeScript type safety is causing bugs
- You're doing a significant refactor anyway
Compare Koa and Fastify package health on PkgPulse.
See the live comparison
View koa vs. fastify on PkgPulse →