> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Controller

> Bind HTTP and WebSocket routes to typed handler classes that receive a rich request context and return responses.

The `@ooneex/controller` component is the HTTP/WebSocket handler layer. A controller is a class with a single `index` method, bound to a route by a `@Route` decorator. When a request matches the route, the framework constructs a `context` — request data, response builder, logger, cache, user, locale, and route metadata — and calls `index(context)`. The handler reads what it needs from `context` and returns a response. Generics carry your `params`, `payload`, `queries`, and `response` shapes end to end, so the context is fully typed.

## Why this component

* **One method per route.** Every controller implements `IController` — a single typed `index(context)` handler. No base class to extend, no boilerplate.
* **Typed context.** `IController<T>` and `ContextType<T>` thread your `params`, `payload`, `queries`, and `response` types through the whole handler.
* **Rich request context.** The handler receives the parsed request, a response builder, the logger, cache, rate limiter, locale, authenticated user, headers, files, IP, host, and matched route metadata.
* **Decorator-bound routing.** `@Route.get`, `@Route.post`, `@Route.socket`, and friends bind a class to a path, method, version, validation schema, and roles in one place.
* **Sync or async.** `index` may return an `IResponse` directly or a `Promise<IResponse>` — both are supported.
* **HTTP and WebSocket.** The same controller shape handles both; socket controllers use a context with an added `channel` API.

## How it works

A request flows through routing into the matched controller and back out as a response. The handler never constructs the context — the framework does, then calls `index`.

| Stage   | What happens                                                                                           |
| ------- | ------------------------------------------------------------------------------------------------------ |
| Match   | The router matches the request to a route declared by a `@Route` decorator.                            |
| Context | The framework builds `ContextType<T>` — request, response, services, user, locale, route metadata.     |
| Handle  | `index(context)` runs your logic, reading `context.params`, `context.payload`, `context.queries`, etc. |
| Respond | The handler returns `context.response.json(...)` (or another response); the framework sends it.        |

The `index` signature is the whole contract:

```typescript theme={null}
interface IController<T extends ContextConfigType = ContextConfigType> {
  index: (
    context: ContextType<T>,
  ) => Promise<IResponse<T["response"]>> | IResponse<T["response"]>;
}
```

`ContextConfigType` describes the shape you bind to a route — `response` is always present, while `params`, `payload`, and `queries` come from the request config and are optional. `ContextType<T>` is the object passed to `index`. Its most-used members:

| Member                                        | Purpose                                                                              |
| --------------------------------------------- | ------------------------------------------------------------------------------------ |
| `request`                                     | Parsed `IRequest` — call `.params()`, `.payload()`, `.queries()`.                    |
| `response`                                    | Response builder, e.g. `response.json(data)`.                                        |
| `params` / `payload` / `queries`              | Typed request data taken from your config `T`.                                       |
| `route`                                       | Matched route metadata: `name`, `path`, `method`, `version`, `description`, `roles`. |
| `user`                                        | Authenticated `IUser` or `null`.                                                     |
| `logger`                                      | Request-scoped logger.                                                               |
| `cache` / `rateLimiter` / `permission`        | Optional services available on the context.                                          |
| `header` / `files` / `ip` / `host` / `method` | Raw request details.                                                                 |
| `lang`                                        | Detected locale (`LocaleInfoType`).                                                  |
| `env`                                         | Application environment (`IAppEnv`).                                                 |

## Usage

A controller is a class with one `index` method, decorated with a `@Route` method matching the HTTP verb. Define a route type once and pass it to both the decorator config and `ContextType<T>` so the context is fully typed.

```typescript theme={null}
import type { ContextType } from "@ooneex/controller";
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";

type UserListRouteType = {
  queries: { page?: string; limit?: string; search?: string };
  response: { data: { id: string; name: string }[]; total: number };
};

@Route.get("/users", {
  name: "user.list",
  version: 1,
  description: "List users with optional filtering and pagination",
  queries: Assert({ page: "string?", limit: "string?", search: "string?" }),
  response: Assert({ data: "object[]", total: "number" }),
  roles: ["ROLE_ADMIN"],
})
export class UserListController {
  constructor(private readonly userService: UserService) {}

  public async index(context: ContextType<UserListRouteType>) {
    const { page, limit, search } = context.request.queries();
    const result = await this.userService.execute({ page, limit, search });

    return context.response.json(result);
  }
}
```

The `@Route` object exposes one decorator per HTTP method — `get`, `post`, `put`, `patch`, `delete`, `options`, `head` — plus `socket` for WebSocket controllers. Each takes the route `path` and a config:

| Config field                       | Purpose                                                                       |
| ---------------------------------- | ----------------------------------------------------------------------------- |
| `name`                             | Unique route name in dot notation, e.g. `user.list`.                          |
| `version`                          | Route version (number).                                                       |
| `description`                      | Human-readable description.                                                   |
| `params`                           | Per-segment validators for `/path/:id` routes, e.g. `{ id: new AssertId() }`. |
| `payload` / `queries` / `response` | `Assert({...})` schemas for body, query string, and response.                 |
| `roles`                            | Role strings required to access the route, e.g. `["ROLE_USER"]`.              |

