Saltearse al contenido

Guía de Decoradores

Guía completa para usar decoradores en Veloce.

Veloce usa decoradores de TypeScript para proporcionar una API limpia y declarativa para definir rutas, validación y dependencias. Los decoradores son funciones especiales que pueden modificar clases, métodos y parámetros en tiempo de diseño.

Para usar decoradores, habilítalos en tu tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

Marca una clase como controlador y opcionalmente establece un prefijo de ruta para todas las rutas.

@Controller('/api/users')
class UserController {
@Get('/')
async list() {
// Ruta: GET /api/users/
return [];
}
@Get('/:id')
async get(@Param('id') id: string) {
// Ruta: GET /api/users/:id
return { id };
}
}

Sin prefijo:

@Controller()
class RootController {
@Get('/health')
async health() {
// Ruta: GET /health
return { status: 'ok' };
}
}

Marca una clase como manejador de WebSocket.

@WebSocket('/chat')
class ChatHandler {
@OnConnect()
handleConnect(client: WebSocketConnection) {
console.log('Cliente conectado');
}
@OnMessage()
handleMessage(client: WebSocketConnection, message: any) {
client.broadcast(message);
}
}

Marca una clase como resolver de GraphQL.

@Resolver()
class UserResolver {
@Query()
async users() {
return [];
}
@Mutation()
async createUser(@Arg('input', UserInput) input: any) {
return { id: 1, ...input };
}
}
@Controller('/users')
class UserController {
@Get()
async list() {
// GET /users
return [];
}
@Get('/:id')
async get(@Param('id') id: string) {
// GET /users/:id
return { id };
}
@Get('/search')
async search(@Query() query: any) {
// GET /users/search?q=...
return [];
}
}
@Post()
async create(@Body(UserSchema) user: User) {
// POST /users
return user;
}
@Post('/bulk')
async createMany(@Body(z.array(UserSchema)) users: User[]) {
// POST /users/bulk
return users;
}
@Put('/:id')
async update(
@Param('id') id: string,
@Body(UserSchema) user: User
) {
// PUT /users/:id
return { id, ...user };
}
@Delete('/:id')
async delete(@Param('id') id: string) {
// DELETE /users/:id
return { success: true };
}
@Patch('/:id')
async patch(
@Param('id') id: string,
@Body(PartialUserSchema) data: Partial<User>
) {
// PATCH /users/:id
return { id, ...data };
}

Responde a todos los métodos HTTP.

@All('/webhook')
async webhook() {
// Maneja GET, POST, PUT, DELETE, etc.
return { received: true };
}

Estos decoradores ofrecen una forma concisa de documentar rutas en el spec de OpenAPI, como alternativa a @ApiDoc({...}).

Establece una descripción corta visible en la lista de rutas de Swagger UI.

@Get('/productos')
@Summary('Listar todos los productos')
async listarProductos() {
return productos;
}

Establece una descripción larga mostrada en el panel de detalle de la operación.

@Get('/productos')
@Summary('Listar todos los productos')
@Description('Devuelve una lista paginada de productos activos. Soporta filtrado por categoría y rango de precio.')
async listarProductos() {
return productos;
}

Asigna un tag individual de OpenAPI a la ruta. Se puede apilar varias veces.

@Get('/productos')
@Tag('Productos')
@Tag('Catálogo')
async listarProductos() {
return productos;
}

Asigna múltiples tags en un solo decorador.

@Get('/productos')
@Tags('Productos', 'Catálogo', 'Público')
async listarProductos() {
return productos;
}

Marca la ruta como obsoleta. Aparecerá tachada en Swagger UI.

@Delete('/productos/:id')
@Deprecated()
@Summary('Eliminar producto (usa PATCH /productos/:id/archivar en su lugar)')
async eliminarProducto(@Param('id') id: string) {
return { success: true };
}

:::note Auto-tagging Si no añades @Tag o @Tags, el generador OpenAPI deriva automáticamente los tags del primer segmento del path. Por ejemplo, /productos/:id recibe el tag "Productos". :::


Sobreescribe el código HTTP de la respuesta. También informa al generador OpenAPI del código de éxito documentado.

El orden de los decoradores importa. TypeScript ejecuta los decoradores de método de abajo hacia arriba. @HttpCode debe colocarse por encima del decorador HTTP (@Post, @Get, etc.) para que se ejecute después de que la ruta ya esté definida y pueda sobrescribir correctamente el código de estado.

@Post('/productos')
@HttpCode(201)
async crearProducto(@Body(CreateProductSchema) data: any) {
return await db.create(data);
}
@Delete('/productos/:id')
@HttpCode(204)
async eliminarProducto(@Param('id') id: string) {
await db.delete(id);
}

@ResponseSchema(schema: ZodSchema, statusCode?: number)

Sección titulada «@ResponseSchema(schema: ZodSchema, statusCode?: number)»

Valida y sanitiza la respuesta del handler con un esquema Zod. También informa el modelo de respuesta en el spec de OpenAPI.

