Decorators Guide
Decorators Guide
Section titled “Decorators Guide”Comprehensive guide to using decorators in Veloce.
Table of Contents
Section titled “Table of Contents”- Introduction
- Class Decorators
- Method Decorators
- OpenAPI Documentation Decorators
- Response Control Decorators
- Per-Route Middleware Decorators
- Parameter Decorators
- Combining Decorators
- Best Practices
Introduction
Section titled “Introduction”Veloce uses TypeScript decorators to provide a clean, declarative API for defining routes, validation, and dependencies. Decorators are special functions that can modify classes, methods, and parameters at design time.
TypeScript Configuration
Section titled “TypeScript Configuration”To use decorators, enable them in your tsconfig.json:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Class Decorators
Section titled “Class Decorators”@Controller(prefix?: string)
Section titled “@Controller(prefix?: string)”Marks a class as a controller and optionally sets a path prefix for all routes.
@Controller('/api/users')class UserController { @Get('/') async list() { // Route: GET /api/users/ return []; }
@Get('/:id') async get(@Param('id') id: string) { // Route: GET /api/users/:id return { id }; }}Without prefix:
@Controller()class RootController { @Get('/health') async health() { // Route: GET /health return { status: 'ok' }; }}@WebSocket(path: string)
Section titled “@WebSocket(path: string)”Marks a class as a WebSocket handler.
@WebSocket('/chat')class ChatHandler { @OnConnect() handleConnect(client: WebSocketConnection) { console.log('Client connected'); }
@OnMessage() handleMessage(client: WebSocketConnection, message: any) { client.broadcast(message); }}@Resolver()
Section titled “@Resolver()”Marks a class as a GraphQL resolver.
@Resolver()class UserResolver { @Query() async users() { return []; }
@Mutation() async createUser(@Arg('input', UserInput) input: any) { return { id: 1, ...input }; }}Method Decorators
Section titled “Method Decorators”HTTP Method Decorators
Section titled “HTTP Method Decorators”@Get(path?: string)
Section titled “@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)
Section titled “@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)
Section titled “@Put(path?: string)”@Put('/:id')async update( @Param('id') id: string, @Body(UserSchema) user: User) { // PUT /users/:id return { id, ...user };}@Delete(path?: string)
Section titled “@Delete(path?: string)”@Delete('/:id')async delete(@Param('id') id: string) { // DELETE /users/:id return { success: true };}@Patch(path?: string)
Section titled “@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)
Section titled “@All(path?: string)”Responds to all HTTP methods.
@All('/webhook')async webhook() { // Handles GET, POST, PUT, DELETE, etc. return { received: true };}GraphQL Method Decorators
Section titled “GraphQL Method Decorators”@Query()
Section titled “@Query()”@Resolver()class UserResolver { @Query() async users() { return await db.getUsers(); }
@Query() async user(@Arg('id', z.number()) id: number) { return await db.getUserById(id); }}@Mutation()
Section titled “@Mutation()”@Mutation()async createUser(@Arg('input', CreateUserInput) input: any) { return await db.createUser(input);}
@Mutation()async deleteUser(@Arg('id', z.number()) id: number) { return await db.deleteUser(id);}@Subscription()
Section titled “@Subscription()”@Subscription()async userCreated() { return pubsub.asyncIterator('USER_CREATED');}WebSocket Method Decorators
Section titled “WebSocket Method Decorators”@OnConnect()
Section titled “@OnConnect()”@OnConnect()handleConnect(client: WebSocketConnection) { console.log(`Client ${client.id} connected`); client.send({ type: 'welcome', message: 'Hello!' });}@OnMessage(schema?: ZodSchema)
Section titled “@OnMessage(schema?: ZodSchema)”const MessageSchema = z.object({ type: z.string(), content: z.string(),});
@OnMessage(MessageSchema)handleMessage( client: WebSocketConnection, message: z.infer<typeof MessageSchema>) { console.log('Received:', message); client.broadcast(message);}@OnDisconnect()
Section titled “@OnDisconnect()”@OnDisconnect()handleDisconnect(client: WebSocketConnection) { console.log(`Client ${client.id} disconnected`);}Middleware Decorator
Section titled “Middleware Decorator”@UseMiddleware(...middleware: Middleware[])
Section titled “@UseMiddleware(...middleware: Middleware[])”Apply middleware to a controller or specific route.
const authMiddleware = async (c, next) => { const token = c.req.header('authorization'); if (!token) { throw new HTTPException(401, 'Unauthorized'); } await next();};
const loggingMiddleware = async (c, next) => { console.log(`${c.req.method} ${c.req.url}`); await next();};
// Apply to entire controller@Controller('/admin')@UseMiddleware(authMiddleware, loggingMiddleware)class AdminController { @Get('/dashboard') async dashboard() { return { data: 'protected' }; }}
// Apply to specific route@Controller('/users')class UserController { @Get('/') async list() { return []; }
@Post('/') @UseMiddleware(authMiddleware) async create(@Body(UserSchema) user: User) { return user; }}OpenAPI Documentation Decorators
Section titled “OpenAPI Documentation Decorators”These decorators provide a concise shorthand for documenting routes in the OpenAPI spec, as an alternative to the verbose @ApiDoc({...}).
@Summary(text: string)
Section titled “@Summary(text: string)”Sets a short, one-line description visible in the Swagger UI route list.
@Get('/products')@Summary('List all products')async listProducts() { return products;}@Description(text: string)
Section titled “@Description(text: string)”Sets a longer description shown in the operation detail panel.
@Get('/products')@Summary('List all products')@Description('Returns a paginated list of active products. Supports filtering by category and price range.')async listProducts() { return products;}@Tag(name: string)
Section titled “@Tag(name: string)”Assigns a single OpenAPI tag to the route. Can be stacked multiple times.
@Get('/products')@Tag('Products')@Tag('Catalog')async listProducts() { return products;}@Tags(...names: string[])
Section titled “@Tags(...names: string[])”Assigns multiple tags in a single decorator.
@Get('/products')@Tags('Products', 'Catalog', 'Public')async listProducts() { return products;}@Deprecated()
Section titled “@Deprecated()”Marks the route as deprecated. It will appear with a strikethrough in Swagger UI.
@Delete('/products/:id')@Deprecated()@Summary('Delete a product (use PATCH /products/:id/archive instead)')async deleteProduct(@Param('id') id: string) { return { success: true };}:::note Auto-tagging
When you don’t add any @Tag or @Tags, the OpenAPI generator automatically derives tags from the first path segment. For example, /products/:id gets the tag "Products".
:::
Response Control Decorators
Section titled “Response Control Decorators”@HttpCode(statusCode: number)
Section titled “@HttpCode(statusCode: number)”Overrides the HTTP status code of the response. Also used by the OpenAPI generator to document the success response code.
Decorator order matters. TypeScript executes method decorators from bottom to top.
@HttpCodemust be placed above the HTTP-method decorator (@Post,@Get, etc.) so it runs after the route has been defined and can correctly overlay the status code.
@Post('/products')@HttpCode(201)async createProduct(@Body(CreateProductSchema) data: any) { return await db.create(data);}
@Delete('/products/:id')@HttpCode(204)async deleteProduct(@Param('id') id: string) { await db.delete(id);}@ResponseSchema(schema: ZodSchema, statusCode?: number)
Section titled “@ResponseSchema(schema: ZodSchema, statusCode?: number)”Validates and sanitizes the handler’s response using a Zod schema. Also informs the OpenAPI spec of the response model.
const ProductSchema = z.object({ id: z.number(), name: z.string(), price: z.number(),});
@Get('/products/:id')@ResponseSchema(ProductSchema)async getProduct(@Param('id') id: string) { return await db.findById(id); // Response will be validated and sanitized against ProductSchema}
@Post('/products')@HttpCode(201)@ResponseSchema(ProductSchema, 201)async createProduct(@Body(CreateProductSchema) data: any) { return await db.create(data);}Per-Route Middleware Decorators
Section titled “Per-Route Middleware Decorators”@Timeout(ms: number, message?: string)
Section titled “@Timeout(ms: number, message?: string)”Aborts the request with a 408 Request Timeout if the handler exceeds the time limit. Automatically injects the timeout middleware and emits the X-Timeout-Ms response header.
@Get('/report')@Timeout(5000)async generateReport() { return await heavyReport();}
@Post('/sync')@Timeout(30000, 'Sync operation timed out. Try again later.')async syncData() { return await externalSync();}@RateLimit(options: RateLimitOptions)
Section titled “@RateLimit(options: RateLimitOptions)”Applies rate-limiting to an individual route. Standard X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers are sent automatically.
@Post('/auth/login')@RateLimit({ windowMs: 15 * 60_000, max: 10 })async login(@Body(LoginSchema) credentials: any) { return await authService.login(credentials);}
@Post('/contact')@RateLimit({ windowMs: 60_000, max: 3, message: 'Too many messages. Wait 1 minute.' })async sendMessage(@Body(MessageSchema) data: any) { return await mailer.send(data);}Parameter Decorators
Section titled “Parameter Decorators”@Body(schema?: ZodSchema)
Section titled “@Body(schema?: ZodSchema)”Extract and validate request body.
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 is validated and typed return user;}Without schema:
@Post('/data')async postData(@Body() data: any) { // No validation, any data accepted return data;}@Query(nameOrSchema?: string | ZodSchema)
Section titled “@Query(nameOrSchema?: string | ZodSchema)”Extract and optionally validate query parameters (stable since v0.2.6).
// Extract all query parameters@Get('/search')async search(@Query() query: any) { const { q, page = 1, limit = 10 } = query; return { query: q, page, limit };}
// Extract specific parameter@Get('/users')async getUsers(@Query('page') page: string) { return { page: parseInt(page) || 1 };}
// Validate with Zod schemaconst 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)
Section titled “@Param(name?: string)”Extract route parameters.
@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 };}Extract all params:
@Get('/users/:userId/posts/:postId')async getPost(@Param() params: { userId: string; postId: string }) { return params;}@Header(name?: string, schema?: ZodSchema)
Section titled “@Header(name?: string, schema?: ZodSchema)”Extract and validate headers.
@Get('/protected')async getProtected(@Header('authorization') auth: string) { return { auth };}
const HeaderSchema = z.object({ 'x-api-key': z.string(), 'x-request-id': z.string().uuid().optional(),});
@Get('/api')async apiEndpoint(@Header(HeaderSchema) headers: z.infer<typeof HeaderSchema>) { return headers;}@Cookie(name?: string, schema?: ZodSchema)
Section titled “@Cookie(name?: string, schema?: ZodSchema)”Extract and validate cookies.
@Get('/profile')async getProfile(@Cookie('session') sessionId: string) { return { sessionId };}
const CookieSchema = z.object({ session: z.string(), preferences: z.string().optional(),});
@Get('/dashboard')async dashboard(@Cookie(CookieSchema) cookies: z.infer<typeof CookieSchema>) { return cookies;}@Ctx()
Section titled “@Ctx()”Injects the Hono context (c). Use it when you need c.redirect(), c.req.json(), headers, or the underlying Web API Request.
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;}For the standard Request object (Fetch API), use the context and read c.req.raw:
@Get('/ip')ip(@Ctx() c: Context) { const req = c.req.raw; // Web standard Request return { url: req.url };}@Req()
Section titled “@Req()”Injects Hono’s request wrapper (c.req in the router). It is exported from veloce-ts as @Req. If you need the plain Web Request, prefer @Ctx() and c.req.raw.
@Get('/path')path(@Req() req: import('hono').HonoRequest) { return { path: req.path };}@Depends(provider: Provider, scope?: Scope)
Section titled “@Depends(provider: Provider, scope?: Scope)”Inject dependencies.
class DatabaseService { async getUsers() { return []; }}
class UserService { constructor(private db: DatabaseService) {}
async findUser(id: string) { return this.db.getUsers().find(u => u.id === id); }}
@Controller('/users')class UserController { @Get('/') async list(@Depends(DatabaseService) db: DatabaseService) { return await db.getUsers(); }
@Get('/:id') async get( @Param('id') id: string, @Depends(UserService) userService: UserService ) { return await userService.findUser(id); }}With scope:
@Get('/users')async getUsers( @Depends(DatabaseService, 'singleton') db: DatabaseService) { return await db.getUsers();}@Arg(name: string, schema?: ZodSchema)
Section titled “@Arg(name: string, schema?: ZodSchema)”GraphQL argument with validation.
@Resolver()class UserResolver { @Query() async user(@Arg('id', z.number().int().positive()) id: number) { return await db.getUserById(id); }
@Mutation() async createUser( @Arg('input', CreateUserInput) input: z.infer<typeof CreateUserInput> ) { return await db.createUser(input); }}Combining Decorators
Section titled “Combining Decorators”Multiple Parameter Decorators
Section titled “Multiple Parameter Decorators”@Post('/users/:id/posts')async createPost( @Param('id') userId: string, @Body(PostSchema) post: Post, @Header('authorization') auth: string, @Depends(DatabaseService) db: DatabaseService) { // All parameters are extracted and validated return await db.createPost(userId, post);}Controller with Multiple Decorators
Section titled “Controller with Multiple Decorators”@Controller('/api/admin')@UseMiddleware(authMiddleware, loggingMiddleware)class AdminController { @Get('/users') async getUsers(@Depends(UserService) userService: UserService) { return await userService.getAllUsers(); }
@Post('/users') @UseMiddleware(validationMiddleware) async createUser( @Body(UserSchema) user: User, @Depends(UserService) userService: UserService ) { return await userService.createUser(user); }
@Delete('/users/:id') async deleteUser( @Param('id') id: string, @Depends(UserService) userService: UserService ) { return await userService.deleteUser(id); }}Best Practices
Section titled “Best Practices”1. Always Use Schemas for Validation
Section titled “1. Always Use Schemas for Validation”// ✓ Goodconst 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;}
// ✗ Bad@Post('/users')async createUser(@Body() user: any) { return user;}2. Use Type Inference
Section titled “2. Use Type Inference”// ✓ Goodconst 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 is fully typed}
// ✗ Bad@Post('/users')async createUser(@Body(UserSchema) user: any) { // Lost type safety}3. Organize Controllers Logically
Section titled “3. Organize Controllers Logically”// ✓ Good - One controller per resource@Controller('/users')class UserController { // All user-related routes}
@Controller('/posts')class PostController { // All post-related routes}
// ✗ Bad - Mixed concerns@Controller('/api')class ApiController { @Get('/users') async getUsers() {}
@Get('/posts') async getPosts() {}}4. Use Dependency Injection
Section titled “4. Use Dependency Injection”// ✓ Goodclass UserService { async getUsers() { return []; }}
@Controller('/users')class UserController { @Get('/') async list(@Depends(UserService) userService: UserService) { return await userService.getUsers(); }}
// ✗ Bad@Controller('/users')class UserController { @Get('/') async list() { // Direct database access in controller return await db.query('SELECT * FROM users'); }}5. Apply Middleware Appropriately
Section titled “5. Apply Middleware Appropriately”// ✓ Good - Apply auth to entire admin controller@Controller('/admin')@UseMiddleware(authMiddleware)class AdminController { // All routes protected}
// ✓ Good - Apply to specific sensitive routes@Controller('/users')class UserController { @Get('/') async list() { // Public }
@Delete('/:id') @UseMiddleware(authMiddleware) async delete(@Param('id') id: string) { // Protected }}6. Handle Errors Gracefully
Section titled “6. Handle Errors Gracefully”@Get('/users/:id')async getUser(@Param('id') id: string) { const user = await db.getUserById(id);
if (!user) { throw new HTTPException(404, 'User not found'); }
return user;}Next Steps
Section titled “Next Steps”- Learn about Dependency Injection
- Explore Plugins
- Check out the Getting Started Guide