Skip to content

Decorators Guide

Comprehensive guide to using decorators in Veloce.

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.

To use decorators, enable them in your tsconfig.json:

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

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' };
}
}

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);
}
}

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 };
}
}
@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 };
}

Responds to all HTTP methods.

@All('/webhook')
async webhook() {
// Handles GET, POST, PUT, DELETE, etc.
return { received: true };
}
@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()
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()
async userCreated() {
return pubsub.asyncIterator('USER_CREATED');
}
@OnConnect()
handleConnect(client: WebSocketConnection) {
console.log(`Client ${client.id} connected`);
client.send({ type: 'welcome', message: 'Hello!' });
}
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()
handleDisconnect(client: WebSocketConnection) {
console.log(`Client ${client.id} disconnected`);
}

@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;
}
}

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;
}

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 schema
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,
};
}

:::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. :::

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();
}

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);
}
}
@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('/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);
}
}
// ✓ Good
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;
}
// ✗ Bad
@Post('/users')
async createUser(@Body() user: any) {
return user;
}
// ✓ Good
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 is fully typed
}
// ✗ Bad
@Post('/users')
async createUser(@Body(UserSchema) user: any) {
// Lost type safety
}
// ✓ 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() {}
}
// ✓ Good
class 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');
}
}
// ✓ 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
}
}
@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;
}