const ProductoSchema = z.object({
id: z.number(),
nombre: z.string(),
precio: z.number(),
});
@Get('/productos/:id')
@ResponseSchema(ProductoSchema)
async obtenerProducto(@Param('id') id: string) {
return await db.findById(id);
}
@Post('/productos')
@HttpCode(201)
@ResponseSchema(ProductoSchema, 201)
async crearProducto(@Body(CreateProductSchema) data: any) {
return await db.create(data);
}

Aborta la petición con 408 Request Timeout si el handler supera el límite de tiempo. Inyecta automáticamente el middleware y emite el header X-Timeout-Ms.

@Get('/reporte')
@Timeout(5000)
async generarReporte() {
return await reportePesado();
}
@Post('/sync')
@Timeout(30000, 'Operación de sincronización agotada. Inténtalo de nuevo.')
async sincronizarDatos() {
return await syncExterno();
}

Aplica rate-limiting a una ruta individual. Los headers X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset se envían automáticamente.

@Post('/auth/login')
@RateLimit({ windowMs: 15 * 60_000, max: 10 })
async login(@Body(LoginSchema) credenciales: any) {
return await authService.login(credenciales);
}
@Post('/contacto')
@RateLimit({ windowMs: 60_000, max: 3, message: 'Demasiados mensajes. Espera 1 minuto.' })
async enviarMensaje(@Body(MensajeSchema) data: any) {
return await mailer.send(data);
}

Extrae y valida el cuerpo de la petición.

const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional(),
});
@Post('/users')
async createUser(@Body(UserSchema) user: z.infer<typeof UserSchema>) {
// user está validado y tipado
return user;
}

Extrae y valida parámetros de consulta.

const SearchSchema = z.object({
q: z.string().min(1),
page: z.string().transform(Number).default('1'),
limit: z.string().transform(Number).default('10'),
});
@Get('/search')
async search(@Query(SearchSchema) query: z.infer<typeof SearchSchema>) {
return {
query: query.q,
page: query.page,
limit: query.limit,
};
}

Extrae parámetros de ruta.

@Get('/users/:id')
async getUser(@Param('id') id: string) {
return { id };
}
@Get('/users/:userId/posts/:postId')
async getPost(
@Param('userId') userId: string,
@Param('postId') postId: string
) {
return { userId, postId };
}

Extrae y valida encabezados.

@Get('/protected')
async getProtected(@Header('authorization') auth: string) {
return { auth };
}

Extrae y valida cookies.

@Get('/profile')
async getProfile(@Cookie('session') sessionId: string) {
return { sessionId };
}

Inyecta el contexto de Hono (c). Úsalo para c.redirect(), c.req.json(), cabeceras o el Request estándar de la Web API.

import type { Context } from 'hono';
@Get('/redirect-example')
redirect(@Ctx() c: Context) {
return c.redirect('https://example.com', 302);
}
@Post('/echo-raw')
async echo(@Ctx() c: Context) {
const body = await c.req.json();
return body;
}

Para el objeto Request estándar (Fetch API), usa el contexto y c.req.raw:

@Get('/ip')
ip(@Ctx() c: Context) {
const req = c.req.raw;
return { url: req.url };
}

Inyecta el wrapper de petición de Hono (c.req en el router). Se exporta desde veloce-ts como @Req. Si necesitas el Request Web plano, usa @Ctx() y c.req.raw.

@Get('/path')
path(@Req() req: import('hono').HonoRequest) {
return { path: req.path };
}

Inyecta dependencias.

class DatabaseService {
async getUsers() {
return [];
}
}
@Controller('/users')
class UserController {
@Get('/')
async list(@Depends(DatabaseService) db: DatabaseService) {
return await db.getUsers();
}
}
@Post('/users/:id/posts')
async createPost(
@Param('id') userId: string,
@Body(PostSchema) post: Post,
@Header('authorization') auth: string,
@Depends(DatabaseService) db: DatabaseService
) {
// Todos los parámetros son extraídos y validados
return await db.createPost(userId, post);
}
// ✓ Bueno
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
@Post('/users')
async createUser(@Body(UserSchema) user: z.infer<typeof UserSchema>) {
return user;
}
// ✗ Malo
@Post('/users')
async createUser(@Body() user: any) {
return user;
}
// ✓ Bueno
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
@Post('/users')
async createUser(@Body(UserSchema) user: User) {
// user está completamente tipado
}
// ✓ Bueno - Un controlador por recurso
@Controller('/users')
class UserController {
// Todas las rutas relacionadas con usuarios
}
@Controller('/posts')
class PostController {
// Todas las rutas relacionadas con posts
}
// ✓ Bueno
class UserService {
async getUsers() {
return [];
}
}
@Controller('/users')
class UserController {
@Get('/')
async list(@Depends(UserService) userService: UserService) {
return await userService.getUsers();
}
}
@Get('/users/:id')
async getUser(@Param('id') id: string) {
const user = await db.getUserById(id);
if (!user) {
throw new HTTPException(404, 'Usuario no encontrado');
}
return user;
}