We use cookies to enhance your experience on the site
CodeWorlds

First REST API - CRUD operations

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
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}

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/1 - get one by ID
11  @Get(':id')
12  findOne(@Param('id') id: string): Legionary {
13    const legionary = this.legionaries.find(l => l.id === Number(id));
14    if (!legionary) {
15      throw new Error('Legionary not found!');
16    }
17    return legionary;
18  }
19
20  // GET /legionaries?province=Roma - filter by province
21  @Get('search')
22  findByProvince(@Query('province') province: string): Legionary[] {
23    return this.legionaries.filter(l => l.province === province);
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)

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.legionaries.length + 1,
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.

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 Error('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 Error('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 Error('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
6  findAll(): Legionary[] {
7    return this.legionaries;
8  }
9
10  findOne(id: number): Legionary {
11    return this.legionaries.find(l => l.id === id);
12  }
13
14  create(data: { name: string; rank: string; province: string }): Legionary {
15    const legionary = { id: this.legionaries.length + 1, ...data };
16    this.legionaries.push(legionary);
17    return legionary;
18  }
19
20  update(id: number, data: Partial<Legionary>): Legionary {
21    const index = this.legionaries.findIndex(l => l.id === id);
22    this.legionaries[index] = { ...this.legionaries[index], ...data };
23    return this.legionaries[index];
24  }
25
26  remove(id: number): void {
27    this.legionaries = this.legionaries.filter(l => l.id !== id);
28  }
29}
30
31// legionaries.controller.ts
32@Controller('legionaries')
33export class LegionariesController {
34  constructor(private readonly legionariesService: LegionariesService) {}
35
36  @Get()
37  findAll() {
38    return this.legionariesService.findAll();
39  }
40
41  @Post()
42  create(@Body() data: { name: string; rank: string; province: string }) {
43    return this.legionariesService.create(data);
44  }
45}

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!

Go to CodeWorlds