# How to Add New Controllers in the Castrozan TCC Repository: Complete Pattern Guide

> Learn to add new controllers in the castrozan tcc repository. Follow this guide to extend OpenAPIRoute, define schemas, use @withErrorHandling, and register your route correctly.

- Repository: [Lucas Zanoni⠀⠀⠀⠀⠀ ⠀╱|、 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ (˚ˎ 。7 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ |、˜〵 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ じしˍ,)ノ/tcc](https://github.com/castrozan/tcc)
- Tags: how-to-guide
- Published: 2026-02-27

---

**To add new controllers in the castrozan/tcc repository, extend the `OpenAPIRoute` class from chanfana, define a Zod-validated schema property, apply the `@withErrorHandling` decorator to the handle method, and register the route in [`src/infrastructure/web/open-api/server.ts`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/web/open-api/server.ts) while exporting from the entity's index file.**

The professionals-dummy-app within castrozan/tcc implements a layered, convention-based architecture for all HTTP endpoints. To add new controllers that generate automatic OpenAPI documentation and maintain consistent validation, you must follow a specific pattern across the Presentation, Application, and Infrastructure layers. This guide demonstrates the exact file structure, class signatures, and registration steps required to extend the API correctly.

## Understanding the Layered Architecture

The codebase enforces a strict separation of concerns across four distinct layers. Each layer has a specific responsibility and file location:

- **Presentation – Controllers**: Define OpenAPI routes, request/response schemas, and delegate to use-cases. Located at `src/presentation/controllers/<entity>/<Controller>.ts`. All controllers extend `OpenAPIRoute` and use `@withErrorHandling`.

- **Application – Use-Cases**: Contain business logic working with repository interfaces. Located at `src/application/use-cases/<entity>/<UseCase>.ts`.

- **Infrastructure – Repositories**: Provide concrete data-access implementations. Located at `src/infrastructure/database/repositories/<entity>.ts`.

- **Routing**: Registers controllers with the OpenAPI-Hono wrapper in [`src/infrastructure/web/open-api/server.ts`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/web/open-api/server.ts).

- **Export Index**: Re-exports controllers for clean imports from `src/presentation/controllers/<entity>/index.ts`.

## Step 1: Extend OpenAPIRoute and Define the Schema

Every new controller must extend the `OpenAPIRoute` class imported from **chanfana**. The class requires a `schema` property that declares OpenAPI metadata using **Zod** for runtime validation.

```typescript
import { OpenAPIRoute, Bool } from 'chanfana';
import { z } from 'zod';
import { withErrorHandling } from 'presentation/decorators';

export class GetProfessionalStatsController extends OpenAPIRoute {
    schema = {
        tags: ['Professionals'],
        summary: 'Retrieve aggregated statistics for a professional',
        security: [{ bearerAuth: [] }],
        request: {
            params: z.object({
                id: z.number().min(1, { message: 'ID is required' })
            })
        },
        responses: {
            '200': {
                description: 'Statistics retrieved successfully',
                content: {
                    'application/json': {
                        schema: z.object({
                            success: Bool(),
                            result: z.object({
                                id: z.number(),
                                projectsCount: z.number(),
                                lastActive: z.string()
                            })
                        })
                    }
                }
            },
            '404': {
                description: 'Professional not found',
                content: {
                    'application/json': {
                        schema: z.object({
                            success: Bool(),
                            message: z.string()
                        })
                    }
                }
            }
        }
    };

```

## Step 2: Apply Error Handling with @withErrorHandling

All controllers must use the `@withErrorHandling` decorator implemented in [`src/presentation/decorators/handleErrors.ts`](https://github.com/castrozan/tcc/blob/main/src/presentation/decorators/handleErrors.ts). This decorator catches synchronous and asynchronous errors, transforms `ZodError` into HTTP 400 responses, and guarantees a stable `{ success, message, ... }` response shape.

Apply the decorator to the `handle` method:

```typescript
    @withErrorHandling
    async handle(): Promise<object> {
        const data = await this.getValidatedData<typeof this.schema>();
        const { id } = data.params;

        const useCase = new GetProfessionalStatsUseCase(professionalRepository);
        const stats = await useCase.execute(id);

        return {
            success: true,
            result: stats
        };
    }
}

```

## Step 3: Delegate to Use-Cases

Inside the `handle()` method, instantiate the appropriate use-case from `src/application/use-cases/<entity>/` and forward the validated payload. This keeps the controller thin and focused on request/response orchestration rather than business logic.

The use-case receives a repository instance (typically imported from `src/infrastructure/database/repositories/<entity>`) to perform data operations.

## Step 4: Export from the Index File

Controllers must be re-exported from `src/presentation/controllers/<entity>/index.ts` to provide a tidy import surface for the server.

```typescript
// src/presentation/controllers/professionals/index.ts
export { GetProfessionalStatsController } from './GetProfessionalStatsController';

```

## Step 5: Register the Route in the Server

Register each controller with the OpenAPI-Hono wrapper in [`src/infrastructure/web/open-api/server.ts`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/web/open-api/server.ts). The HTTP method and path must match your intended REST contract.

```typescript
// src/infrastructure/web/open-api/server.ts
openapi.get('/professional/:id/stats', GetProfessionalStatsController);

```

## Complete Implementation Example

Here is the full `GetProfessionalStatsController` implementation located at [`src/presentation/controllers/professionals/GetProfessionalStatsController.ts`](https://github.com/castrozan/tcc/blob/main/src/presentation/controllers/professionals/GetProfessionalStatsController.ts):

```typescript
import { GetProfessionalStatsUseCase } from 'application/use-cases/professionals/GetProfessionalStatsUseCase';
import { Bool, OpenAPIRoute } from 'chanfana';
import professionalRepository from 'infrastructure/database/repositories/professionals';
import { withErrorHandling } from 'presentation/decorators';
import { z } from 'zod';

export class GetProfessionalStatsController extends OpenAPIRoute {
    schema = {
        tags: ['Professionals'],
        summary: 'Retrieve aggregated statistics for a professional',
        security: [{ bearerAuth: [] }],
        request: {
            params: z.object({
                id: z.number().min(1, { message: 'ID is required' })
            })
        },
        responses: {
            '200': {
                description: 'Statistics retrieved successfully',
                content: {
                    'application/json': {
                        schema: z.object({
                            success: Bool(),
                            result: z.object({
                                id: z.number(),
                                projectsCount: z.number(),
                                lastActive: z.string()
                            })
                        })
                    }
                }
            },
            '404': {
                description: 'Professional not found',
                content: {
                    'application/json': {
                        schema: z.object({
                            success: Bool(),
                            message: z.string()
                        })
                    }
                }
            },
            '500': {
                description: 'Server error',
                content: {
                    'application/json': {
                        schema: z.object({
                            success: Bool(),
                            message: z.string()
                        })
                    }
                }
            }
        }
    };

    @withErrorHandling
    async handle(): Promise<object> {
        const data = await this.getValidatedData<typeof this.schema>();
        const { id } = data.params;

        const useCase = new GetProfessionalStatsUseCase(professionalRepository);
        const stats = await useCase.execute(id);

        return {
            success: true,
            result: stats
        };
    }
}

```

## Summary

- **Extend `OpenAPIRoute`**: All controllers must inherit from the chanfana base class to enable automatic OpenAPI generation.
- **Define Zod schemas**: Use `z.object()` in the `schema` property for request validation and response typing.
- **Apply `@withErrorHandling`**: This decorator ensures consistent error formatting and Zod error transformation.
- **Delegate to use-cases**: Controllers instantiate use-cases from `src/application/use-cases/` and pass validated data.
- **Register in server.ts**: Add routes using `openapi.get()`, `openapi.post()`, etc., in [`src/infrastructure/web/open-api/server.ts`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/web/open-api/server.ts).
- **Export from index.ts**: Re-export controllers from `src/presentation/controllers/<entity>/index.ts` for clean imports.

## Frequently Asked Questions

### Where are existing controller examples located?

Existing controller implementations are located in `professionals-dummy-app/src/presentation/controllers/professionals/`. Reference [`CreateProfessionalController.ts`](https://github.com/castrozan/tcc/blob/main/CreateProfessionalController.ts), [`UpdateProfessionalController.ts`](https://github.com/castrozan/tcc/blob/main/UpdateProfessionalController.ts), and [`FindAllProfessionalController.ts`](https://github.com/castrozan/tcc/blob/main/FindAllProfessionalController.ts) for working examples of POST, PUT/PATCH, and GET endpoints respectively.

### What happens if I don't use the @withErrorHandling decorator?

Without the `@withErrorHandling` decorator, errors will not be caught and transformed into the standardized `{ success, message }` format. Zod validation errors will not convert to HTTP 400 responses automatically, and unhandled exceptions may crash the request instead of returning a proper HTTP 500 response with a structured error payload.

### Can I use a different validation library instead of Zod?

No. The `@withErrorHandling` decorator in [`src/presentation/decorators/handleErrors.ts`](https://github.com/castrozan/tcc/blob/main/src/presentation/decorators/handleErrors.ts) specifically checks for `ZodError` instances to generate 400 responses. The `OpenAPIRoute` class from chanfana also expects Zod schemas for automatic OpenAPI documentation generation. Using alternative validation libraries would break error transformation and documentation generation.

### How do I handle authentication in new controllers?

Add the `security` property to your schema object with `[{ bearerAuth: [] }]` to require JWT authentication, as shown in the `GetProfessionalStatsController` example. The bearer token will be validated before your `handle()` method executes, and the route will appear with a lock icon in the generated OpenAPI documentation.