NestJS course Β· Module 1: NestJS Basics
First REST API - CRUD operations
In this lesson9
Ave, road builder! Consul Caesar.js entrusts you with one of the most important tasks in the Empire - building the REST API road system. Just as Roman roads connected all provinces of the Empire, REST API connects our server application with the outside world. Time to learn CRUD operations - the foundation of every REST API!
What is CRUD?
CRUD is an acronym for four basic data operations:
- Create - recruiting new legionaries
- Read - browsing Empire registries
- Update - promoting legionaries
- Delete - removing from registries
Each CRUD operation corresponds to a specific HTTP method and NestJS decorator:
| Operation | HTTP Method | NestJS Decorator |
|---|---|---|
| Create | POST | @Post() |
| Read | GET | @Get() |
| Update | PUT / PATCH | @Put() / @Patch() |
| Delete | DELETE | @Delete() |
Building a CRUD controller step by step
Let's build a complete controller for managing legionaries of our Empire:
1import {
2 Controller, Get, Post, Put, Delete, Patch,
3 Param, Body, Query, HttpCode, HttpStatus, NotFoundException
4} from '@nestjs/common';
5
6// Legionary interface
7interface Legionary {
8 id: number;
9 name: string;
10 rank: string;
11 province: string;
12}
13
14@Controller('legionaries')
15export class LegionariesController {
16 // Temporary array as a "database"
17 private legionaries: Legionary[] = [
18 { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', province: 'Roma' },
19 { id: 2, name: 'Gaius Julius', rank: 'Miles', province: 'Gallia' },
20 ];
21 // Next free number (1 and 2 are taken)
22 private nextId = 3;
23}READ - reading data (@Get)
The @Get() decorator handles HTTP GET requests. We use it to retrieve data:
1@Controller('legionaries')
2export class LegionariesController {
3
4 // GET /legionaries - get all legionaries
5 @Get()
6 findAll(): Legionary[] {
7 return this.legionaries;
8 }
9
10 // GET /legionaries/search?province=Roma - filter by province
11 @Get('search')
12 findByProvince(@Query('province') province: string): Legionary[] {
13 return this.legionaries.filter(l => l.province === province);
14 }
15
16 // GET /legionaries/1 - get one by ID
17 @Get(':id')
18 findOne(@Param('id') id: string): Legionary {
19 const legionary = this.legionaries.find(l => l.id === Number(id));
20 if (!legionary) {
21 throw new NotFoundException('Legionary not found!');
22 }
23 return legionary;
24 }
25}Note three key parameter decorators:
@Param('id')- extracts a parameter from the URL (e.g.,/legionaries/1)@Query('province')- extracts a query parameter (e.g.,?province=Roma)@Body()- extracts data from the request body (for POST/PUT)
Two things in this code are deliberate. The search route stands before :id - otherwise GET /legionaries/search would reach findOne with id equal to 'search'. And a missing legionary is reported with the NotFoundException exception from @nestjs/common: NestJS then responds with 404 Not Found. A plain throw new Error(...) would give the client 500 Internal Server Error, as if the server had broken.
CREATE - creating data (@Post)
The @Post() decorator handles HTTP POST requests. It is used to create new resources:
1// POST /legionaries - recruit a new legionary
2@Post()
3@HttpCode(HttpStatus.CREATED) // Returns status 201
4create(@Body() newLegionary: { name: string; rank: string; province: string }): Legionary {
5 const legionary: Legionary = {
6 id: this.nextId++,
7 ...newLegionary,
8 };
9 this.legionaries.push(legionary);
10 return legionary;
11}The @Body() decorator extracts data from the JSON request body. The @HttpCode() decorator allows you to set the HTTP response code - for resource creation the standard is 201 Created.
The new legionary's number comes from the nextId counter in the controller class. It grows with every recruitment, so it never repeats, even after someone is struck off the array - a number computed as length + 1 could already belong to another legionary.
UPDATE - updating data (@Put and @Patch)
We have two decorators for updating:
@Put()- replaces the entire resource (full update)@Patch()- updates only selected fields (partial update)
1// PUT /legionaries/1 - full update of a legionary
2@Put(':id')
3update(
4 @Param('id') id: string,
5 @Body() updateData: { name: string; rank: string; province: string },
6): Legionary {
7 const index = this.legionaries.findIndex(l => l.id === Number(id));
8 if (index === -1) {
9 throw new NotFoundException('Legionary not found!');
10 }
11 this.legionaries[index] = { id: Number(id), ...updateData };
12 return this.legionaries[index];
13}
14
15// PATCH /legionaries/1 - partial update (e.g., rank only)
16@Patch(':id')
17partialUpdate(
18 @Param('id') id: string,
19 @Body() updateData: Partial<Legionary>,
20): Legionary {
21 const index = this.legionaries.findIndex(l => l.id === Number(id));
22 if (index === -1) {
23 throw new NotFoundException('Legionary not found!');
24 }
25 this.legionaries[index] = { ...this.legionaries[index], ...updateData };
26 return this.legionaries[index];
27}DELETE - deleting data (@Delete)
The @Delete() decorator handles HTTP DELETE requests:
1// DELETE /legionaries/1 - remove a legionary
2@Delete(':id')
3@HttpCode(HttpStatus.NO_CONTENT) // Returns status 204
4remove(@Param('id') id: string): void {
5 const index = this.legionaries.findIndex(l => l.id === Number(id));
6 if (index === -1) {
7 throw new NotFoundException('Legionary not found!');
8 }
9 this.legionaries.splice(index, 1);
10}For delete operations, the standard response code is 204 No Content - it means success without returning data.
HTTP Status Codes
Knowing HTTP codes is a duty of every API builder:
| Code | Name | When to use |
|---|---|---|
| 200 | OK | Success (default for GET, PUT, PATCH) |
| 201 | Created | A new resource was created (POST) |
| 204 | No Content | Success without response body (DELETE) |
| 400 | Bad Request | Invalid input data |
| 404 | Not Found | Resource does not exist |
| 500 | Internal Server Error | Server error |
Controller + Service - the proper pattern
In a real Empire, the controller does not execute logic itself - it delegates it to a service:
1// legionaries.service.ts
2@Injectable()
3export class LegionariesService {
4 private legionaries: Legionary[] = [];
5 private nextId = 1;
6
7 findAll(): Legionary[] {
8 return this.legionaries;
9 }
10
11 findOne(id: number): Legionary {
12 return this.legionaries.find(l => l.id === id);
13 }
14
15 create(data: { name: string; rank: string; province: string }): Legionary {
16 const legionary = { id: this.nextId++, ...data };
17 this.legionaries.push(legionary);
18 return legionary;
19 }
20
21 update(id: number, data: Partial<Legionary>): Legionary {
22 const index = this.legionaries.findIndex(l => l.id === id);
23 this.legionaries[index] = { ...this.legionaries[index], ...data };
24 return this.legionaries[index];
25 }
26
27 remove(id: number): void {
28 this.legionaries = this.legionaries.filter(l => l.id !== id);
29 }
30}
31
32// legionaries.controller.ts
33@Controller('legionaries')
34export class LegionariesController {
35 constructor(private readonly legionariesService: LegionariesService) {}
36
37 @Get()
38 findAll() {
39 return this.legionariesService.findAll();
40 }
41
42 @Post()
43 create(@Body() data: { name: string; rank: string; province: string }) {
44 return this.legionariesService.create(data);
45 }
46}Testing with curl
You can test your API using the curl tool in the terminal:
1# GET - get all
2curl http://localhost:3000/legionaries
3
4# POST - create new
5curl -X POST http://localhost:3000/legionaries \
6 -H "Content-Type: application/json" \
7 -d '{"name": "Titus Flavius", "rank": "Miles", "province": "Judea"}'
8
9# PUT - full update
10curl -X PUT http://localhost:3000/legionaries/1 \
11 -H "Content-Type: application/json" \
12 -d '{"name": "Marcus Aurelius", "rank": "Legatus", "province": "Roma"}'
13
14# DELETE - remove
15curl -X DELETE http://localhost:3000/legionaries/1Well done, builder! Now you know the fundamentals of REST API - the roads that connect all provinces of our NestJS Empire!
Code for this lesson: src/crud-controller.ts
1// First REST API - CRUD Operations in the Imperium
2import {
3 Controller, Get, Post, Put, Delete, Patch,
4 Param, Body, Query, HttpCode, HttpStatus, Injectable
5} from '@nestjs/common';
6
7console.log("REST API CRUD - roads of the imperium!");
8
9// ===========================================
10// 1. Interface and data
11// ===========================================
12
13interface Legionary {
14 id: number;
15 name: string;
16 rank: string;
17 province: string;
18}
19
20const legionaries: Legionary[] = [
21 { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', province: 'Roma' },
22 { id: 2, name: 'Gaius Julius', rank: 'Miles', province: 'Gallia' },
23 { id: 3, name: 'Titus Flavius', rank: 'Optio', province: 'Judea' },
24];
25
26// ===========================================
27// 2. READ - @Get() - fetching data
28// ===========================================
29
30// GET /legionaries - list all
31// @Get()
32function findAll(): Legionary[] {
33 return legionaries;
34}
35
36// GET /legionaries/:id - one by ID
37// @Get(':id')
38function findOne(id: string): Legionary | undefined {
39 return legionaries.find(l => l.id === Number(id));
40}
41
42// GET /legionaries?province=Roma - filtering
43// @Get('search')
44function findByProvince(province: string): Legionary[] {
45 return legionaries.filter(l => l.province === province);
46}
47
48console.log("GET /legionaries:", findAll());
49console.log("GET /legionaries/1:", findOne('1'));
50console.log("GET ?province=Roma:", findByProvince('Roma'));
51
52// ===========================================
53// 3. CREATE - @Post() - creating data
54// ===========================================
55
56// POST /legionaries
57function create(data: { name: string; rank: string; province: string }): Legionary {
58 const newLegionary: Legionary = {
59 id: legionaries.length + 1,
60 ...data,
61 };
62 legionaries.push(newLegionary);
63 return newLegionary;
64}
65
66const created = create({ name: 'Lucius Verus', rank: 'Miles', province: 'Syria' });
67console.log("\nPOST - new legionary:", created);
68
69// ===========================================
70// 4. UPDATE - @Put() / @Patch()
71// ===========================================
72
73// PUT /legionaries/:id - full update
74function update(id: string, data: Omit<Legionary, 'id'>): Legionary | null {
75 const index = legionaries.findIndex(l => l.id === Number(id));
76 if (index === -1) return null;
77 legionaries[index] = { id: Number(id), ...data };
78 return legionaries[index];
79}
80
81// PATCH /legionaries/:id - partial update
82function partialUpdate(id: string, data: Partial<Legionary>): Legionary | null {
83 const index = legionaries.findIndex(l => l.id === Number(id));
84 if (index === -1) return null;
85 legionaries[index] = { ...legionaries[index], ...data };
86 return legionaries[index];
87}
88
89console.log("\nPUT /legionaries/2:", update('2', { name: 'Gaius Julius', rank: 'Centurio', province: 'Roma' }));
90console.log("PATCH /legionaries/3:", partialUpdate('3', { rank: 'Centurio' }));
91
92// ===========================================
93// 5. DELETE - @Delete() - deleting data
94// ===========================================
95
96function remove(id: string): boolean {
97 const index = legionaries.findIndex(l => l.id === Number(id));
98 if (index === -1) return false;
99 legionaries.splice(index, 1);
100 return true;
101}
102
103console.log("\nDELETE /legionaries/4:", remove('4'));
104console.log("State after operations:", legionaries);
105
106// ===========================================
107// 6. HTTP status codes
108// ===========================================
109
110console.log("\n=== HTTP STATUS CODES ===");
111console.log("200 OK - success (GET, PUT, PATCH)");
112console.log("201 Created - resource created (POST)");
113console.log("204 No Content - success without body (DELETE)");
114console.log("400 Bad Request - invalid data");
115console.log("404 Not Found - resource does not exist");
116
117console.log("\n=== CRUD DECORATORS ===");
118console.log("@Get() -> read data");
119console.log("@Post() -> create data");
120console.log("@Put() -> full update");
121console.log("@Patch() -> partial update");
122console.log("@Delete() -> delete data");
123console.log("@Param() -> parameter from URL");
124console.log("@Body() -> data from request body");
125console.log("@Query() -> query parameter");
126Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the CRUD acronym stand for?
2. Which response code does NestJS return by default from a method marked with the @Post() decorator?
3. What is the @Param() decorator used for in a NestJS controller?
Hands-on tasks in the game
- Code editor
In a LegionController with the @Controller('legionaries') decorator, keep an array of legionaries, e.g. [{ id: 1, name: 'Marcus Aurelius' }]. Write a findOne method with @Get(':id') that reads the id through @Param('id') and returns the legionary with that id, and throws NotFoundException when there is none. Remember that the id from the URL is a string.
- Horizontal ordering
Arrange the elements of the POST method in a NestJS controller:
- Vertical ordering
Arrange HTTP status codes from success to server error:
- Code editor
In a LegionController with the @Controller('legionaries') decorator and an array of legionaries, write a remove method with @Delete(':id') and @HttpCode(HttpStatus.NO_CONTENT) that reads the id through @Param('id') and removes the legionary with that id from the array. The method returns nothing.