# REST Controllers

REST controllers handle incoming HTTP requests and return JSON responses -- they are your API endpoints. This is the default transport in Ignis and covers the majority of use cases.

> **Deep Dive:** See [REST Controllers Reference](../../references/base/controllers.md) for the complete API.

## Creating a REST Controller

Extend `BaseRestController` and use decorators to define routes:

```typescript
import { BaseRestController, controller, get, jsonResponse, z, TRouteContext } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';

@controller({ path: '/users' })
export class UserController extends BaseRestController {
  constructor() {
    // scope is used for logging; path can be omitted if @controller provides it
    super({ scope: UserController.name });
  }

  @get({
    configs: {
      path: '/',
      responses: jsonResponse({
        description: 'A list of users',
        schema: z.array(z.object({ id: z.string(), name: z.string() })),
      }),
    },
  })
  getAllUsers(c: TRouteContext) {
    return c.json([{ id: '1', name: 'John Doe' }], HTTP.ResultCodes.RS_2.Ok);
  }
}
```

Notice that the `binding()` method is no longer needed when using decorators. The path is resolved from the `@controller` decorator metadata first, then falls back to the constructor `path` option.

## Controller Lifecycle

Controllers have a simple and predictable lifecycle managed by the application.

| Stage | Method | Description |
| :--- | :--- | :--- |
| **1. Instantiation** | `constructor(opts)` | The controller is created by the DI container. Dependencies are injected, and `super()` initializes the internal `OpenAPIHono` router. The `path` is resolved from `@controller` decorator metadata, falling back to the constructor option. |
| **2. Configuration**| `configure()` | The application calls `configure()` which invokes `binding()` (for manual routes) and `registerRoutesFromRegistry()` (for decorator routes). This method is **idempotent** -- calling it multiple times has no effect after the first call. |

## Defining Routes with Decorators (Recommended)

The recommended way to define routes is by using decorators directly on the controller methods that handle them. This approach is more declarative, keeps your code organized, and provides **full type safety** for your request parameters, query, body, and even the response.

:::tip Type Safety without Boilerplate
For decorator-based routes, you do not need to explicitly annotate the return type with `TRouteResponse`. TypeScript will automatically infer and validate the return type against the OpenAPI response schema you define in your `configs`. This gives you full type safety with less code.
:::

### HTTP Method Decorators

`Ignis` provides a decorator for each common HTTP method:

-   `@get(opts)`
-   `@post(opts)`
-   `@put(opts)`
-   `@patch(opts)`
-   `@del(opts)`
-   `@api(opts)` (a generic decorator where you specify the method in the `configs`)

The `opts` object contains a `configs` property that defines the route's path, request validation, and OpenAPI response schemas.

**Example using decorators with `ROUTE_CONFIGS` and full type inference:**

For optimal organization and type safety, define your route configurations in a constant with `as const`. This allows TypeScript to precisely infer the types for your request data and expected responses within your handler methods.

```typescript
import { BaseRestController, controller, get, post, jsonContent, jsonResponse, TRouteContext } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';
import { z } from '@hono/zod-openapi';

// Define route configs as const for type inference
const TestRoutes = {
  GET_DATA: {
    path: '/',
    responses: jsonResponse({
      description: 'A simple message',
      schema: z.object({ message: z.string(), method: z.string() }),
    }),
  },
  CREATE_ITEM: {
    path: '/',
    request: {
      body: jsonContent({
        description: 'Request body for creating an item',
        schema: z.object({ name: z.string(), value: z.number().int().positive() }),
      }),
    },
    responses: jsonResponse({
      description: 'Created item',
      schema: z.object({ id: z.string(), name: z.string(), value: z.number() }),
    }),
  },
} as const; // Crucial for strict type inference!

@controller({ path: '/my-items' })
export class MyItemsController extends BaseRestController {
  constructor() {
    super({ scope: MyItemsController.name });
  }

  @get({ configs: TestRoutes.GET_DATA })
  getData(c: TRouteContext) {
    // 'c' is fully typed here, including c.req.valid and c.json return type
    return c.json({ message: 'Hello from decorator', method: 'GET' }, HTTP.ResultCodes.RS_2.Ok);
  }

  @post({ configs: TestRoutes.CREATE_ITEM })
  createItem(c: TRouteContext) {
    // c.req.valid('json') is automatically typed based on CREATE_ITEM.request.body.content['application/json']['schema']
    // You can also provide an explicit type for stricter checking:
    const body = c.req.valid<{ name: string; value: number }>('json');

    // Return type is automatically validated against CREATE_ITEM.responses[200].content['application/json']['schema']
    return c.json(
      {
        id: 'some-uuid',
        name: body.name,
        value: body.value,
      },
      HTTP.ResultCodes.RS_2.Ok,
    );
  }
}
```

