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

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

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

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

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

// 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. The HTTP method and path must match your intended REST contract.

// 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:

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.
  • 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, UpdateProfessionalController.ts, and 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →