> ## 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.

# Routing

> Map incoming HTTP and WebSocket requests to controllers with decorator-driven routes, validated parameters, and named URL generation.

Routing is how an Ooneex app decides *which controller handles which request*. You never wire up a route table by hand: you decorate a controller class with a route decorator, and the framework registers the path, method, and validation rules for you. When a request arrives, the router matches it against the registered routes and runs the matched controller's `index(context)` method. A route and its [controller](/basics/controller) are two halves of the same unit — the decorator describes the *contract* (path, method, what input is valid, who is allowed in), and the controller supplies the *behavior*.

Because every route is declared on the class that serves it, the route definition lives next to the code that runs, and the same configuration object drives parameter parsing, validation, access control, and named URL generation.

## Why this matters

* **Declarative, co-located routes.** The route lives on the controller it serves — no central route file drifting out of sync with handlers.
* **Validation at the boundary.** Path params, query strings, and request bodies are validated with [`@ooneex/validation`](/basics/validation) schemas before your controller runs, so `index` receives typed, trusted data.
* **Access control in the contract.** Roles, permission classes, and env/ip/host restrictions are part of the route definition, so who-can-reach-this is visible where the route is declared.
* **Named routes, not hardcoded URLs.** Every route has a name; generate URLs from the name with `router.generate(...)` instead of stringly-typed paths scattered across the codebase.
* **HTTP and WebSocket together.** The same decorator family declares both, so socket endpoints follow the same naming and validation conventions as HTTP ones.

## How it works

The router holds a map of paths to route configurations. Each `Route.*` decorator builds a route config from the path and your options, marks the HTTP method (or `isSocket: true` for sockets), attaches the controller class, and registers it with the container as a singleton. At request time the framework matches the path and method, validates the input against the route's schemas, enforces any access rules, and invokes the matched controller's `index(context)`.

| Stage          | What happens                                                                                                                                |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Declaration    | A `@Route.get(...)` / `@Route.post(...)` / `@Route.socket(...)` decorator registers the controller class with its path, method, and config. |
| Matching       | An incoming request is matched by path and HTTP method (sockets match by path only).                                                        |
| Validation     | `params`, `queries`, and `payload` schemas run; invalid input is rejected before the controller.                                            |
| Access control | `roles`, `permission`, `env`, `ip`, and `host` are checked; disallowed requests are rejected.                                               |
| Handling       | The matched controller's `index(context)` runs with `context.params`, `context.queries`, and `context.payload` populated.                   |

The decorator takes the **path as its first argument** and the **config object as its second**:

```typescript theme={null}
Route.get("/api/users/:id", {
  name: "api.users.show",
  version: 1,
  description: "Get a user by id",
});
```

## Defining a route

Import `Route` and decorate a controller class. The method decorator name maps to the HTTP method: `Route.get`, `Route.post`, `Route.put`, `Route.delete`, `Route.patch`, `Route.options`, and `Route.head`. The controller implements `IController` and exposes an `index(context)` method that returns a [response](/basics/response).

```typescript theme={null}
import { Route } from "@ooneex/routing";
import type { ContextType, IController } from "@ooneex/controller";
import type { IResponse } from "@ooneex/http-response";

@Route.get("/api/users", {
  name: "api.users.list",
  version: 1,
  description: "List all users",
})
export class UserListController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    return context.response.json({
      users: [{ id: 1, name: "John" }],
    });
  }
}
```

### Configuration options

Every option except `path` and `method` (which come from the decorator) is passed in the config object.

| Field         | Type                         | Description                                                                                              |
| ------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------- |
| `name`        | `string`                     | Unique route name in `namespace.resource.action` form. Used for lookup and URL generation. **Required.** |
| `version`     | `number`                     | Numeric API version for the route. **Required.**                                                         |
| `description` | `string`                     | Human-readable description of the route. **Required.**                                                   |
| `params`      | `Record<string, AssertType>` | Schema per path parameter (the `:id` segments). Validates `context.params`.                              |
| `queries`     | `AssertType`                 | Schema for the query string. Validates `context.queries`.                                                |
| `payload`     | `AssertType`                 | Schema for the request body. Validates `context.payload`.                                                |
| `response`    | `AssertType`                 | Schema describing the response shape (used for documentation and codegen).                               |
| `roles`       | `Uppercase<string>[]`        | Roles allowed to access the route.                                                                       |
| `permission`  | `PermissionClassType`        | A custom permission class evaluated per request.                                                         |
| `featureFlag` | `FeatureFlagClassType`       | A feature-flag class gating the route.                                                                   |
| `env`         | `EnvironmentNameType[]`      | Environments in which the route is available.                                                            |
| `ip`          | `string[]`                   | Allowed IP addresses or CIDR ranges.                                                                     |
| `host`        | `string[]`                   | Allowed hostnames.                                                                                       |
| `cache`       | `string`                     | Cache key prefix that enables response caching for the route.                                            |

