Skip to content
← Back to blog
NestJSREST APITypeScriptTutorialCRUD

NestJS REST API Tutorial: Build Your First CRUD App in 30 Minutes

Firas Sayah·May 20, 2026·7 min read

I still remember my first NestJS project. I had spent years wrestling with Express apps that turned into spaghetti the moment they grew past a few routes. The day I scaffolded my first NestJS app and saw how modules, services, and controllers just clicked together, I knew I was never going back. If you have been fighting unstructured Node.js backends, this tutorial is your escape hatch.

NestJS REST API Development

What You Will Build

By the end of this tutorial, you will have a fully functional REST API for managing tasks. It will support creating, reading, updating, and deleting tasks, with input validation, proper error handling, and a clean project structure that scales.

No prior NestJS experience is required. If you know basic TypeScript, you are ready.

Prerequisites

Make sure you have Node.js 18 or higher installed. You can check with:

node --version

You will also need the NestJS CLI:

npm install -g @nestjs/cli

Cloudrix SaaS Starter ships with this pre-configured and tested.

Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.

Try the live demo →

Step 1: Scaffold a New Project

NestJS provides a CLI that generates a well-structured project for you:

nest new task-api
cd task-api

Choose npm or yarn when prompted. The CLI creates a project with a module, controller, and service already wired together. Start the development server to confirm everything works:

npm run start:dev

Visit http://localhost:3000 and you should see "Hello World!". Your API is running.

Pro Tip: If you have used Express before, notice what just happened -- NestJS gave you a structured project with dependency injection, a module system, and hot reload out of the box. With Express, you would be configuring all of this manually.

Step 2: Generate the Tasks Module

NestJS organizes code into modules. Each feature gets its own module, controller, and service. Generate all three at once:

nest generate module tasks
nest generate controller tasks --no-spec
nest generate service tasks --no-spec

This creates src/tasks/ with three files. The module automatically registers the controller and service. NestJS uses dependency injection, so the controller can use the service without manual wiring.

Step 3: Define the Task Model

Create a file src/tasks/task.entity.ts to define what a task looks like:

export enum TaskStatus {
  OPEN = 'OPEN',
  IN_PROGRESS = 'IN_PROGRESS',
  DONE = 'DONE',
}

export interface Task {
  id: string;
  title: string;
  description: string;
  status: TaskStatus;
  createdAt: Date;
}

Using an enum for status prevents invalid values and gives you autocomplete in your editor.

Step 4: Build the Service Layer

The service handles business logic. Open src/tasks/tasks.service.ts:

import { Injectable, NotFoundException } from '@nestjs/common';
import { Task, TaskStatus } from './task.entity';
import { v4 as uuid } from 'uuid';

@Injectable()
export class TasksService {
  private tasks: Task[] = [];

  findAll(): Task[] {
    return this.tasks;
  }

  findOne(id: string): Task {
    const task = this.tasks.find((t) => t.id === id);
    if (!task) {
      throw new NotFoundException(`Task with ID "${id}" not found`);
    }
    return task;
  }

  create(title: string, description: string): Task {
    const task: Task = {
      id: uuid(),
      title,
      description,
      status: TaskStatus.OPEN,
      createdAt: new Date(),
    };
    this.tasks.push(task);
    return task;
  }

  update(id: string, status: TaskStatus): Task {
    const task = this.findOne(id);
    task.status = status;
    return task;
  }

  remove(id: string): void {
    const task = this.findOne(id);
    this.tasks = this.tasks.filter((t) => t.id !== task.id);
  }
}

Notice how NotFoundException is a built-in NestJS exception. It automatically returns a 404 response with a proper error message. No manual response formatting needed.

Clean code architecture

Step 5: Add Input Validation with DTOs

DTOs (Data Transfer Objects) define the shape of incoming data. Install the validation packages:

npm install class-validator class-transformer

Create src/tasks/dto/create-task.dto.ts:

import { IsNotEmpty, IsString, MinLength } from 'class-validator';

