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 extendOpenAPIRouteand 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 theschemaproperty 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., insrc/infrastructure/web/open-api/server.ts. - Export from index.ts: Re-export controllers from
src/presentation/controllers/<entity>/index.tsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →