Learn NestJS from scratch by building a production-ready REST API. This hands-on guide covers setup, core concepts, and best practices for modern backend development.

NestJS has become the go-to framework for building scalable backend applications in the Node.js ecosystem. If you're coming from Express or Fastify, you might wonder what makes NestJS different. The answer lies in its opinionated architecture, built-in dependency injection, and TypeScript-first approach that makes your codebase maintainable and testable from day one.
In this guide, we'll build a real project—a task management API—that demonstrates NestJS fundamentals. You'll understand how modules, controllers, services, and decorators work together to create clean, production-grade code.
Before diving into code, let's understand why NestJS matters. Traditional Express applications often become messy as they scale. NestJS enforces structure through its modular architecture, inspired by Angular. This means:
The fastest way to start is using the NestJS CLI:
npm install -g @nestjs/cliCreate a new project:
nest new task-api
cd task-apiThe CLI generates a complete project structure with everything you need. Let's verify the setup works:
npm run start:devVisit http://localhost:3000 and you should see Hello World!. That's your first NestJS app running.
task-api/
├── src/
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test/
├── package.json
├── tsconfig.json
└── nest-cli.jsonEach file serves a purpose:
main.ts: Application entry pointapp.module.ts: Root module that organizes your appapp.controller.ts: Handles HTTP requestsapp.service.ts: Contains business logicModules are containers that organize related functionality. Think of them as feature domains. A module can have controllers, services, and other modules as dependencies.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}The @Module decorator defines:
imports: Other modules this module depends oncontrollers: Request handlersproviders: Services and other injectable classesControllers handle incoming HTTP requests and return responses. They use decorators to define routes and HTTP methods.
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('tasks')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getAllTasks() {
return this.appService.getAllTasks();
}
@Post()
createTask(@Body() createTaskDto: any) {
return this.appService.createTask(createTaskDto);
}
@Get(':id')
getTaskById(@Param('id') id: string) {
return this.appService.getTaskById(id);
}
}Key decorators:
@Controller('tasks'): Base route for all methods in this controller@Get(), @Post(): HTTP method decorators@Body(): Extract request body@Param(): Extract URL parametersServices contain business logic and are reusable across controllers. They're injected via the constructor.
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
private tasks = [
{ id: 1, title: 'Learn NestJS', completed: false },
{ id: 2, title: 'Build an API', completed: false },
];
getAllTasks() {
return this.tasks;
}
createTask(createTaskDto: any) {
const newTask = {
id: this.tasks.length + 1,
...createTaskDto,
completed: false,
};
this.tasks.push(newTask);
return newTask;
}
getTaskById(id: string) {
return this.tasks.find(task => task.id === parseInt(id));
}
}The @Injectable() decorator marks a class as a provider that can be injected into other classes.
Now let's build a complete task API with proper structure. We'll create a dedicated tasks module.
nest generate module tasks
nest generate controller tasks
nest generate service tasksThis creates the module structure automatically:
src/
├── tasks/
│ ├── tasks.module.ts
│ ├── tasks.controller.ts
│ └── tasks.service.tsDTOs define the shape of data coming in and going out. Create src/tasks/dto/create-task.dto.ts:
export class CreateTaskDto {
title: string;
description?: string;
}import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateTaskDto } from './dto/create-task.dto';
interface Task {
id: number;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
}
@Injectable()
export class TasksService {
private tasks: Task[] = [];
private idCounter = 1;
create(createTaskDto: CreateTaskDto): Task {
const task: Task = {
id: this.idCounter++,
...createTaskDto,
completed: false,
createdAt: new Date(),
};
this.tasks.push(task);
return task;
}
findAll(): Task[] {
return this.tasks;
}
findOne(id: number): Task {
const task = this.tasks.find(t => t.id === id);
if (!task) {
throw new NotFoundException(`Task with ID ${id} not found`);
}
return task;
}
update(id: number, updateTaskDto: Partial<CreateTaskDto>): Task {
const task = this.findOne(id);
Object.assign(task, updateTaskDto);
return task;
}
remove(id: number): void {
const index = this.tasks.findIndex(t => t.id === id);
if (index === -1) {
throw new NotFoundException(`Task with ID ${id} not found`);
}
this.tasks.splice(index, 1);
}
toggleComplete(id: number): Task {
const task = this.findOne(id);
task.completed = !task.completed;
return task;
}
}import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Patch,
ParseIntPipe,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Post()
create(@Body() createTaskDto: CreateTaskDto) {
return this.tasksService.create(createTaskDto);
}
@Get()
findAll() {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.tasksService.findOne(id);
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateTaskDto: Partial<CreateTaskDto>,
) {
return this.tasksService.update(id, updateTaskDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
this.tasksService.remove(id);
return { message: 'Task deleted successfully' };
}
@Patch(':id/toggle')
toggleComplete(@Param('id', ParseIntPipe) id: number) {
return this.tasksService.toggleComplete(id);
}
}Notice ParseIntPipe—it automatically converts the string parameter to an integer and validates it.
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { TasksController } from './tasks.controller';
@Module({
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}import { Module } from '@nestjs/common';
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [TasksModule],
})
export class AppModule {}Start the development server:
npm run start:devUse curl or a tool like Bruno/Postman to test:
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Learn NestJS","description":"Complete the tutorial"}'Services must be declared in the module's providers array and injected via constructor. Forgetting either step causes runtime errors.
@Module({
controllers: [TasksController],
// Missing TasksService in providers
})
export class TasksModule {}@Module({
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}Accepting any type defeats TypeScript's benefits. Always define DTOs for type safety and validation.
@Post()
create(@Body() data: any) {
return this.tasksService.create(data);
}@Post()
create(@Body() createTaskDto: CreateTaskDto) {
return this.tasksService.create(createTaskDto);
}Controllers should only handle HTTP concerns. Business logic belongs in services.
@Get()
findAll() {
const tasks = this.tasks.filter(t => !t.completed);
return tasks.sort((a, b) => a.id - b.id);
}// In service
findAllPending(): Task[] {
return this.tasks
.filter(t => !t.completed)
.sort((a, b) => a.id - b.id);
}
// In controller
@Get('pending')
findAllPending() {
return this.tasksService.findAllPending();
}Install class-validator and class-transformer:
npm install class-validator class-transformerUpdate your DTO with validation decorators:
import { IsString, IsOptional, MinLength } from 'class-validator';
export class CreateTaskDto {
@IsString()
@MinLength(3)
title: string;
@IsOptional()
@IsString()
description?: string;
}Enable global validation in main.ts:
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();Structure your project by features, not by type:
src/
├── tasks/
│ ├── dto/
│ ├── tasks.controller.ts
│ ├── tasks.service.ts
│ └── tasks.module.ts
├── users/
│ ├── dto/
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── users.module.ts
└── app.module.tsThis makes it easy to find related code and scale your application.
Use NestJS built-in exceptions:
import { NotFoundException, BadRequestException } from '@nestjs/common';
findOne(id: number): Task {
if (id <= 0) {
throw new BadRequestException('ID must be a positive number');
}
const task = this.tasks.find(t => t.id === id);
if (!task) {
throw new NotFoundException(`Task with ID ${id} not found`);
}
return task;
}NestJS comes with Jest pre-configured. Create tasks.service.spec.ts:
Run tests with:
npm run testNestJS is powerful but not always the right choice:
For most REST APIs, GraphQL servers, and backend services, NestJS is an excellent choice.
You now have a working task management API built with NestJS. You've learned about modules, controllers, services, dependency injection, and best practices. The architecture you've built scales from a small project to an enterprise application.
Next steps: explore NestJS documentation on databases (TypeORM, Prisma), authentication, and middleware. The patterns you've learned here apply to all NestJS projects.
Start building, and you'll quickly see why NestJS has become the framework of choice for serious Node.js backend development.