<Note>
  The decorator supplies `path`, `method`, `isSocket`, and `controller`
  automatically — you never set those in the config object.
</Note>

## Route naming

Every route has a unique `name` following the `namespace.resource.action` convention, for example `api.users.list` or `admin.products.create`. Names must be unique across the whole app; registering two routes with the same name throws a `RouterException`. The name is what you pass to `router.generate(...)` and what the router uses for lookups.

Valid namespaces:

`api`, `client`, `admin`, `public`, `auth`, `webhook`, `internal`, `external`, `system`, `metrics`, `docs`

The middle `resource` segment names the thing the route acts on (`users`, `products`, `users.orders`), and the final `action` segment names the operation (`list`, `show`, `create`, `update`, `delete`, `search`, and many more).

## Path parameters

Dynamic path segments are written with a leading colon, like `:id`. Each declared parameter should have a schema in `params`; after validation the values are available on `context.params`.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";
import type { ContextType, IController } from "@ooneex/controller";
import type { IResponse } from "@ooneex/http-response";

@Route.get("/api/users/:id", {
  name: "api.users.show",
  version: 1,
  description: "Get a user by id",
  params: {
    id: Assert("string.uuid"),
  },
})
export class UserShowController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    const { id } = context.params;
    const user = await this.userRepository.findById(id);

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

A path may declare several parameters — `/api/users/:userId/orders/:orderId` — and each is matched into `context.params` by name.

## Query validation

Provide a `queries` schema to validate and coerce the query string. The schema handles defaults and optional keys, so the controller reads ready-to-use values from `context.queries`.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";
import type { ContextType, IController } from "@ooneex/controller";
import type { IResponse } from "@ooneex/http-response";

@Route.get("/api/products", {
  name: "api.products.search",
  version: 1,
  description: "Search products",
  queries: Assert({
    q: "string",
    page: "number = 1",
    limit: "number = 10",
    "category?": "string",
  }),
})
export class ProductSearchController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    const { q, page, limit, category } = context.queries;
    const products = await this.search(q, { page, limit, category });

    return context.response.json({ products });
  }
}
```

## Payload validation

For routes that accept a request body, declare a `payload` schema. The body is validated before `index` runs, and the parsed value is on `context.payload`.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";
import type { ContextType, IController } from "@ooneex/controller";
import type { IResponse } from "@ooneex/http-response";

@Route.post("/api/users", {
  name: "api.users.create",
  version: 1,
  description: "Create a new user",
  payload: Assert({
    email: "string.email",
    name: "string >= 2",
    password: "string >= 8",
  }),
})
export class UserCreateController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    const { email, name, password } = context.payload;
    const user = await this.userService.create({ email, name, password });

    return context.response.json({ user }, 201);
  }
}
```

See [validation](/basics/validation) for the full schema syntax, and [response](/basics/response) for the response builder used in these handlers.

## Response validation

Declare a `response` schema to describe the shape `index` returns. Unlike `params`, `queries`, and `payload`, the response schema is not a runtime gate on the request — it documents the route's output contract and drives type derivation and codegen, so generated clients and API docs stay in sync with what the controller actually sends back.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";
import type { ContextType, IController } from "@ooneex/controller";
import type { IResponse } from "@ooneex/http-response";

@Route.get("/api/users/:id", {
  name: "api.users.show",
  version: 1,
  description: "Get a user by id",
  params: {
    id: Assert("string.uuid"),
  },
  response: Assert({
    user: {
      id: "string.uuid",
      email: "string.email",
      name: "string",
    },
  }),
})
export class UserShowController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    const user = await this.userRepository.findById(context.params.id);

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

Because the schema is the same [`@ooneex/validation`](/basics/validation) construct used for input, the response shape lives in the route contract alongside its inputs — describe it once on the decorator and the generated types and documentation follow.

## WebSocket routes

`Route.socket(path, config)` declares a WebSocket endpoint. It takes the same config as the HTTP decorators (the `method` is ignored and `isSocket` is set to `true`), but the controller's context comes from `@ooneex/socket` and exposes a `channel` for subscribing and publishing.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import { Assert } from "@ooneex/validation";
import type { ContextType, IController } from "@ooneex/socket";
import type { IResponse } from "@ooneex/http-response";

@Route.socket("/ws/chat/:roomId", {
  name: "api.chat.connect",
  version: 1,
  description: "Connect to a chat room",
  params: {
    roomId: Assert("string"),
  },
})
export class ChatController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    const { roomId } = context.params;
    await context.channel.subscribe();

    return context.response.json({ connected: true, room: roomId });
  }
}
```

## Generating URLs

Look routes up by name and build their URL with `router.generate(name, params)`. Path parameters are interpolated from the `params` object; missing required parameters throw a `RouterException`.

```typescript theme={null}
import { router } from "@ooneex/routing";

const userUrl = router.generate("api.users.show", { id: "123" });
// "/api/users/123"

const orderUrl = router.generate("api.users.orders.show", {
  userId: "123",
  orderId: "456",
});
// "/api/users/123/orders/456"
```

Generating from names keeps URLs in one place: rename a path in its decorator and every call site that uses the route name keeps working.

## Access control

Routes can declare who is allowed to reach them. These checks run before the controller, so an unauthorized request never reaches `index`.

**Role-based access** — list the roles permitted to call the route:

```typescript theme={null}
@Route.delete("/admin/users/:id", {
  name: "admin.users.delete",
  version: 1,
  description: "Delete a user (admin only)",
  roles: ["ROLE_ADMIN", "ROLE_SUPER_ADMIN"],
})
export class UserDeleteController implements IController {
  public async index(context: ContextType): Promise<IResponse> {
    await this.userService.delete(context.params.id);
    return context.response.json({ deleted: true });
  }
}
```

**Permission classes** — for logic beyond a fixed role list, point `permission` at a permission class that decides per request:

```typescript theme={null}
@Route.put("/api/users/:id", {
  name: "api.users.update",
  version: 1,
  description: "Update a user profile",
  permission: CanEditOwnProfile,
})
export class UserUpdateController implements IController {
  // only the owner may update — the permission class enforces it
}
```

**Environment, IP, and host restrictions** — limit a route to certain environments or network origins:

```typescript theme={null}
@Route.get("/internal/metrics", {
  name: "internal.metrics.show",
  version: 1,
  description: "Internal metrics endpoint",
  env: ["local", "development"],
  ip: ["127.0.0.1", "10.0.0.0/8"],
  host: ["metrics.internal"],
})
export class MetricsController implements IController {
  // reachable only from the listed envs, IP ranges, and hosts
}
```

Access checks compose with the request [middleware](/basics/middleware) pipeline: middleware runs first and may short-circuit, then the route's own access rules apply, then the controller runs.

## Best practices

* **Name every route consistently.** Follow `namespace.resource.action` so names are predictable and `router.generate` calls read clearly.
* **Validate at the boundary.** Declare `params`, `queries`, and `payload` schemas so `index` works with trusted, typed data instead of re-checking input by hand.
* **Generate URLs, don't hardcode them.** Use `router.generate(name, params)` everywhere a route URL is needed.
* **Keep one route per controller.** A controller's `index` serves a single route; split distinct operations into distinct controllers.
* **Declare access in the route.** Put `roles`, `permission`, and env/ip/host rules on the route so the contract states who may reach it.
* **Version your API routes.** Set `version` deliberately so clients and generated docs track route revisions.

## Scaffolding

Generate a controller and its route together with the CLI rather than wiring one by hand:

```bash theme={null}
ooneex controller:create
```

See [controller:create](/cli/commands/controller-create) for the full command reference, and the [controller](/basics/controller) page for how the generated `index(context)` method consumes the validated request.
