NestJS course Β· Module 1: NestJS Basics

First REST API - CRUD operations

6 min read
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:

OperationHTTP MethodNestJS Decorator
CreatePOST@Post()
ReadGET@Get()
UpdatePUT / PATCH@Put() / @Patch()
DeleteDELETE@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:

CodeNameWhen to use
200OKSuccess (default for GET, PUT, PATCH)
201CreatedA new resource was created (POST)
204No ContentSuccess without response body (DELETE)
400Bad RequestInvalid input data
404Not FoundResource does not exist
500Internal Server ErrorServer 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/1

Well 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");
126

Spotted 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. 1. What does the CRUD acronym stand for?

  2. 2. Which response code does NestJS return by default from a method marked with the @Post() decorator?

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

Useful articles