Node.js Framework

Koa

Koa is the tiny async/await rewrite of Express by its original authors. One context object and an onion of middleware — everything else is a package you choose.

intermediate14 min readUpdated Sep 16, 2026
app.js
js
// app.js
import Koa from "koa";
import Router from "@koa/router";

const app = new Koa();
const router = new Router();

router.get("/users/:id", async (ctx) => {
  const user = await db.user.findById(ctx.params.id);
  if (!user) ctx.throw(404, "user not found");
  ctx.body = user;
});

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  ctx.set("X-Response-Time", `${Date.now() - start}ms`);
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);
Released
2013
Created by
The Express team
Runs on
Node.js
Style
Minimal, async/await
Core idea
Onion middleware
Routing
@koa/router
Language
JavaScript / TypeScript

Why it matters

Why Koa exists

A core small enough to read

Koa ships almost nothing: no router, no body parser, no static file server. You add exactly the middleware you need, which keeps the dependency surface tiny and the control flow visible.

The onion, not a flat chain

Middleware are async functions that call await next(). Code before the call runs on the way in, code after it runs on the way out, so timing, logging and transactions wrap the whole request.

Middleware you actually choose

Everything beyond the core is a package. Pick a router, a parser, a logger and a session store, then compose them without a framework deciding the rest for you.

The big picture

Context, onion, async

One object holds the request and response, middleware wrap each other, and async/await carries the flow.

Context

Unify

A single ctx object merges the request and the response. ctx.body, ctx.status, ctx.params and ctx.state replace juggling req and res.

Onion

Compose

Each middleware is async (ctx, next) => {}. Calling await next() hands control downstream; the code after it runs once everything downstream has finished.

Async

Flow

Koa was built for promises from the start, so errors are ordinary exceptions that bubble to one handler instead of being silently dropped.

A short history

A small framework that influenced the rest

  1. 2013

    Koa is announced

    TJ Holowaychuk and the Express team release a tiny framework built on generator functions and the co library.

    13
  2. 2014

    The context object settles

    The merged ctx object and the onion middleware model become the shape Koa still uses today.

    14
  3. 2017

    Koa 2 and async/await

    Node 7.6 lands native async/await, and Koa 2 drops generators for plain promises.

    17
  4. 2019

    @koa/router

    The long-lived koa-router package is renamed and handed to the community as @koa/router.

    19
  5. Today

    Quiet and influential

    Koa keeps a small, stable API while newer frameworks borrow its async middleware ideas.

    Today

The complete guide

Koa: Everything you need to know

What is Koa?

Koa is a minimal web framework for Node.js written by the same team behind Express. Where Express grew a large middleware ecosystem and the familiar req/res API, Koa started over with two goals: a tiny core and first-class async/await. It is less a competitor to Express than a deliberate rethink of it.

The original Express author, TJ Holowaychuk, built Koa to fix what felt awkward about Express at the time — nested callbacks, ad-hoc error handling and a core that had accumulated more than its authors wanted. Koa 1 used generator functions. Koa 2 arrived after Node 7.6 brought native async/await, and that is the version you will use today.

If Express teaches you the middleware chain, Koa teaches you what happens when that chain is allowed to wrap around a request instead of only passing through it.

The context object

Koa collapses the request and the response into a single object called the context, conventionally named ctx. Instead of reading req and writing to res, you read and write properties on one object.

app.use(async (ctx) => {
  ctx.status = 200;               // response status
  ctx.type = "application/json";  // response content type
  ctx.body = { ok: true };        // response body
});

The request and response are still available in full when you need them:

  • ctx.request — the wrapped incoming message (ctx.request.body, ctx.request.header).
  • ctx.response — the wrapped outgoing message (ctx.response.status).
  • ctx.params, ctx.query and ctx.request.body — routing and parsed input.
  • ctx.state — a plain object for passing values between middleware, such as the authenticated user.
  • ctx.throw(status, message) — raise an HTTP error that the error middleware can catch.

ctx.state is the idiomatic place to attach shared data. Authentication middleware sets ctx.state.user, and later middleware or the route reads it, without polluting the request object itself.

The onion: middleware that wraps

Koa middleware is a single async function with the signature (ctx, next). Calling await next() passes control inward; whatever you write after that line runs once the inner layers have completed.

request  ──▶  mw1 before ──▶  mw2 before ──▶  route

response ◀──  mw1 after  ◀──  mw2 after  ◀────────┘

This is the onion model, and it is the single idea that makes Koa distinctive. A flat chain can only run code before the response; the onion can run code on both sides.

app.use(async (ctx, next) => {
  const start = Date.now();

  await next(); // everything downstream runs here

  ctx.set("X-Response-Time", `${Date.now() - start}ms`);
});

That one middleware times the entire request, including every route and every other middleware registered below it. The same pattern handles logging, database transactions, and cleanup.

A middleware can also short-circuit the chain: if it sets ctx.body and never calls next(), the request ends there. That is how authentication rejects a request before it reaches a route.

Routing with @koa/router

Koa has no router in its core, so routing is provided by @koa/router, the community-maintained package.

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() mounts the matching handlers, and router.allowedMethods() answers with a correct 405 when the path exists but the method does not. Routers can be nested and prefixed, which keeps a large API modular in the same way express.Router() does.

