Bundle Several Middleware Into One Reusable, Pipeable Function
koa-compose
koa-compose turns an array of middleware functions into a single function with the same (ctx, next) signature. It is how Koa builds its own internal pipeline and how you create shareable middleware stacks.
What you'll learn
- Use koa-compose to combine middleware into a single function
- Understand that Koa uses compose internally for app.use()
- Build a reusable middleware stack for API routes
koa-compose is the function at the heart of Koa. It takes an array of
middleware and returns a single function with the (ctx, next) signature —
exactly what app.use() expects.
Installation
npm install koa-compose Basic Usage
import compose from 'koa-compose';
const mw1 = async (ctx, next) => {
console.log('mw1 before');
await next();
console.log('mw1 after');
};
const mw2 = async (ctx, next) => {
console.log('mw2 before');
await next();
console.log('mw2 after');
};
const combined = compose([mw1, mw2]);
// Use anywhere an (ctx, next) function is accepted
app.use(combined); The composed function preserves the onion order: mw1 wraps mw2.
How Koa Uses It Internally
Every time you call app.use(fn), Koa pushes fn into an internal array.
When a request arrives, Koa calls compose(this.middleware)(ctx) to
execute the full pipeline. Understanding this makes the entire middleware
system predictable.
Reusable API Middleware Stack
A common pattern is composing a shared set of middleware for a sub-section of your app:
import compose from 'koa-compose';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import { requireAuth } from './auth.js';
import { rateLimiter } from './rate-limit.js';
// One composed unit applied to every API route
export const apiStack = compose([
cors({ origin: process.env.ALLOWED_ORIGIN }),
bodyParser(),
requireAuth,
rateLimiter,
]); // In your main app file
import { apiStack } from './middleware/api-stack.js';
import router from './router.js';
app.use(errorHandler);
app.use(apiStack); // all four middleware in one line
app.use(router.routes()); Composing for Testing
compose makes unit testing individual middleware stacks trivial because
the composed function is a plain async function:
import { createMockContext } from '@shopify/jest-koa-mocks';
const stack = compose([requireAuth, rateLimiter]);
test('rejects unauthenticated requests', async () => {
const ctx = createMockContext({ headers: {} });
await expect(stack(ctx, async () => {})).rejects.toThrow('401');
}); Nesting Composed Stacks
Composed stacks can themselves be composed — they are just functions:
const loggingStack = compose([requestId, logger]);
const securityStack = compose([helmet(), cors()]);
const fullStack = compose([loggingStack, securityStack]);
app.use(fullStack); Up Next
koa-mount takes a sub-application or composed middleware and mounts it
under a path prefix — the next step in modular Koa architecture.