## Manual Route Definition: An Alternative Approach

While decorators are the recommended approach for most use cases, `Ignis` also provides a manual way to define routes within the controller's `binding()` method.

### Decorator vs Manual: Quick Comparison

| Aspect | Decorators (`@get`, `@post`) | Manual (`defineRoute`, `bindRoute`) |
| :--- | :--- | :--- |
| **Syntax** | Clean, declarative | More verbose |
| **Organization** | Route config near the handler | Can be in separate files |
| **Use Case** | Most standard CRUD endpoints | Dynamic route generation |
| **Type Safety** | Full inference with `TRouteContext` | Full inference with `TRouteContext` |
| **Readability** | High - easy to scan | Medium - requires scrolling |
| **Best For** | 90% of use cases | Advanced scenarios |

### When to Use Manual Route Definition

Manual route definition is useful for:

-   **Dynamically generating routes** based on configuration (e.g., loading routes from a database)
-   **Organizing all routes** for a controller within a single method if you prefer centralized route declarations
-   **Conditional route registration** (e.g., enabling/disabling routes based on feature flags)
-   **Developers who prefer non-decorator syntax** (coming from Express/Fastify)

When using this method, you will override the `binding()` method in your controller and use `this.defineRoute` or `this.bindRoute` to register your endpoints.

### `defineRoute`

Use this method for defining a single API endpoint with all its configurations and handler. It also benefits from type inference when used with `TRouteContext`.

```typescript
import { Authentication, jsonResponse, z, TRouteContext } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';

// ... inside the binding() method

const GetUsersRoute = {
  path: '/',
  method: 'get',
  authenticate: { strategies: [Authentication.STRATEGY_JWT] },
  responses: jsonResponse({
    description: 'List of all users',
    schema: z.array(z.object({ id: z.number(), name: z.string() })),
  }),
} as const;

this.defineRoute({
  configs: GetUsersRoute,
  handler: (c: TRouteContext) => {
    return c.json([{ id: 1, name: 'John Doe' }], HTTP.ResultCodes.RS_2.Ok);
  },
});
```

### `bindRoute`

This method offers a fluent API for defining routes, similar to `defineRoute`, but structured for chaining. It also benefits from `TRouteContext` for type safety.

```typescript
import { jsonResponse, z, TRouteContext } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';

// ... inside the binding() method

const GetUserByIdRoute = {
  path: '/{id}',
  method: 'get',
  responses: jsonResponse({
    description: 'A single user',
    schema: z.object({ id: z.string(), name: z.string() }),
  }),
} as const;

this.bindRoute({
  configs: GetUserByIdRoute,
}).to({
  handler: (c: TRouteContext) => {
    const { id } = c.req.valid<{ id: string }>('param');  // Use valid<T>() for type-safe validated params
    return c.json({ id: id, name: 'John Doe' }, HTTP.ResultCodes.RS_2.Ok);
  },
});
```

## `ControllerFactory` for CRUD Operations

For standard CRUD (Create, Read, Update, Delete) operations, `Ignis` provides a `ControllerFactory` that can generate a full-featured controller for any given entity. This significantly reduces boilerplate code.

