Controller Functions

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.

4 min read Level 2/5 #koa#controllers#router
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 →