export class CreateTaskDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(3)
  title: string;

  @IsString()
  @IsNotEmpty()
  description: string;
}

Create src/tasks/dto/update-task.dto.ts:

import { IsEnum } from 'class-validator';
import { TaskStatus } from '../task.entity';

export class UpdateTaskDto {
  @IsEnum(TaskStatus)
  status: TaskStatus;
}

Enable validation globally in src/main.ts:

import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
  await app.listen(3000);
}
bootstrap();

The whitelist: true option strips any properties that are not in the DTO. This prevents clients from sending unexpected fields.

Pro Tip: The whitelist option is one of those things you should always enable in production. Without it, a malicious client could send extra fields like isAdmin: true and, depending on your ORM setup, those could end up in your database. I have seen this happen in production -- do not let it happen to you.

Step 6: Wire Up the Controller

The controller maps HTTP requests to service methods. Open src/tasks/tasks.controller.ts:

import {
  Controller, Get, Post, Patch, Delete,
  Param, Body, HttpCode, HttpStatus,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';

@Controller('tasks')
export class TasksController {
  constructor(private readonly tasksService: TasksService) {}

  @Get()
  findAll() {
    return this.tasksService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.tasksService.findOne(id);
  }

  @Post()
  create(@Body() dto: CreateTaskDto) {
    return this.tasksService.create(dto.title, dto.description);
  }

  @Patch(':id/status')
  update(@Param('id') id: string, @Body() dto: UpdateTaskDto) {
    return this.tasksService.update(id, dto.status);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    this.tasksService.remove(id);
  }
}

Each decorator maps directly to an HTTP method and route. The @Body() and @Param() decorators extract data from the request automatically.

Ready to skip the setup? SaaS Starter includes all of this pre-built and tested -- CRUD operations, validation, error handling, plus authentication, Stripe billing, and multi-tenancy. View the live demo or get the free lite version.

Step 7: Test Your API

With the server running (npm run start:dev), test each endpoint:

# Create a task
curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Learn NestJS", "description": "Complete the CRUD tutorial"}'

# Get all tasks
curl http://localhost:3000/tasks

# Get a single task (replace the ID)
curl http://localhost:3000/tasks/YOUR_TASK_ID

# Update status
curl -X PATCH http://localhost:3000/tasks/YOUR_TASK_ID/status \
  -H "Content-Type: application/json" \
  -d '{"status": "IN_PROGRESS"}'

# Delete a task
curl -X DELETE http://localhost:3000/tasks/YOUR_TASK_ID

Try sending invalid data to see validation in action:

curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "ab"}'

You will get a 400 response listing exactly which validations failed.

What to Add Next

This tutorial uses an in-memory array for storage. In a real application, you would add:

  • A database with TypeORM or Prisma for persistent storage -- see our multi-tenancy architecture guide for how to set this up with tenant isolation
  • Authentication with JWT tokens and guards -- we cover this in depth in our authentication feature
  • Pagination for large result sets
  • Swagger documentation using @nestjs/swagger
  • Rate limiting to prevent abuse
  • Testing -- our NestJS testing guide walks through unit, integration, and E2E tests with Jest

Each of these is a separate NestJS module that plugs into your existing structure without rewriting anything.

Skip the Boilerplate

I have personally built CRUD APIs from scratch more times than I care to admit. Every single time, I ended up rebuilding the same authentication layer, the same validation patterns, the same error handling. That is exactly why we built SaaS Starter.

If you want all of this already built, configured, and production-ready, the Cloudrix SaaS Starter ships with a complete NestJS backend that includes CRUD operations, authentication, role-based access control, Stripe billing, multi-tenancy, and full test coverage. Instead of spending weeks building infrastructure, you can focus on your actual product from day one.

The free lite version lets you explore the full architecture with zero commitment. The pro version (a one-time $199 purchase) adds Terraform deployment, advanced RBAC, and 55+ test files.

Try the live demo | Compare with ShipFast and Supastarter

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.