NestJS for Beginners: Build Your First REST API Step-By-Step

Posted November 17, 2025 by Karol Polakowski

NestJS provides a structured, opinionated way to build scalable server-side applications using TypeScript. In this guide you’ll create a small CRUD REST API with an in-memory store, learn core Nest concepts (modules, controllers, services, DTOs), and run and test your API locally.


Why choose NestJS

NestJS combines the best of modern Node.js patterns with a familiar, Angular-like architecture. Key benefits:

  • Built with TypeScript and supports decorators, dependency injection, and modular design.
  • Scales well from small APIs to large microservices architectures.
  • Integrates easily with validation, authentication, ORMs, and testing tools.

Prerequisites

  • Node.js 16+ and npm/yarn installed
  • Basic familiarity with TypeScript and REST concepts

Create a new project

Install the Nest CLI and scaffold a project:

npm install -g @nestjs/cli
nest new nestjs-first-api
cd nestjs-first-api

Choose npm or yarn when prompted. After scaffolding, open the project in your editor.

Project structure overview

A typical Nest project has:

  • src/main.ts — bootstrap file
  • src/app.module.ts — root module
  • feature modules under src/ (e.g., src/items)

We’ll create an Items module that provides CRUD endpoints.

Generate an Items resource (optional)

You can use the CLI to scaffold files, then replace with our simplified code:

nest generate resource items --no-spec

Implement a simple in-memory CRUD

We’ll keep persistence in-memory for simplicity. Create these files under src/items.

src/items/dto/create-item.dto.ts

export class CreateItemDto {
  name: string;
  description?: string;
}

src/items/dto/update-item.dto.ts

export class UpdateItemDto {
  name?: string;
  description?: string;
}

src/items/items.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';

@Injectable()
export class ItemsService {
  private items: Record<string, any> = {};
  private idCounter = 0;

  create(createDto: CreateItemDto) {
    const id = String(++this.idCounter);
    const item = { id, ...createDto };
    this.items[id] = item;
    return item;
  }

  findAll() {
    return Object.values(this.items);
  }

  findOne(id: string) {
    const item = this.items[id];
    if (!item) throw new NotFoundException('Item not found');
    return item;
  }

  update(id: string, updateDto: UpdateItemDto) {
    const item = this.items[id];
    if (!item) throw new NotFoundException('Item not found');
    const updated = { ...item, ...updateDto };
    this.items[id] = updated;
    return updated;
  }

  remove(id: string) {
    const item = this.items[id];
    if (!item) throw new NotFoundException('Item not found');
    delete this.items[id];
    return { deleted: true };
  }
}

Note: the service exposes simple synchronous methods for clarity. In a real app you’d usually use async methods communicating with a database.

src/items/items.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  Put,
  Delete,
} from '@nestjs/common';
import { ItemsService } from './items.service';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Post()
  create(@Body() createDto: CreateItemDto) {
    return this.itemsService.create(createDto);
  }

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

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

  @Put(':id')
  update(@Param('id') id: string, @Body() updateDto: UpdateItemDto) {
    return this.itemsService.update(id, updateDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.itemsService.remove(id);
  }
}

src/items/items.module.ts

import { Module } from '@nestjs/common';
import { ItemsController } from './items.controller';
import { ItemsService } from './items.service';

@Module({
  controllers: \[ItemsController\],
  providers: \[ItemsService\],
})
export class ItemsModule {}

(Notice that in code examples the array characters are escaped.)

Wire the module into the app

Edit src/app.module.ts to import ItemsModule:

import { Module } from '@nestjs/common';
import { ItemsModule } from './items/items.module';

@Module({
  imports: \[ItemsModule\],
})
export class AppModule {}

Run the API

Start the app:

npm run start:dev

Nest defaults to port 3000. You should see logging that the server is listening.

Test the endpoints with curl

Create an item:

curl -s -X POST http://localhost:3000/items \
  -H 'Content-Type: application/json' \
  -d '{"name":"First item","description":"A sample"}'

Response (example):

{ "id": "1", "name": "First item", "description": "A sample" }

Get all items:

curl -s http://localhost:3000/items

Response (example):

\[ { "id": "1", "name": "First item", "description": "A sample" } \]

Get one item:

curl -s http://localhost:3000/items/1

Update an item:

curl -s -X PUT http://localhost:3000/items/1 \
  -H 'Content-Type: application/json' \
  -d '{"description":"Updated description"}'

Delete an item:

curl -s -X DELETE http://localhost:3000/items/1

Add validation (optional but recommended)

Install class-validator and class-transformer:

npm install class-validator class-transformer

Enable validation in src/main.ts:

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, forbidNonWhitelisted: true }));
  await app.listen(3000);
}
bootstrap();

Annotate DTOs with validators:

import { IsString, IsOptional } from 'class-validator';

export class CreateItemDto {
  @IsString()
  name: string;

  @IsOptional()
  @IsString()
  description?: string;
}

With validation enabled, malformed requests will return 400 responses with a clear error payload.

Next steps

  • Replace the in-memory store with a real database (TypeORM, Prisma, or Mongoose). Note: ORM types often use generics that include angle brackets; when adapting, escape or format examples appropriately in docs.
  • Add request logging, authentication (JWT/Passport), and proper error handling.
  • Write unit and e2e tests with Jest.
  • Containerize with Docker and add CI/CD.

Conclusion

This article walked through creating a minimal REST API with NestJS, covering modules, controllers, services, DTOs, and validation. Nest’s structure helps maintain clarity as your app grows—start with small modules like this and iterate toward production-ready features (database, auth, tests).