Complete guide to routing in uWestJS with NestJS decorators and middleware.
- Overview
- Route Registration
- Path Parameters
- Query Parameters
- Route Middleware
- Guards
- Pipes
- Filters
- Execution Order
- Examples
uWestJS provides full routing support with NestJS decorators:
- HTTP Methods - GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD
- Path Parameters - Dynamic route segments
- Query Parameters - URL query string parsing
- Middleware - Guards, Pipes, Filters, Interceptors
- Wildcard Routes - Catch-all routes
- Route Versioning - API versioning support
Use NestJS decorators to define routes:
import { Controller, Get, Post, Put, Delete, Patch } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
return { users: [] };
}
@Get(':id')
findOne(@Param('id') id: string) {
return { user: { id } };
}
@Post()
create(@Body() data: any) {
return { created: true, data };
}
@Put(':id')
update(@Param('id') id: string, @Body() data: any) {
return { updated: true, id, data };
}
@Delete(':id')
remove(@Param('id') id: string) {
return { deleted: true, id };
}
@Patch(':id')
patch(@Param('id') id: string, @Body() data: any) {
return { patched: true, id, data };
}
}Routes are automatically prefixed with the controller path:
@Controller('api/v1/users')
export class UsersController {
@Get() // Matches: GET /api/v1/users
findAll() { }
@Get(':id') // Matches: GET /api/v1/users/:id
findOne() { }
@Get('profile/:id') // Matches: GET /api/v1/users/profile/:id
getProfile() { }
}All standard HTTP methods are supported:
@Controller('api')
export class ApiController {
@Get('resource')
get() { }
@Post('resource')
post() { }
@Put('resource')
put() { }
@Delete('resource')
delete() { }
@Patch('resource')
patch() { }
@Options('resource')
options() { }
@Head('resource')
head() { }
}Extract path parameters using @Param():
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
return { user: { id } };
}
}@Controller('posts')
export class PostsController {
@Get(':userId/posts/:postId')
getPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return { userId, postId };
}
}Get all parameters as an object:
@Get(':category/:id')
getItem(@Param() params: any) {
console.log(params.category, params.id);
return params;
}Use pipes to validate and transform parameters:
import { ParseIntPipe } from '@nestjs/common';
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id is automatically converted to number
return { user: { id } };
}Extract query parameters using @Query():
@Get('search')
search(@Query('q') query: string) {
return { results: [], query };
}@Get('search')
search(
@Query('q') query: string,
@Query('page') page: string,
@Query('limit') limit: string,
) {
return { query, page, limit };
}Get all query parameters as an object:
@Get('search')
search(@Query() query: any) {
console.log(query); // { q: 'test', page: '1', limit: '10' }
return { results: [], query };
}import { ParseIntPipe } from '@nestjs/common';
@Get('users')
findAll(
@Query('page', ParseIntPipe) page: number,
@Query('limit', ParseIntPipe) limit: number,
) {
return { users: [], page, limit };
}Guards determine whether a request should be handled:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: any): boolean {
// Validate authentication
return !!request.headers.authorization;
}
}
// Use guard
@Controller('api')
@UseGuards(AuthGuard)
export class ApiController {
@Get('protected')
getProtected() {
return { data: 'protected' };
}
}Pipes transform and validate input data:
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// metadata provides information about the argument being processed:
// - type: 'body' | 'query' | 'param' | 'custom'
// - metatype: The TypeScript type (e.g., String, Number, CreateUserDto)
// - data: The parameter name (e.g., 'id', 'email')
if (!value) {
throw new BadRequestException(`${metadata.data || 'Value'} is required`);
}
// Example: Use metadata to apply different validation based on type
if (metadata.type === 'param' && typeof value !== 'string') {
throw new BadRequestException(`${metadata.data} must be a string`);
}
return value;
}
}
// Use pipe
@Post('users')
create(@Body(ValidationPipe) data: any) {
return { created: true, data };
}Filters handle exceptions:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
// Use filter
@Controller('api')
@UseFilters(HttpExceptionFilter)
export class ApiController {
@Get('error')
throwError() {
throw new HttpException('Something went wrong', 500);
}
}Middleware executes in this order:
- Guards - Check if request should be processed
- Interceptors (before) - Pre-processing
- Pipes - Transform and validate data
- Route Handler - Execute the controller method
- Interceptors (after) - Post-processing
- Filters - Catch exceptions (if thrown)
@UseGuards(AuthGuard) // 1. Guard
@UseInterceptors(LoggingInterceptor) // 2. Interceptor
@UsePipes(ValidationPipe) // 3. Pipe
@UseFilters(HttpExceptionFilter) // 6. Filter (if error)
@Get('resource')
getResource() { // 4. Handler
return { data: 'resource' };
}@Controller('api/posts')
export class PostsController {
constructor(private postsService: PostsService) {}
@Get()
async findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.postsService.findAll(page, limit);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.postsService.findOne(id);
}
@Post()
@UseGuards(AuthGuard)
@UsePipes(ValidationPipe)
async create(@Body() createPostDto: CreatePostDto) {
return this.postsService.create(createPostDto);
}
@Put(':id')
@UseGuards(AuthGuard)
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updatePostDto: UpdatePostDto,
) {
return this.postsService.update(id, updatePostDto);
}
@Delete(':id')
@UseGuards(AuthGuard)
async remove(@Param('id', ParseIntPipe) id: number) {
return this.postsService.remove(id);
}
}@Controller('users')
export class UsersController {
@Get(':userId/posts')
getUserPosts(@Param('userId') userId: string) {
return { posts: [], userId };
}
@Get(':userId/posts/:postId')
getUserPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return { post: {}, userId, postId };
}
@Get(':userId/posts/:postId/comments')
getPostComments(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return { comments: [], userId, postId };
}
}@Controller('api/products')
export class ProductsController {
@Get('search')
search(
@Query('q') query: string,
@Query('category') category?: string,
@Query('minPrice') minPrice?: number,
@Query('maxPrice') maxPrice?: number,
@Query('page') page = 1,
@Query('limit') limit = 20,
) {
return this.productsService.search({
query,
category,
minPrice,
maxPrice,
page,
limit,
});
}
}import { UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('upload')
export class UploadController {
@Post('file')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
filename: file.originalname,
size: file.size,
mimetype: file.mimetype,
};
}
}@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@UsePipes(ValidationPipe)
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@Post('register')
@UsePipes(ValidationPipe)
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@Get('profile')
@UseGuards(AuthGuard)
async getProfile(@Req() req: any) {
return req.user;
}
@Post('logout')
@UseGuards(AuthGuard)
async logout(@Req() req: any) {
return this.authService.logout(req.user);
}
}@Controller({ path: 'users', version: '1' })
export class UsersV1Controller {
@Get()
findAll() {
return { version: 1, users: [] };
}
}
@Controller({ path: 'users', version: '2' })
export class UsersV2Controller {
@Get()
findAll() {
return { version: 2, users: [], metadata: {} };
}
}@Controller('api')
export class ApiController {
@Get('*')
catchAll(@Req() req: any) {
return {
message: 'Route not found',
path: req.url,
};
}
}import { createParamDecorator, ExecutionContext } from '@nestjs/common';
// Create custom decorator
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// Use custom decorator
@Controller('api')
export class ApiController {
@Get('me')
@UseGuards(AuthGuard)
getMe(@User() user: any) {
return user;
}
}import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
// IMPORTANT: This uses in-memory storage and only works for single-instance deployments
// For production with multiple server instances, use @nestjs/throttler with Redis
@Injectable()
export class RateLimitGuard implements CanActivate {
private requests = new Map<string, number[]>();
private readonly limit = 100;
private readonly window = 60000; // 1 minute
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const ip = request.ip;
const now = Date.now();
const requests = this.requests.get(ip) || [];
const recentRequests = requests.filter(time => now - time < this.window);
if (recentRequests.length >= this.limit) {
return false;
}
recentRequests.push(now);
this.requests.set(ip, recentRequests);
return true;
}
}
@Controller('api')
@UseGuards(RateLimitGuard)
export class ApiController {
@Get('resource')
getResource() {
return { data: 'resource' };
}
}
// For distributed rate limiting across multiple server instances:
// npm install @nestjs/throttler @nestjs/throttler-storage-redis
//
// import { ThrottlerModule } from '@nestjs/throttler';
// import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
//
// @Module({
// imports: [
// ThrottlerModule.forRoot({
// ttl: 60,
// limit: 100,
// storage: new ThrottlerStorageRedisService(redisClient),
// }),
// ],
// })