# Koapp Controller

> Organize route handlers into reusable classes by extending the @axiosleo/koapp Controller base class. Use when grouping related handlers, reusing response helpers (this.success/failed/result/response/error/log), sharing state across handlers, or scaffolding service modules for a koapp project. Use when this capability is needed.

- Skill: `tomevault-io/koapp-controller` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/koapp-controller`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/koapp-controller/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/koapp-controller

---


# @axiosleo/koapp Controller

Source: [`src/controller.js`](../../../src/controller.js).

`Controller` is a thin base class that mirrors the
[response helpers](../koapp-response/SKILL.md) as instance methods. Extend
it to keep related handlers together and share auxiliary state.

## Import

```javascript
const { Controller } = require('@axiosleo/koapp');
```

## Instance API

| Method | Equivalent function | Notes |
| --- | --- | --- |
| `this.success(data?, headers?)` | `success(data, headers)` | 200 OK JSON envelope |
| `this.failed(data?, code?, status?, headers?)` | `failed(data, code, status, headers)` | Non-2xx JSON envelope |
| `this.result(data, status?, headers?)` | `result(data, status, headers)` | Raw body pass-through |
| `this.response(data, code, status?, headers?)` | `response(data, code, status, headers)` | Full control envelope |
| `this.error(status, msg, headers?)` | `error(status, msg, headers)` | Shortcut error envelope |
| `this.log(...data)` | `debug.log` from `@axiosleo/cli-tool` | Conditional logging |

All `this.success / failed / result / response / error` throw, same as the
functional helpers. See **koapp-response** for envelope details.

## Basic usage

```javascript
const { Controller, Router } = require('@axiosleo/koapp');

class HealthController extends Controller {
  async ping(context) {
    this.log('ping from', context.request_id);
    this.success({ pong: true });
  }

  async version() {
    this.success({ version: require('../package.json').version });
  }
}

const health = new HealthController();
const router = new Router('/health');
router.get('/ping', (ctx) => health.ping(ctx));
router.get('/version', () => health.version());
```

## Why use Controller over plain functions?

- **Shared state**: load DB handles, caches, or config once in the
  constructor and reuse across methods.
- **Inheritance**: define a `BaseController` with common middlewares or
  helpers, then extend it per module (the `init` scaffolding template at
  [`assets/tmpl/services/src/modules/controller.ts.tmpl`](../../tmpl/services/src/modules/controller.ts.tmpl) does this).
- **Testability**: easier to mock a controller instance than free-floating
  functions.

## Binding handlers to routes

`Controller` methods expect a `context` argument and may use `this`, so bind
them explicitly when registering with the router:

```javascript
router.get('/users/{:id}', (ctx) => controller.find(ctx));
router.post('/users', (ctx) => controller.create(ctx));

// or with arrow methods
class UsersController extends Controller {
  find = async (ctx) => {
    const id = Number(ctx.params.id);
    this.success({ id });
  };
}
```

Pure wrappers `(ctx) => controller.method(ctx)` preserve `this` and avoid
`.bind` clutter.

## Error handling

Since `this.success / failed / ...` throw, any code after them does not
run. The workflow catches the throw and writes the response. Keep try /
catch only where you intend to shape a different error:

```javascript
async create(context) {
  try {
    const user = await this.db.users.insert(context.body);
    this.success({ id: user.id });
  } catch (err) {
    if (err.code === 'DUP_ENTRY') {
      this.failed({ email: context.body.email }, '409;Email Already Registered', 409);
    }
    throw err;  // let the workflow convert to 500
  }
}
```

## Logging

`this.log` is wired to `debug.log` from `@axiosleo/cli-tool`. Enable it by
running with `DEBUG=1` (or the cli-tool equivalent), or by setting
`debug: true` on the application.

```javascript
this.log('loading user', context.params.id);
```

## Composing with validators

Validation stays on the router:

```javascript
router.push('post', '/users', (ctx) => controller.create(ctx), {
  body: {
    rules: {
      name: 'required|string',
      email: 'required|email'
    }
  }
});
```

The validator runs before the handler, so by the time `controller.create`
executes you already have a validated `context.body`.

## Generator output

Running `npx @axiosleo/koapp gen -d ./meta -o ./services/src/modules`
produces `.controller.ts` files that extend a project-local
`BaseController`. That `BaseController` typically extends `Controller` and
adds database handles:

```typescript
import { Controller } from '@axiosleo/koapp';
import { mainDB } from '../services/db';

export class BaseController extends Controller {
  protected mainDB = mainDB;
}
```

## Common pitfalls

- Forgetting to pass `context` into instance methods: `router.get('/x', controller.handler)` loses `this` when the router calls the method as a free function. Use arrow wrappers.
- Returning `this.success(...)` from a handler is redundant - the throw short-circuits execution.
- Controllers are long-lived across requests; never stash per-request data on `this`. Use `context` or local variables instead.

## See also

- Response helper details: **koapp-response**
- Grouping controllers under a router tree: **koapp-router**
- Concrete controller recipes: [examples.md](examples.md)

---
> Source: [AxiosLeo/node-koapp](https://github.com/AxiosLeo/node-koapp) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-19 -->

