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