Organise Handlers into a Controllers Module per Resource
Controller Functions
Extract route handler logic into named controller functions and group them by resource, then wire each controller to its @koa/router route.
What you'll learn
- Explain why inline anonymous handlers do not scale in larger APIs
- Create a controllers/ module that exports one function per route action
- Register controller functions on a router without tight coupling
Inline anonymous handlers work fine for toy examples, but they become hard to test and navigate as a codebase grows. The controller pattern moves each handler into a named function living in its own module, one module per resource.
The Problem with Inline Handlers
// hard to test, hard to reuse
router.get("/articles", async (ctx) => {
const rows = await db.query("SELECT * FROM articles");
ctx.body = rows;
}); Creating a Controller Module
Create src/controllers/articles.js and export one async function per action:
// src/controllers/articles.js
export async function listArticles(ctx) {
ctx.body = { articles: [] }; // replace with real DB call
}
export async function getArticle(ctx) {
const { id } = ctx.params;
ctx.body = { id: Number(id), title: "Placeholder" };
}
export async function createArticle(ctx) {
const { title, body } = ctx.request.body;
ctx.status = 201;
ctx.body = { id: Date.now(), title, body };
}
export async function updateArticle(ctx) {
const { id } = ctx.params;
ctx.body = { id: Number(id), ...ctx.request.body };
}
export async function deleteArticle(ctx) {
ctx.status = 204;
} Wiring Controllers to the Router
Import the controller functions and pass them to the corresponding router methods:
// src/routes/articles.js
import Router from "@koa/router";
import {
listArticles,
getArticle,
createArticle,
updateArticle,
deleteArticle,
} from "../controllers/articles.js";
const router = new Router({ prefix: "/articles" });
router.get("/", listArticles);
router.get("/:id", getArticle);
router.post("/", createArticle);
router.put("/:id", updateArticle);
router.delete("/:id", deleteArticle);
export default router; Mounting in app.js
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import articlesRouter from "./routes/articles.js";
const app = new Koa();
app.use(bodyParser());
app.use(articlesRouter.routes());
app.use(articlesRouter.allowedMethods());
app.listen(3000); Each controller function is now a plain async function that accepts ctx. That
makes it straightforward to unit-test by passing a mock context object without
spinning up an HTTP server.
Up Next
Controllers should stay thin. Move database queries and business rules into a dedicated service layer.
Service Layer →