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
- 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; }}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. Fixed in v0.2.6 - now works reliably!
// 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, };}:::tip v0.2.6 Improvement
The @Query() decorator now properly extracts query parameters without requiring manual access to the context object. This makes the API cleaner and more intuitive.
:::
@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;}@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