NestJS Fundamentals: Day1

Quick Setup

Install NestJS CLI

npm install -g @nestjs/cli

Create project

nest new todo-app

Start development server

npm run start:dev

Generate resource (creates Controller, Service, Module, DTO, Entity)

nest generate resource todos

Basic API Structure

The generated files provide a complete CRUD API: - GET /todos - Get all todos - POST /todos - Create new todo - GET /todos/:id - Get single todo - PATCH /todos/:id - Update todo - DELETE /todos/:id - Delete todo

Day 2: Service Layer and Data Validation

Install Validation Dependencies

npm install class-validator class-transformer

Entity Definition

// src/todos/entities/todo.entity.ts
export class Todo {
  id: number;
  title: string;
  description?: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate?: Date;
  createdAt: Date;
  updatedAt: Date;

  constructor(partial: Partial) {
    Object.assign(this, partial);
    this.completed = this.completed || false;
    this.priority = this.priority || 'medium';
    this.createdAt = this.createdAt || new Date();
    this.updatedAt = new Date();
  }
}

Create Todo DTO

// src/todos/dto/create-todo.dto.ts
import { IsString, IsNotEmpty, IsOptional, IsEnum, IsDateString, MaxLength } from 'class-validator';

export class CreateTodoDto {
  @IsString()
  @IsNotEmpty({ message: 'Title is required' })
  @MaxLength(100, { message: 'Title must be 100 characters or less' })
  title: string;

  @IsString()
  @IsOptional()
  @MaxLength(500, { message: 'Description must be 500 characters or less' })
  description?: string;

  @IsEnum(['low', 'medium', 'high'], { message: 'Priority must be low, medium, or high' })
  @IsOptional()
  priority?: 'low' | 'medium' | 'high';

  @IsDateString({}, { message: 'Please provide a valid date format' })
  @IsOptional()
  dueDate?: string;
}

Update Todo DTO

// src/todos/dto/update-todo.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { IsBoolean, IsOptional } from 'class-validator';
import { CreateTodoDto } from './create-todo.dto';

export class UpdateTodoDto extends PartialType(CreateTodoDto) {
  @IsBoolean({ message: 'Completed status must be true or false' })
  @IsOptional()
  completed?: boolean;
}

Service Implementation

// src/todos/todos.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { Todo } from './entities/todo.entity';

@Injectable()
export class TodosService {
  private todos: Todo[] = [];
  private currentId = 1;

  create(createTodoDto: CreateTodoDto): Todo {
    const todo = new Todo({
      id: this.currentId++,
      title: createTodoDto.title,
      description: createTodoDto.description,
      priority: createTodoDto.priority || 'medium',
      dueDate: createTodoDto.dueDate ? new Date(createTodoDto.dueDate) : undefined,
    });

    this.todos.push(todo);
    return todo;
  }

  findAll(): Todo[] {
    return this.todos;
  }

  findOne(id: number): Todo {
    const todo = this.todos.find(todo => todo.id === id);
    if (!todo) {
      throw new NotFoundException(`Todo with ID ${id} not found`);
    }
    return todo;
  }

  update(id: number, updateTodoDto: UpdateTodoDto): Todo {
    const todoIndex = this.todos.findIndex(todo => todo.id === id);
    if (todoIndex === -1) {
      throw new NotFoundException(`Todo with ID ${id} not found`);
    }

    const existingTodo = this.todos[todoIndex];
    const updatedTodo = new Todo({
      ...existingTodo,
      ...updateTodoDto,
      dueDate: updateTodoDto.dueDate ? new Date(updateTodoDto.dueDate) : existingTodo.dueDate,
      updatedAt: new Date(),
    });

    this.todos[todoIndex] = updatedTodo;
    return updatedTodo;
  }

  remove(id: number): { message: string } {
    const todoIndex = this.todos.findIndex(todo => todo.id === id);
    if (todoIndex === -1) {
      throw new NotFoundException(`Todo with ID ${id} not found`);
    }

    this.todos.splice(todoIndex, 1);
    return { message: `Todo with ID ${id} has been deleted` };
  }

  findByStatus(completed: boolean): Todo[] {
    return this.todos.filter(todo => todo.completed === completed);
  }

  findByPriority(priority: 'low' | 'medium' | 'high'): Todo[] {
    return this.todos.filter(todo => todo.priority === priority);
  }

  getStats(): { total: number; completed: number; pending: number } {
    const total = this.todos.length;
    const completed = this.todos.filter(todo => todo.completed).length;
    const pending = total - completed;
    return { total, completed, pending };
  }
}

Controller Implementation

import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, ValidationPipe } from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto, Priority } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  @Post()
  create(@Body(ValidationPipe) createTodoDto: CreateTodoDto) {
    // @Body(ValidationPipe) does several things:
    // 1. Extracts JSON from request body
    // 2. Validates it against CreateTodoDto rules
    // 3. Transforms it to CreateTodoDto instance
    // 4. If validation fails, automatically returns 400 error
    return this.todosService.create(createTodoDto);
  }

  @Get()
  findAll(@Query("status")status?: string, @Query("priority")priority?: Priority) {
    if (status === "completed") {
      return this.todosService.findByStatus(true);
    }

    if (status === "pending") {
      return this.todosService.findByStatus(false);
    }
    
    if (priority && ['low', 'medium', 'high'].includes(priority)) {
      return this.todosService.findByPriority(priority);
    }

    return this.todosService.findAll();
  }

  @Get('stats')
  getStats() {
    return this.todosService.getByStats();
  }

  @Get(":id")
  findOne(@Param('id', ParseIntPipe) id: number) {
    // @Param('id') extracts the :id from URL
    // ParseIntPipe converts string "123" to number 123
    // If conversion fails, automatically returns 400 error
    return this.todosService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id', ParseIntPipe) id: number, @Body(ValidationPipe) updateTodoDto: UpdateTodoDto) {
    return this.todosService.update(+id, updateTodoDto);
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.todosService.remove(+id);
  }
}