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).