```typescript
// src/controllers/configuration/configuration.controller.ts (Example from @examples/vert)
import { Configuration } from '@/models';
import { ConfigurationRepository } from '@/repositories';
import {
  BindingKeys,
  BindingNamespaces,
  controller,
  ControllerFactory,
  inject,
} from '@venizia/ignis';

const BASE_PATH = '/configurations';

const _Controller = ControllerFactory.defineCrudController({
  repository: { name: ConfigurationRepository.name },
  controller: {
    name: 'ConfigurationController',
    basePath: BASE_PATH,
    isStrict: { path: true, requestSchema: true },
  },
  entity: () => Configuration, // Provide a resolver for your entity class
});

@controller({ path: BASE_PATH })
export class ConfigurationController extends _Controller {
  constructor(
    @inject({
      key: BindingKeys.build({
        namespace: BindingNamespaces.REPOSITORY,
        key: ConfigurationRepository.name,
      }),
    })
    repository: ConfigurationRepository,
  ) {
    super(repository); // The generated controller expects the repository in its constructor
  }
}
```
The `ControllerFactory.defineCrudController` method automatically sets up the following routes based on your entity schema:

| Route Name | Method | Path | Description |
| :--- | :--- | :--- | :--- |
| `count` | `GET` | `/count` | Get the number of records matching a filter. |
| `find` | `GET` | `/` | Retrieve all records matching a filter. |
| `findById` | `GET` | `/{id}` | Retrieve a single record by its ID. |
| `findOne` | `GET` | `/find-one` | Retrieve a single record matching a filter. |
| `create` | `POST` | `/` | Create a new record. |
| `updateById` | `PATCH` | `/{id}` | Update a single record by its ID. |
| `updateBy` | `PATCH` | `/` | Update multiple records matching a `where` filter. |
| `deleteById` | `DELETE` | `/{id}` | Delete a single record by its ID. |
| `deleteBy` | `DELETE` | `/` | Delete multiple records matching a `where` filter. |

:::info Customization
The `ControllerFactory` is highly customizable. You can override the Zod schemas for any of the generated routes to add, remove, or modify fields for request validation and response shapes. You can also configure other behaviors, like making delete operations return the deleted records.

For a full list of customization options, see the [**Deep Dive: `ControllerFactory`**](../../references/base/controllers.md#controllerfactory) documentation.
:::

### `defineJSXRoute` (Server-Side Rendered HTML)

Use this method for routes that render HTML pages using JSX components. This is perfect for building server-side rendered web applications.

```typescript
import { htmlResponse } from '@venizia/ignis';

// ... inside the binding() method

this.defineJSXRoute({
  configs: {
    path: '/',
    method: 'get',
    description: 'Home page',
    responses: htmlResponse({
      description: 'HTML home page',
    }),
  },
  handler: (c) => {
    const user = { name: 'John Doe' };
    return c.html(<HomePage user={user} />);
  },
});
```

**Key Points:**
- The handler **must** return `c.html()` with a JSX component
- JSX routes automatically include HTML content-type in OpenAPI documentation
- Views are typically organized in `src/views/` directory
- Components can be reused across multiple pages

**Example View Component:**

```typescript
// src/views/pages/home.page.tsx
import type { FC } from '@venizia/ignis-helpers';
import { MainLayout } from '../layouts/main.layout';

interface HomePageProps {
  user: { name: string };
}

export const HomePage: FC<HomePageProps> = ({ user }) => {
  return (
    <MainLayout title="Home">
      <h1>Welcome, {user.name}!</h1>
      <p>This page is rendered on the server using JSX.</p>
    </MainLayout>
  );
};
```

**Example Layout Component:**

```typescript
// src/views/layouts/main.layout.tsx
import type { FC, PropsWithChildren } from '@venizia/ignis-helpers';

interface MainLayoutProps {
  title: string;
}

export const MainLayout: FC<PropsWithChildren<MainLayoutProps>> = ({ title, children }) => {
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{title}</title>
      </head>
      <body>
        <header>
          <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>&copy; 2025 My App</p>
        </footer>
      </body>
    </html>
  );
};
```

> **Note:** JSX support in `Ignis` uses Hono's built-in JSX runtime. Make sure your `tsconfig.json` includes the JSX configuration (this is already set up in the framework's base configuration).

