Guía de Decoradores
Guía de Decoradores
Sección titulada «Guía de Decoradores»Guía completa para usar decoradores en Veloce.
Tabla de Contenidos
Sección titulada «Tabla de Contenidos»- Introducción
- Decoradores de Clase
- Decoradores de Método
- Decoradores de Documentación OpenAPI
- Decoradores de Control de Respuesta
- Decoradores de Middleware por Ruta
- Decoradores de Parámetro
- Combinando Decoradores
- Mejores Prácticas
Introducción
Sección titulada «Introducción»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.
Configuración de TypeScript
Sección titulada «Configuración de TypeScript»Para usar decoradores, habilítalos en tu tsconfig.json:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Decoradores de Clase
Sección titulada «Decoradores de Clase»@Controller(prefix?: string)
Sección titulada «@Controller(prefix?: string)»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' }; }}@WebSocket(path: string)
Sección titulada «@WebSocket(path: string)»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); }}@Resolver()
Sección titulada «@Resolver()»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 }; }}Decoradores de Método
Sección titulada «Decoradores de Método»Decoradores de Métodos HTTP
Sección titulada «Decoradores de Métodos HTTP»@Get(path?: string)
Sección titulada «@Get(path?: string)»@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(path?: string)
Sección titulada «@Post(path?: string)»@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(path?: string)
Sección titulada «@Put(path?: string)»@Put('/:id')async update( @Param('id') id: string, @Body(UserSchema) user: User) { // PUT /users/:id return { id, ...user };}@Delete(path?: string)
Sección titulada «@Delete(path?: string)»@Delete('/:id')async delete(@Param('id') id: string) { // DELETE /users/:id return { success: true };}@Patch(path?: string)
Sección titulada «@Patch(path?: string)»@Patch('/:id')async patch( @Param('id') id: string, @Body(PartialUserSchema) data: Partial<User>) { // PATCH /users/:id return { id, ...data };}@All(path?: string)
Sección titulada «@All(path?: string)»Responde a todos los métodos HTTP.
@All('/webhook')async webhook() { // Maneja GET, POST, PUT, DELETE, etc. return { received: true };}Decoradores de Documentación OpenAPI
Sección titulada «Decoradores de Documentación OpenAPI»Estos decoradores ofrecen una forma concisa de documentar rutas en el spec de OpenAPI, como alternativa a @ApiDoc({...}).
@Summary(text: string)
Sección titulada «@Summary(text: string)»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;}@Description(text: string)
Sección titulada «@Description(text: string)»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;}@Tag(name: string)
Sección titulada «@Tag(name: string)»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;}@Tags(...names: string[])
Sección titulada «@Tags(...names: string[])»Asigna múltiples tags en un solo decorador.
@Get('/productos')@Tags('Productos', 'Catálogo', 'Público')async listarProductos() { return productos;}@Deprecated()
Sección titulada «@Deprecated()»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".
:::
Decoradores de Control de Respuesta
Sección titulada «Decoradores de Control de Respuesta»@HttpCode(statusCode: number)
Sección titulada «@HttpCode(statusCode: number)»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.
@HttpCodedebe 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);}Decoradores de Middleware por Ruta
Sección titulada «Decoradores de Middleware por Ruta»@Timeout(ms: number, message?: string)
Sección titulada «@Timeout(ms: number, message?: string)»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();}@RateLimit(options: RateLimitOptions)
Sección titulada «@RateLimit(options: RateLimitOptions)»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);}Decoradores de Parámetro
Sección titulada «Decoradores de Parámetro»@Body(schema?: ZodSchema)
Sección titulada «@Body(schema?: ZodSchema)»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;}@Query(schema?: ZodSchema)
Sección titulada «@Query(schema?: ZodSchema)»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, };}@Param(name?: string)
Sección titulada «@Param(name?: string)»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 };}@Header(name?: string, schema?: ZodSchema)
Sección titulada «@Header(name?: string, schema?: ZodSchema)»Extrae y valida encabezados.
@Get('/protected')async getProtected(@Header('authorization') auth: string) { return { auth };}@Cookie(name?: string, schema?: ZodSchema)
Sección titulada «@Cookie(name?: string, schema?: ZodSchema)»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 };}@Depends(provider: Provider, scope?: Scope)
Sección titulada «@Depends(provider: Provider, scope?: Scope)»Inyecta dependencias.
class DatabaseService { async getUsers() { return []; }}
@Controller('/users')class UserController { @Get('/') async list(@Depends(DatabaseService) db: DatabaseService) { return await db.getUsers(); }}Combinando Decoradores
Sección titulada «Combinando Decoradores»Múltiples Decoradores de Parámetro
Sección titulada «Múltiples Decoradores de Parámetro»@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);}Mejores Prácticas
Sección titulada «Mejores Prácticas»1. Siempre Usa Esquemas para Validación
Sección titulada «1. Siempre Usa Esquemas para Validación»// ✓ Buenoconst 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;}2. Usa Inferencia de Tipos
Sección titulada «2. Usa Inferencia de Tipos»// ✓ Buenoconst 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}3. Organiza los Controladores Lógicamente
Sección titulada «3. Organiza los Controladores Lógicamente»// ✓ 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}4. Usa Inyección de Dependencias
Sección titulada «4. Usa Inyección de Dependencias»// ✓ Buenoclass UserService { async getUsers() { return []; }}
@Controller('/users')class UserController { @Get('/') async list(@Depends(UserService) userService: UserService) { return await userService.getUsers(); }}5. Maneja Errores Apropiadamente
Sección titulada «5. Maneja Errores Apropiadamente»@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;}Próximos Pasos
Sección titulada «Próximos Pasos»- Aprende sobre Inyección de Dependencias
- Explora Plugins
- Revisa la Guía de Inicio