@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 typedindex(context)handler. No base class to extend, no boilerplate. - Typed context.
IController<T>andContextType<T>thread yourparams,payload,queries, andresponsetypes 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.
indexmay return anIResponsedirectly or aPromise<IResponse>— both are supported. - HTTP and WebSocket. The same controller shape handles both; socket controllers use a context with an added
channelAPI.
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 callsindex.
| 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. |
index signature is the whole contract:
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 oneindex 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.
@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"]. |
context.request.params() (validated by the decorator’s params):
context.request.payload():
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
@Routeconfig andContextType<T>so params, payload, queries, and response stay in sync. - Include only what the route needs. Add
paramsfor path segments,payloadforpost/put/patch,queriesfor list/search — always includeresponse. - Read through the request methods. Use
context.request.params(),.payload(), and.queries()so the decorator’sAssertschemas validate before your code runs. - Pick the least-privileged role. Set
rolesto 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.responsebuilder rather than building raw HTTP responses by hand. - Treat optional context as optional.
cache,rateLimiter,permission, andusermay 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 tomodules/<module>/src/controllers/<Name>Controller.ts, registers it in the module, and installs @ooneex/controller if it is missing.
| 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 |
@Route decorator with empty Assert schemas, and an index that returns an empty JSON response, ready for you to fill in:
Use with Claude and Codex
The generator ships a matchingcontroller: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:
- Claude
- Codex
Prompt
controller:create run with an inferred name, route, and method, then implements the index handler against the user’s profile service.