Menu

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:

  1. Controllers are the entry points for HTTP requests

  2. Route decorators (@Get, @Post, etc.) define HTTP methods and paths

  3. Parameter decorators (@Param, @Query, @Body) extract data from requests

  4. DTOs provide validation and type safety for incoming data

  5. Pipes transform and validate data

  6. Response decorators (@HttpCode, @Header, @Redirect) customize responses

  7. Request/Response objects can be accessed directly when needed

  8. 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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?