React & Next.js Topic
NestJs Controllers, Routing & Request Handling
1. Controllers Overview
Controllers in NestJS are responsible for handling incoming HTTP requests and returning responses to the client. They use decorators to define routes and map them to handler methods.
Basic Controller Structure:
typescript
import { Controller } from '@nestjs/common';
@Controller('prefix')
export class MyController {
// Handler methods go here
}
2. Creating Routes
Generate a Controller:
bash
nest generate controller cats # Shorthand: nest g co cats
Basic Controller Example:
typescript
import { Controller, Get } from '@nestjs/common';
@Controller('cats') // Route prefix: /cats
export class CatsController {
@Get() // GET /cats
findAll(): string {
return 'This action returns all cats';
}
@Get('profile') // GET /cats/profile
getProfile(): string {
return 'This is the cat profile';
}
}
3. Route Decorators
NestJS provides HTTP method decorators that correspond to HTTP verbs:
HTTP Method Decorators:
typescript
import {
Controller,
Get, // GET request
Post, // POST request
Put, // PUT request
Patch, // PATCH request
Delete, // DELETE request
Options, // OPTIONS request
Head, // HEAD request
All // All HTTP methods
} from '@nestjs/common';
@Controller('items')
export class ItemsController {
@Get()
findAll(): string {
return 'GET: All items';
}
@Post()
create(): string {
return 'POST: Create item';
}
@Put(':id')
update(): string {
return 'PUT: Update item';
}
@Delete(':id')
remove(): string {
return 'DELETE: Remove item';
}
@Patch(':id')
partialUpdate(): string {
return 'PATCH: Partial update';
}
@All()
handleAllMethods(): string {
return 'This route handles ALL HTTP methods';
}
}
Status Code Decorators:
typescript
import { HttpCode, HttpStatus } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
@HttpCode(201) // Set specific status code
create() {
return 'User created';
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // Using HttpStatus enum
remove() {
// No content response
}
}
Route Wildcards:
typescript
@Controller('files')
export class FilesController {
@Get('docs/*') // Matches /files/docs/anything
getDocs() {
return 'Documentation files';
}
@Get('images/*.jpg') // Matches /files/images/anyfile.jpg
getJpgImages() {
return 'JPG images only';
}
}
4. Route Parameters
Basic Route Parameters:
typescript
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param() params: any): string {
return `User ID: ${params.id}`;
}
// Alternative: Extract specific parameter
@Get(':id')
findOne(@Param('id') id: string): string {
return `User ID: ${id}`;
}
}
Multiple Route Parameters:
typescript
@Controller('blogs')
export class BlogsController {
@Get(':userId/posts/:postId')
findPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return `User ${userId}, Post ${postId}`;
}
// Or get all parameters as an object
@Get(':userId/posts/:postId')
findPost(@Param() params: any) {
return `User ${params.userId}, Post ${params.postId}`;
}
}
Parameter Transformation with Pipes:
typescript
import { ParseIntPipe, ParseUUIDPipe, ParseBoolPipe } from '@nestjs/common';
@Controller('products')
export class ProductsController {
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id is automatically converted to number
return `Product ID (number): ${id}`;
}
@Get('uuid/:id')
findById(@Param('id', ParseUUIDPipe) id: string) {
// Validates UUID format
return `UUID: ${id}`;
}
@Get('status/:isActive')
findByStatus(@Param('isActive', ParseBoolPipe) isActive: boolean) {
// Converts string to boolean
return `Active status: ${isActive}`;
}
}
Custom Parameter Validation:
typescript
import { Controller, Get, Param, BadRequestException } from '@nestjs/common';
@Controller('orders')
export class OrdersController {
@Get(':id')
findOne(@Param('id') id: string) {
if (!/^\d+$/.test(id)) {
throw new BadRequestException('ID must be numeric');
}
return `Order ID: ${id}`;
}
}
5. Query Parameters
Basic Query Parameters:
typescript
import { Controller, Get, Query } from '@nestjs/common';
@Controller('products')
export class ProductsController {
@Get('search')
search(
@Query('name') name?: string,
@Query('category') category?: string,
@Query('price') price?: string,
) {
return {
message: 'Search results',
filters: { name, category, price }
};
}
}
// URL: /products/search?name=laptop&category=electronics&price=1000
Multiple Query Parameters:
typescript
@Get('filter')
filter(@Query() queryParams: any) {
// Get all query parameters as an object
return {
filters: queryParams,
message: 'Filtering products'
};
}
// URL: /products/filter?category=books&minPrice=10&maxPrice=100&sort=desc
Query Parameters with Default Values:
typescript
@Get('list')
list(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
@Query('sort') sort: string = 'asc',
) {
return {
page: parseInt(page, 10),
limit: parseInt(limit, 10),
sort,
items: []
};
}
Query Parameters with Validation Pipes:
typescript
import {
Controller,
Get,
Query,
DefaultValuePipe,
ParseIntPipe
} from '@nestjs/common';
@Controller('articles')
export class ArticlesController {
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return {
page,
limit,
data: []
};
}
}
Complex Query Parameters (Arrays):
typescript
@Get('multi-filter')
multiFilter(@Query('categories') categories?: string | string[]) {
// Handle both single value and array values
const categoriesArray = Array.isArray(categories)
? categories
: categories ? [categories] : [];
return {
categories: categoriesArray,
message: 'Multiple categories filter'
};
}
// URL: /products/multi-filter?categories=books&categories=electronics
6. Request & Response Objects
Accessing Request Object:
typescript
import {
Controller,
Get,
Req,
Res,
Headers,
Ip,
HostParam
} from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('info')
export class InfoController {
@Get('request')
getRequestInfo(@Req() request: Request) {
// Access full Express request object
return {
method: request.method,
url: request.url,
headers: request.headers,
ip: request.ip
};
}
@Get('headers')
getHeaders(@Headers() headers: any) {
// Get specific header
return {
'user-agent': headers['user-agent'],
'accept': headers.accept
};
}
@Get('ip')
getIp(@Ip() ip: string) {
return { clientIp: ip };
}
@Get('host')
getHost(@HostParam() host: string) {
return { host };
}
}
Custom Response Handling:
typescript
import { Controller, Get, Res, Header } from '@nestjs/common';
import { Response } from 'express';
@Controller('custom')
export class CustomController {
@Get('download')
@Header('Content-Type', 'text/csv')
@Header('Content-Disposition', 'attachment; filename="data.csv"')
download(@Res() res: Response) {
const csvData = 'id,name,email\n1,John,john@example.com\n2,Jane,jane@example.com';
res.send(csvData);
}
@Get('json')
sendJson(@Res() res: Response) {
return res.status(201).json({
success: true,
data: { message: 'Custom response' }
});
}
}
Response Decorators:
typescript
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
Header,
Redirect
} from '@nestjs/common';
@Controller('auth')
export class AuthController {
@Post('login')
@HttpCode(HttpStatus.OK)
@Header('Cache-Control', 'none')
login(@Body() credentials: any) {
return {
token: 'jwt-token-here',
user: { id: 1, name: 'John' }
};
}
@Get('old-route')
@Redirect('/auth/new-route', 301)
oldRoute() {
// Permanent redirect
}
@Get('redirect')
@Redirect('https://nestjs.com', 302)
redirectToDocs() {
// Temporary redirect
}
}
7. DTO Basics
DTO (Data Transfer Object) is an object that defines how data will be sent over the network. It helps with:
-
Data validation
-
Data transformation
-
Type safety
-
Documentation
Basic DTO Example:
1. Install required packages:
bash
npm install class-validator class-transformer
2. Create a DTO class:
typescript
// src/users/dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsInt,
Min,
Max,
IsOptional,
IsNotEmpty,
MinLength,
MaxLength,
IsBoolean
} from 'class-validator';
import { Type } from 'class-transformer';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsInt()
@Min(18)
@Max(120)
@IsOptional()
age?: number;
@IsBoolean()
@IsOptional()
isActive?: boolean = true;
}
export class UpdateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
@IsOptional()
name?: string;
@IsEmail()
@IsOptional()
email?: string;
@IsInt()
@Min(18)
@Max(120)
@IsOptional()
age?: number;
}
3. Using DTOs in Controller:
typescript
import {
Controller,
Post,
Body,
Put,
Param,
ValidationPipe
} from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
// TypeScript knows the shape of createUserDto
return {
message: 'User created successfully',
data: createUserDto
};
}
@Put(':id')
update(
@Param('id') id: string,
@Body(ValidationPipe) updateUserDto: UpdateUserDto,
) {
return {
message: 'User updated',
id,
updates: updateUserDto
};
}
}
4. Enable Global Validation (in main.ts):
typescript
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Remove properties not in DTO
forbidNonWhitelisted: true, // Throw error if extra properties exist
transform: true, // Transform payload to DTO instance
transformOptions: {
enableImplicitConversion: true, // Convert types automatically
},
}));
await app.listen(3000);
}
bootstrap();
5. Nested DTOs (Complex Objects):
typescript
// src/products/dto/create-product.dto.ts
import {
IsString,
IsNumber,
IsArray,
ValidateNested,
ArrayMinSize
} from 'class-validator';
import { Type } from 'class-transformer';
class ProductImageDto {
@IsString()
url: string;
@IsString()
alt: string;
}
class ProductCategoryDto {
@IsString()
id: string;
@IsString()
name: string;
}
export class CreateProductDto {
@IsString()
name: string;
@IsNumber()
price: number;
@IsArray()
@ValidateNested({ each: true })
@ArrayMinSize(1)
@Type(() => ProductImageDto)
images: ProductImageDto[];
@ValidateNested()
@Type(() => ProductCategoryDto)
category: ProductCategoryDto;
}
8. Complete Example: Blog API Controller
typescript
// src/posts/posts.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
UsePipes,
ValidationPipe,
ParseIntPipe,
DefaultValuePipe,
Req,
Header
} from '@nestjs/common';
import { Request } from 'express';
import { PostsService } from './posts.service';
import { CreatePostDto, UpdatePostDto } from './dto/post.dto';
import { FilterPostsDto } from './dto/filter-posts.dto';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
// GET /posts
@Get()
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query('category') category?: string,
@Query('author') author?: string,
) {
return this.postsService.findAll({
page,
limit,
category,
author
});
}
// GET /posts/filter (with DTO)
@Get('filter')
@UsePipes(new ValidationPipe({ transform: true }))
async filter(@Query() filterDto: FilterPostsDto) {
return this.postsService.filter(filterDto);
}
// GET /posts/:id
@Get(':id')
@Header('Cache-Control', 'public, max-age=3600')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.postsService.findOne(id);
}
// POST /posts
@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Body(ValidationPipe) createPostDto: CreatePostDto,
@Req() req: Request,
) {
const userId = req.headers['user-id']; // Example: Get user from headers
return this.postsService.create(createPostDto, userId);
}
// PUT /posts/:id
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body(ValidationPipe) updatePostDto: UpdatePostDto,
) {
return this.postsService.update(id, updatePostDto);
}
// DELETE /posts/:id
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number) {
return this.postsService.remove(id);
}
// Nested route: GET /posts/:id/comments
@Get(':id/comments')
async getComments(
@Param('id', ParseIntPipe) id: number,
@Query('sort') sort: string = 'newest',
) {
return this.postsService.getComments(id, sort);
}
// Wildcard route: GET /posts/search/* (matches any path after search)
@Get('search/*')
async search(@Req() req: Request) {
const searchTerm = req.params[0]; // Get the wildcard parameter
return this.postsService.search(searchTerm);
}
}
Corresponding DTOs:
typescript
// src/posts/dto/post.dto.ts
import {
IsString,
IsNotEmpty,
IsOptional,
IsArray,
IsInt,
Min,
MaxLength,
IsBoolean
} from 'class-validator';
export class CreatePostDto {
@IsString()
@IsNotEmpty()
@MaxLength(200)
title: string;
@IsString()
@IsNotEmpty()
content: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
tags?: string[];
@IsBoolean()
@IsOptional()
published?: boolean = false;
}
export class UpdatePostDto {
@IsString()
@IsOptional()
@MaxLength(200)
title?: string;
@IsString()
@IsOptional()
content?: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
tags?: string[];
@IsBoolean()
@IsOptional()
published?: boolean;
}
// src/posts/dto/filter-posts.dto.ts
import {
IsOptional,
IsString,
IsInt,
Min,
Max,
IsDateString
} from 'class-validator';
import { Type } from 'class-transformer';
export class FilterPostsDto {
@IsOptional()
@IsInt()
@Type(() => Number)
@Min(1)
page?: number = 1;
@IsOptional()
@IsInt()
@Type(() => Number)
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@IsString()
category?: string;
@IsOptional()
@IsString()
author?: string;
@IsOptional()
@IsDateString()
fromDate?: string;
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC' = 'DESC';
}
Short notes:
-
Controllers are the entry points for HTTP requests
-
Route decorators (@Get, @Post, etc.) define HTTP methods and paths
-
Parameter decorators (@Param, @Query, @Body) extract data from requests
-
DTOs provide validation and type safety for incoming data
-
Pipes transform and validate data
-
Response decorators (@HttpCode, @Header, @Redirect) customize responses
-
Request/Response objects can be accessed directly when needed
-
Nested routes and wildcards provide flexible routing options
This structure ensures clean, maintainable, and type-safe code while handling all common HTTP request scenarios.
Comments (0)
No comments yet. Be the first to comment!