## Accessing Validated Request Data

When you define Zod schemas in your route's `request` configuration (whether with decorators or manual definition), Hono's validation middleware automatically parses and validates the incoming data. You can access this validated data using `c.req.valid()`.

```typescript
import { z } from '@hono/zod-openapi';
import { jsonContent, put, TRouteContext } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';

// ... inside a controller class

const UserSchema = z.object({ name: z.string(), email: z.string().email() });

@put({
  configs: {
    path: '/{id}',
    request: {
      params: z.object({ id: z.string() }),
      query: z.object({ notify: z.string().optional() }),
      body: jsonContent({ schema: UserSchema }),
    },
    // ... responses
  },
})
updateUser(c: TRouteContext) {
  // Access validated data from the request
  const { id } = c.req.valid('param');
  const { notify } = c.req.valid('query');
  const userUpdateData = c.req.valid('json'); // for body

  console.log(`Updating user ${id} with data:`, userUpdateData);
  if (notify) {
    console.log('Notification is enabled.');
  }

  return c.json({ success: true, id, ...userUpdateData }, HTTP.ResultCodes.RS_2.Ok);
}
```

Using `c.req.valid()` is the recommended way to access request data as it ensures that the data conforms to the schema you've defined. For even better type safety, you can use the `TRouteContext` utility type.

```typescript
import { z } from '@hono/zod-openapi';
import { jsonContent, put, TRouteContext } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';

// ... inside a controller class

const UpdateUserConfig = {
  path: '/{id}',
  request: {
    params: z.object({ id: z.string() }),
    query: z.object({ notify: z.string().optional() }),
    body: jsonContent({
      schema: z.object({ name: z.string(), email: z.string().email() }),
    }),
  },
  // ... responses
} as const; // Use 'as const' for strict type inference

@put({ configs: UpdateUserConfig })
updateUser(c: TRouteContext) {
  // Access validated data from the request with explicit types
  const { id } = c.req.valid<{ id: string }>('param');
  const { notify } = c.req.valid<{ notify?: string }>('query');
  const userUpdateData = c.req.valid<{ name: string; email: string }>('json'); // for body

  console.log(`Updating user ${id} with data:`, userUpdateData);
  if (notify) {
    console.log('Notification is enabled.');
  }

  return c.json({ success: true, id, ...userUpdateData }, HTTP.ResultCodes.RS_2.Ok);
}
```

Using `TRouteContext` provides a typed context object. By using `c.req.valid<T>()`, you ensure that the data you are accessing matches your expectations and provides autocomplete in your editor.

## See Also

- **Related Concepts:**
  - [Application](/guides/core-concepts/application/) - Registering controllers
  - [Services](/guides/core-concepts/services) - Business logic layer called by controllers
  - [Dependency Injection](/guides/core-concepts/dependency-injection) - Injecting services into controllers
  - [gRPC Controllers](/guides/core-concepts/grpc-controllers) - RPC-based controllers

- **References:**
  - [BaseRestController API](/references/base/controllers) - Complete REST controller API reference
  - [Middlewares](/references/base/middlewares) - Request interceptors
  - [Swagger Component](/extensions/components/swagger) - Auto-generate API docs
  - [Schema Utilities](/references/utilities/schema) - Request/response helpers

- **Tutorials:**
  - [Building a CRUD API](/guides/tutorials/building-a-crud-api) - Controller examples
  - [E-commerce API](/guides/tutorials/ecommerce-api) - Advanced routing patterns