Error handling and app.on(“error”)

Because Koa middleware are ordinary async functions, errors are ordinary exceptions. Catch them in one place by wrapping the downstream call.

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);
  }
});

Inside a handler, ctx.throw(404, "not found") creates an error with a status, a message and an expose flag that marks it as safe to show to clients. Errors without a status become 500, and their message is hidden from the response.

Not every error can be turned into a response. If a failure happens after the headers are sent, Koa emits an error event on the app instead:

app.on("error", (err, ctx) => {
  console.error(`${ctx.method} ${ctx.url}`, err);
});

Subscribe to that event no matter what, so unexpected failures are always logged.

Why Koa has no built-in router or body parser

Koa’s core is intentionally almost empty. It gives you the context, the middleware pipeline and the HTTP plumbing; routing, body parsing, cookies, sessions, static files and security headers all live in separate packages.

This is a deliberate trade. A tiny core is easy to audit, has few dependencies and evolves slowly, so it is hard for Koa itself to break your app. The cost is that you own the composition: you decide which body parser to add, how to parse it, and where to register it.

import bodyParser from "koa-bodyparser";

app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());

Order matters here just as it does in Express. A parser registered after the router will not be available to the routes.

Koa or Express?

Both frameworks share the same request/response ideas, so the choice is mostly about philosophy.

Choose Express when familiarity matters most: it is the most widely used Node framework, ships with routing, and has the largest collection of middleware. It is the safest default for a team that wants to move quickly.

Choose Koa when you want a smaller core and the onion model. Async error handling is cleaner, the context object removes a lot of req/res juggling, and you pay for only the middleware you add. The trade is a smaller ecosystem and more assembly on your part.

Testing a Koa app

Because a Koa app is a middleware pipeline, it is straightforward to test with supertest. Export the app and pass app.callback() to the request helper so no port is opened.

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);
});

As with Express, keep the app definition separate from app.listen() so tests can import it without starting a server.

Best practices

  • Keep the error-handling middleware first so it wraps every other layer.
  • Use ctx.state for request-scoped values such as the authenticated user.
  • Set ctx.body and ctx.status rather than touching the raw response.
  • Register bodyParser before the router so ctx.request.body is populated.
  • Use ctx.throw for expected HTTP errors and a single handler for the rest.
  • Always subscribe to app.on("error") to log failures that happen after headers.
  • Export the app separately from the server so tests stay fast.

Common mistakes

  • Calling next() twice in one middleware and running downstream code again.
  • Forgetting await before next(), which skips the “way out” half of the onion.
  • Registering the router before the body parser and finding ctx.request.body empty.
  • Assuming Koa has a router or body parser built in and importing the wrong package.
  • Swallowing errors without emitting them, leaving nothing in the logs.
  • Mutating ctx.res directly and bypassing Koa’s status and header handling.

Where to go next

Koa is the cleanest demonstration of async middleware in Node, and its onion model shows up in frameworks across every language. If you want a larger built-in feature set and more speed, read the Fastify guide. If you prefer the framework Koa grew out of, revisit Express. To understand the HTTP layer underneath ctx, start with the HTTP guide, and keep the Node.js basics close by.

In practice

The onion in code

Switch between the tabs to see how middleware, routing and errors fit together.

middleware/timing.js
export async function timing(ctx, next) {
  const start = Date.now();

  // runs on the way in
  await next();

  // runs on the way out, after every downstream middleware
  const ms = Date.now() - start;
  ctx.set("X-Response-Time", `${ms}ms`);
}

The order of the onion

Middleware run top to bottom on the way in and bottom to top on the way out. Registering the logger last means it only wraps what comes after it.

Prefer
app.use(timing);          // wraps everything below
app.use(logger);
app.use(router.routes());
Avoid
app.use(router.routes());
app.use(timing);          // never runs for matched routes

Responding to a request

Set ctx.body and let Koa write the response. Reaching for the raw Node response skips Koa's status, header and error handling.

Prefer
ctx.status = 201;
ctx.body = post;
Avoid
ctx.res.statusCode = 201;
ctx.res.end(JSON.stringify(post));

Trade-offs

Is Koa the right base for your API?

Koa gives you a small, elegant core and leaves the rest to you. That is a strength and a cost.

Strengths

  • A core you can hold in your head

    The framework is a few hundred lines. You can read the source and know exactly how a request flows from socket to response.

  • The onion is genuinely useful

    Wrapping every request with timing, logging or a database transaction becomes trivial because code after await next() runs on the way out.

  • Clean async error handling

    Because middleware are async functions, a thrown error is caught by the nearest try/catch instead of vanishing into an unhandled rejection.

Trade-offs

  • You assemble the stack

    Routing, body parsing, cookies and static files are all separate packages. Budget time for choosing and wiring them, and for keeping them compatible.

  • A smaller ecosystem

    There are fewer Koa-specific middleware than Express middleware, though most Express packages have a thin Koa wrapper or a direct equivalent.

  • Less guidance for large apps

    Koa has no opinions on structure. Teams must agree on conventions early or large projects drift into inconsistent handlers.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Koa?

Our interactive tutorial walks you through Koa step by step — with quizzes and real code you can run in the browser.