A controller with URL params reads them through `context.request.params()` (validated by the decorator's `params`):

```typescript theme={null}
import type { ContextType } from "@ooneex/controller";
import { Route } from "@ooneex/routing";
import { Assert, AssertId } from "@ooneex/validation";

type UserDetailRouteType = {
  params: { id: string };
  response: { id: string; name: string; email: string };
};

@Route.get("/users/:id", {
  name: "user.detail",
  version: 1,
  description: "Get user by ID",
  params: { id: new AssertId() },
  response: Assert({ id: "string", name: "string", email: "string" }),
  roles: ["ROLE_USER"],
})
export class UserDetailController {
  constructor(private readonly userService: UserService) {}

  public async index(context: ContextType<UserDetailRouteType>) {
    const { id } = context.request.params();
    const user = await this.userService.execute(id);

    return context.response.json(user);
  }
}
```

Mutation routes read the body from `context.request.payload()`:

```typescript theme={null}
type UserCreateRouteType = {
  payload: { name: string; email: string; password: string };
  response: { id: string; name: string; email: string };
};

@Route.post("/users", {
  name: "user.create",
  version: 1,
  description: "Create a new user account",
  payload: Assert({ name: "string", email: "string.email", password: "8 <= string <= 100" }),
  response: Assert({ id: "string", name: "string", email: "string" }),
  roles: ["ROLE_ADMIN"],
})
export class UserCreateController {
  constructor(private readonly userService: UserService) {}

  public async index(context: ContextType<UserCreateRouteType>) {
    const data = await context.request.payload();
    const user = await this.userService.execute(data);

    return context.response.json(user);
  }
}
```

The context exposes the authenticated user, logger, and locale directly — read only what the handler needs:

```typescript theme={null}
public async index(context: ContextType<UserCreateRouteType>) {
  const { user, logger, lang } = context;

  logger.info("Creating user", { actor: user?.id, lang: lang.code });

  const data = await context.request.payload();
  return context.response.json(await this.userService.execute(data));
}
```

### Socket controllers

WebSocket controllers share the same one-method shape but are bound with `@Route.socket` and use `ContextType` from `@ooneex/socket`, which adds a `channel` API (`channel.send`, `channel.publish`, `channel.subscribe`, `channel.close`, `channel.ws`). The CLI scaffolds either variant — pass `--is-socket=true` for the socket template.

## Best practices

* **Keep controllers thin.** Validate and shape the request, delegate the work to an injected service, and return the response. No business logic in `index`.
* **Define one route type and reuse it.** Pass the same type to the `@Route` config and `ContextType<T>` so params, payload, queries, and response stay in sync.
* **Include only what the route needs.** Add `params` for path segments, `payload` for `post`/`put`/`patch`, `queries` for list/search — always include `response`.
* **Read through the request methods.** Use `context.request.params()`, `.payload()`, and `.queries()` so the decorator's `Assert` schemas validate before your code runs.
* **Pick the least-privileged role.** Set `roles` to the lowest role that satisfies the endpoint; access is hierarchical, so a role also grants the roles it inherits.
* **Return responses, don't construct them.** Use the provided `context.response` builder rather than building raw HTTP responses by hand.
* **Treat optional context as optional.** `cache`, `rateLimiter`, `permission`, and `user` may be absent — guard with `?.` and fall back.

## CLI command

Scaffold a controller, its route type, and a test file with the generator. It writes the class to `modules/<module>/src/controllers/<Name>Controller.ts`, registers it in the module, and installs `@ooneex/controller` if it is missing.

```bash theme={null}
# Interactive: prompts for name, socket, route name/path/method
ooneex controller:create

# HTTP controller with route details
ooneex controller:create --name=UserList --module=user --route-name=user.list --route-path=/users --route-method=get

# Socket controller
ooneex controller:create --name=ChatMessage --module=chat --is-socket=true --route-name=chat.message --route-path=/chat
```

| Option           | Description                                                                                         | Default             |
| ---------------- | --------------------------------------------------------------------------------------------------- | ------------------- |
| `--name`         | Controller class name. Normalized to PascalCase; the `Controller` suffix is appended automatically. | Prompted if omitted |
| `--module`       | Target module the class is generated into.                                                          | `shared`            |
| `--is-socket`    | Generate a WebSocket controller instead of HTTP.                                                    | Prompted if omitted |
| `--route-name`   | Route name in dot notation, e.g. `user.list`.                                                       | Prompted if omitted |
| `--route-path`   | Route path; normalized to kebab-case.                                                               | Prompted if omitted |
| `--route-method` | HTTP method (HTTP controllers only).                                                                | Prompted if omitted |
| `--override`     | Overwrite an existing controller without prompting.                                                 | `false`             |

The generated class is a typed stub — a route type, a `@Route` decorator with empty `Assert` schemas, and an `index` that returns an empty JSON response, ready for you to fill in:

```typescript theme={null}
import type { ContextType } from "@ooneex/controller";
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";

export type UserListRouteType = {
  params: {

  },
  payload: {

  },
  queries: {

  },
  response: {

  },
};

@Route.get("/users", {
  name: "user.list",
  version: 1,
  description: "",
  params: {
    // id: Assert("string"),
  },
  payload: Assert({

  }),
  queries: Assert({

  }),
  response: Assert({

  }),
  roles: ["ROLE_USER"],
})
export class UserListController {
  public async index(context: ContextType<UserListRouteType>) {
    // const { id } = context.params;

    return context.response.json({

    });
  }
}
```

See [controller:create](/cli/commands/controller-create) for the full command reference.

## Use with Claude and Codex

The generator ships a matching `controller:create` skill. It runs the scaffold and then guides your AI agent through completing the controller — filling in the route type, validation schemas, roles, and the `index` handler, and delegating logic to a service. Initialize the skills once for your agent:

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a controller that returns the current user's profile.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    Then ask Codex in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a controller that returns the current user's profile.
    ```
  </Tab>
</Tabs>

For example, the prompt above maps to a `controller:create` run with an inferred name, route, and method, then implements the `index` handler against the user's profile service.
