Koa
Koa: lightweight Node.js framework by the Express team with async/await middleware, context object, and composable architecture
You are an expert in building APIs with Koa. ## Key Points - Always place error handling middleware first (outermost layer of the onion) so it catches errors from all downstream middleware and route handlers. - Use `ctx.state` for passing data between middleware layers instead of attaching properties directly to `ctx` — this is the convention Koa reserves for application-level state. - Compose related middleware with `koa-compose` to create reusable middleware stacks that can be applied as a single unit to routes or routers. - Forgetting `await next()` in middleware, which silently stops the request from reaching downstream handlers and often results in empty 404 responses. - Using `ctx.body` instead of `ctx.request.body` to read the parsed request payload — `ctx.body` is the response body setter, and reading from it returns whatever you previously set as the response. ## Quick Example ```bash npm init -y npm install koa @koa/router koa-bodyparser npm install -D typescript @types/koa @types/koa__router @types/koa-bodyparser tsx ``` ```bash npm install zod ```
skilldb get api-frameworks-skills/KoaFull skill: 269 linesKoa — API Framework
You are an expert in building APIs with Koa.
Core Philosophy
Overview
Koa is a minimal and expressive Node.js web framework created by the team behind Express. It leverages async/await natively, replacing Express's callback-based middleware with a cleaner "onion model" where each middleware can act on both the request and the response. Koa itself ships with no middleware bundled — routing, body parsing, and other features are added via the ecosystem — giving developers full control over their stack.
Setup & Configuration
npm init -y
npm install koa @koa/router koa-bodyparser
npm install -D typescript @types/koa @types/koa__router @types/koa-bodyparser tsx
Basic server (src/server.ts):
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
const router = new Router();
// Global middleware
app.use(bodyParser());
// Routes
router.get('/', (ctx) => {
ctx.body = { status: 'ok' };
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Core Patterns
Routing with @koa/router
import Router from '@koa/router';
const router = new Router({ prefix: '/api/v1' });
router.get('/users', async (ctx) => {
ctx.body = await db.getUsers();
});
router.get('/users/:id', async (ctx) => {
const user = await db.getUserById(ctx.params.id);
if (!user) {
ctx.throw(404, 'User not found');
}
ctx.body = user;
});
router.post('/users', async (ctx) => {
const { name, email } = ctx.request.body as { name: string; email: string };
const user = await db.createUser({ name, email });
ctx.status = 201;
ctx.body = user;
});
router.put('/users/:id', async (ctx) => {
const user = await db.updateUser(ctx.params.id, ctx.request.body);
ctx.body = user;
});
router.delete('/users/:id', async (ctx) => {
await db.deleteUser(ctx.params.id);
ctx.status = 204;
});
Nested routers for modular route organization:
const usersRouter = new Router({ prefix: '/users' });
const postsRouter = new Router({ prefix: '/posts' });
// Mount sub-routers under the main API router
const apiRouter = new Router({ prefix: '/api/v1' });
apiRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
apiRouter.use(postsRouter.routes(), postsRouter.allowedMethods());
app.use(apiRouter.routes());
app.use(apiRouter.allowedMethods());
The Onion Middleware Model
Koa middleware uses the "onion" pattern — each middleware can execute code before and after await next(), wrapping downstream middleware.
// Logging middleware — runs code before AND after downstream
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // pass control downstream
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);
});
// Error handling middleware — wraps all downstream in try/catch
app.use(async (ctx, next) => {
try {
await next();
} catch (err: any) {
ctx.status = err.status || 500;
ctx.body = {
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
};
ctx.app.emit('error', err, ctx);
}
});
Validation Middleware
Using a lightweight validation approach with Zod:
npm install zod
import { z, ZodSchema } from 'zod';
import { Middleware } from 'koa';
function validate(schema: { body?: ZodSchema; query?: ZodSchema; params?: ZodSchema }): Middleware {
return async (ctx, next) => {
if (schema.body) {
const result = schema.body.safeParse(ctx.request.body);
if (!result.success) {
ctx.throw(400, JSON.stringify(result.error.flatten()));
}
ctx.request.body = result.data;
}
if (schema.query) {
const result = schema.query.safeParse(ctx.query);
if (!result.success) {
ctx.throw(400, JSON.stringify(result.error.flatten()));
}
}
await next();
};
}
// Usage
const createUserSchema = {
body: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).optional(),
}),
};
router.post('/users', validate(createUserSchema), async (ctx) => {
const { name, email, age } = ctx.request.body;
// Body is validated and typed
ctx.status = 201;
ctx.body = await db.createUser({ name, email, age });
});
Authentication Middleware
import { Middleware } from 'koa';
function authenticate(): Middleware {
return async (ctx, next) => {
const token = ctx.get('Authorization')?.replace('Bearer ', '');
if (!token) {
ctx.throw(401, 'Authentication required');
}
try {
const user = await verifyJWT(token);
ctx.state.user = user; // ctx.state is the recommended namespace for passing data
await next();
} catch {
ctx.throw(401, 'Invalid token');
}
};
}
// Apply to specific routes
router.get('/profile', authenticate(), async (ctx) => {
ctx.body = ctx.state.user;
});
// Or apply to an entire router
const protectedRouter = new Router();
protectedRouter.use(authenticate());
protectedRouter.get('/dashboard', async (ctx) => {
ctx.body = { user: ctx.state.user };
});
Context (ctx) Essentials
The Koa context encapsulates the request and response into a single object with convenience accessors.
app.use(async (ctx) => {
// Request
ctx.method; // HTTP method
ctx.url; // Full URL
ctx.path; // URL path
ctx.query; // Parsed query string object
ctx.params; // Route params (via @koa/router)
ctx.request.body; // Parsed body (via koa-bodyparser)
ctx.get('Content-Type'); // Request header
ctx.ip; // Client IP
// Response
ctx.status = 200; // Set status code
ctx.body = { data: 'value' }; // Set response body (auto-sets Content-Type)
ctx.set('X-Custom', 'value'); // Set response header
ctx.type = 'json'; // Set Content-Type shorthand
// Shared state between middleware
ctx.state.user = { id: 1 }; // Convention for middleware-to-handler data passing
// Errors
ctx.throw(404, 'Not found'); // Throw HTTP error
ctx.assert(ctx.state.user, 401, 'Login required'); // Assert or throw
});
Best Practices
- Always place error handling middleware first (outermost layer of the onion) so it catches errors from all downstream middleware and route handlers.
- Use
ctx.statefor passing data between middleware layers instead of attaching properties directly toctx— this is the convention Koa reserves for application-level state. - Compose related middleware with
koa-composeto create reusable middleware stacks that can be applied as a single unit to routes or routers.
Common Pitfalls
- Forgetting
await next()in middleware, which silently stops the request from reaching downstream handlers and often results in empty 404 responses. - Using
ctx.bodyinstead ofctx.request.bodyto read the parsed request payload —ctx.bodyis the response body setter, and reading from it returns whatever you previously set as the response.
Anti-Patterns
Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.
Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.
Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add api-frameworks-skills
Related Skills
Elysia
"Elysia: Bun-native web framework with type-safe routing, TypeBox validation, plugins, Eden treaty client, lifecycle hooks, and Swagger documentation"
Express.js
"Express.js with TypeScript: routing, middleware patterns, error handling, validation, authentication, static files, CORS, and production-ready configuration"
Fastify
Fastify: high-performance Node.js web framework with schema-based validation, logging, plugin architecture, and TypeScript support
Apollo GraphQL
"Apollo GraphQL: schema design, resolvers, Apollo Server, Apollo Client with React, useQuery/useMutation, caching strategies, subscriptions, and codegen"
Hono
"Hono: ultra-fast web framework for edge, serverless, and Node.js — middleware, routing, Zod validation, JWT, CORS, RPC client, and JSX support"
NestJS
NestJS: progressive Node.js framework with decorators, dependency injection, modules, guards, pipes, and interceptors for scalable APIs