Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

Mounting and routing

Eugene Lazutkin edited this page Apr 19, 2026 · 1 revision

Mounting and routing

The adapter matches routes against ctx.path. Any strategy that gets the right path into ctx.path before the adapter runs will work — the idiomatic choice is koa-mount.

Why koa-mount?

The adapter's matchRoute call treats the path as rooted at the collection:

  • / → list / create / bulk-delete
  • /-by-names → collection method
  • /earth → item
  • /earth/-clone → item method

If you mount the adapter top-level with app.use(createKoaAdapter(planets)), a request for /planets/earth doesn't match — the adapter sees /planets/earth as a two-segment path (unknown shape) and hands back to next(). That's usually not what you want.

koa-mount rewrites ctx.path so the adapter sees /earth:

import mount from 'koa-mount';
app.use(mount('/planets', createKoaAdapter(planets)));
// GET /planets/earth → adapter sees GET /earth

Multiple collections

Mount as many adapters as you need. Each gets its own prefix and Adapter instance:

const planets = new Adapter({client, table: 'planets', keyFields: ['name']});
const moons   = new Adapter({client, table: 'moons',   keyFields: ['planet', 'name']});
const probes  = new Adapter({client, table: 'probes',  keyFields: ['id']});

const app = new Koa();
app.use(bodyParser());
app.use(mount('/planets', createKoaAdapter(planets)));
app.use(mount('/moons',   createKoaAdapter(moons, {
  keyFromPath: raw => {
    const [planet, name] = raw.split(':');
    return {planet, name};
  }
})));
app.use(mount('/probes',  createKoaAdapter(probes)));
app.listen(3000);

Because the adapter hands back to next() on unknown routes, mount order doesn't matter within an app that doesn't share prefixes.

Using @koa/router

@koa/router is the other common Koa routing tool. It works too, but requires careful path handling:

import Router from '@koa/router';
import mount from 'koa-mount';

const router = new Router();

// `@koa/router` alone doesn't strip the mount prefix from `ctx.path`.
// Nest the adapter under `koa-mount` inside the router if you want that behavior:
router.use('/planets', mount('/', createKoaAdapter(planets)));
// ... or use a catch-all route and trust the adapter to delegate:
// router.all('/planets/:rest(.*)', (ctx, next) => {
//   ctx.path = '/' + (ctx.params.rest || '');
//   return createKoaAdapter(planets)(ctx, next);
// });

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

In practice, plain koa-mount is shorter and avoids the prefix-stripping dance. Use @koa/router when you're already routing other endpoints with it.

Scoping body parsers per adapter

Global bodyParser() runs before everything. If one adapter needs a larger body cap than the rest, scope the parser:

import bodyParser from 'koa-bodyparser';

// Global — 256 KiB default
app.use(bodyParser({jsonLimit: '256kb'}));
app.use(mount('/planets', createKoaAdapter(planets)));

// Bulk endpoint — mount BEFORE the global parser, stream its own body
// (adapter's maxBodyBytes kicks in because ctx.request.body is undefined here).
const bulkApp = new Koa();
bulkApp.use(createKoaAdapter(bulk, {maxBodyBytes: 16 * 1024 * 1024}));
app.use(mount('/bulk', bulkApp));

See Body parsing for the complete decision flow.

Mounting adjacent middleware

Other middleware can run before or after the adapter. Because the adapter calls next() on unknown routes, anything mounted afterward still sees requests that don't match the route pack.

app.use(mount('/planets', createKoaAdapter(planets)));
app.use(mount('/planets', async (ctx, next) => {
  // Hit for /planets/special-endpoint (unknown shape, adapter delegated)
  if (ctx.path === '/special-endpoint') {
    ctx.body = {custom: true};
    return;
  }
  await next();
}));

Use this to add per-collection endpoints that aren't part of the standard route pack.

Response logging / metrics

Standard Koa middleware composes naturally — the adapter writes to ctx.body and ctx.status, so downstream loggers / metrics middleware can read them:

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  console.log(`${ctx.method} ${ctx.path} ${ctx.status} ${Date.now() - start}ms`);
});
app.use(mount('/planets', createKoaAdapter(planets)));

This is the main reason the adapter uses ctx.body/ctx.status instead of writing to ctx.res directly — everything in the Koa ecosystem that reads the response sees the expected state.

Compression, conditional responses, CORS

All standard Koa middleware works:

import compress from 'koa-compress';
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
import cors from '@koa/cors';

app.use(compress());
app.use(conditional());
app.use(etag());
app.use(cors());
app.use(bodyParser());
app.use(mount('/planets', createKoaAdapter(planets)));

The adapter sets JSON bodies via ctx.body = and lets Koa's response pipeline handle serialization, ETags, compression, and content-type negotiation.

Clone this wiki locally