Пошук уроків, статей та іншого контенту
Створите контролери для операцій створення, читання, оновлення та видалення ресурсів.
CRUD — це чотири базові операції над ресурсом:
Create — створення;
Read — отримання;
Update — оновлення;
Delete — видалення.
У NestJS контролер приймає HTTP-запити та передає їх сервісу. Контролер не повинен містити всю бізнес-логіку — його завданням є:
визначити маршрут;
отримати параметри, тіло або query-параметри запиту;
викликати відповідний метод сервісу;
повернути результат клієнту.
Наприклад, для ресурсу products CRUD-маршрути можуть виглядати так:
| Операція | HTTP-метод | Маршрут | |---|---|---| | Створити товар | POST | /products | | Отримати всі товари | GET | /products | | Отримати один товар | GET | /products/:id | | Оновити товар | PATCH | /products/:id | | Видалити товар | DELETE | /products/:id |
Контролер позначається декоратором @Controller():
import { Controller } from '@nestjs/common';
@Controller('products')
export class ProductsController {}Рядок 'products' — це базовий шлях для всіх методів контролера. Наприклад, якщо в контролері буде метод із декоратором @Get(), повний маршрут буде GET /products.
Для обробки запитів NestJS надає декоратори:
@Body() — отримує тіло запиту;
@Param() — отримує параметри маршруту;
@Query() — отримує query-параметри;
@Req() — отримує весь об’єкт запиту.
Для CRUD-контролерів найчастіше достатньо @Body(), @Param() і @Query().
@Post()
create(@Body() data: CreateProductDto) {
return this.productsService.create(data);
}Якщо клієнт надішле JSON:
{
"name": "Keyboard",
"price": 2500
}об’єкт із цими даними буде доступний у параметрі data.
Для маршруту /products/:id значення id можна отримати так:
@Get(':id')
findOne(@Param('id') id: string) {
return this.productsService.findOne(Number(id));
}Усі значення з URL спочатку надходять як рядки. Тому id потрібно перетворити на число.
NestJS також має вбудований ParseIntPipe, який одразу перетворює значення на число:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}Якщо значення неможливо перетворити на число, NestJS автоматично поверне помилку HTTP 400 Bad Request.
DTO (Data Transfer Object) описує структуру даних, які контролер очікує від клієнта.
Для створення товару можна визначити такий DTO:
export class CreateProductDto {
name: string;
price: number;
}Для оновлення всі поля можуть бути необов’язковими:
export class UpdateProductDto {
name?: string;
price?: number;
}Окремий DTO для оновлення потрібен тому, що під час часткового оновлення клієнт може передати лише одне поле.
Нижче наведено самодостатній приклад модуля. Його можна розмістити у файлі src/app.module.ts у стандартному проєкті NestJS. Дані зберігаються в масиві в пам’яті, тому після перезапуску застосунку вони будуть втрачені.
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Injectable,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Module,
} from '@nestjs/common';
export class CreateProductDto {
name: string;
price: number;
}
export class UpdateProductDto {
name?: string;
price?: number;
}
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable()
export class ProductsService {
private products: Product[] = [
{
id: 1,
name: 'Keyboard',
price: 2500,
},
{
id: 2,
name: 'Mouse',
price: 1200,
},
];
private nextId = 3;
create(data: CreateProductDto): Product {
const product: Product = {
id: this.nextId++,
name: data.name,
price: data.price,
};
this.products.push(product);
return product;
}
findAll(): Product[] {
return this.products;
}
findOne(id: number): Product {
const product = this.products.find((item) => item.id === id);
if (!product) {
throw new NotFoundException(`Товар з id ${id} не знайдено`);
}
return product;
}
update(id: number, data: UpdateProductDto): Product {
const product = this.findOne(id);
if (data.name !== undefined) {
product.name = data.name;
}
if (data.price !== undefined) {
product.price = data.price;
}
return product;
}
remove(id: number): void {
const productIndex = this.products.findIndex((item) => item.id === id);
if (productIndex === -1) {
throw new NotFoundException(`Товар з id ${id} не знайдено`);
}
this.products.splice(productIndex, 1);
}
}
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Post()
create(@Body() data: CreateProductDto): Product {
return this.productsService.create(data);
}
@Get()
findAll(): Product[] {
return this.productsService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Product {
return this.productsService.findOne(id);
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() data: UpdateProductDto,
): Product {
return this.productsService.update(id, data);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number): void {
this.productsService.remove(id);
}
}
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class AppModule {}У реальному проєкті DTO, сервіс і контролер зазвичай розміщують в окремих файлах. Для навчального прикладу вони об’єднані в один файл, щоб було видно повний зв’язок між компонентами.
@Post()
create(@Body() data: CreateProductDto): Product {
return this.productsService.create(data);
}Декоратор @Post() створює маршрут POST /products.
Тіло запиту:
{
"name": "Monitor",
"price": 8500
}Приклад запиту через curl:
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"name":"Monitor","price":8500}'За замовчуванням NestJS поверне статус 201 Created для POST-методу.
@Get()
findAll(): Product[] {
return this.productsService.findAll();
}Цей метод обробляє запит GET /products.
curl http://localhost:3000/productsРезультатом буде масив товарів:
[
{
"id": 1,
"name": "Keyboard",
"price": 2500
},
{
"id": 2,
"name": "Mouse",
"price": 1200
}
]@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Product {
return this.productsService.findOne(id);
}Метод обробляє запит GET /products/1.
curl http://localhost:3000/products/1ParseIntPipe перетворює значення id на число. Якщо товару з таким ідентифікатором немає, сервіс викидає NotFoundException. NestJS перетворить цей виняток на відповідь зі статусом 404 Not Found.
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() data: UpdateProductDto,
): Product {
return this.productsService.update(id, data);
}Метод обробляє запит PATCH /products/:id.
Наприклад, можна змінити лише ціну:
curl -X PATCH http://localhost:3000/products/1 \
-H "Content-Type: application/json" \
-d '{"price":2800}'Оскільки використовується UpdateProductDto, передавати всі поля товару необов’язково.
Сервіс спочатку знаходить товар, а потім змінює лише ті поля, які були передані:
if (data.name !== undefined) {
product.name = data.name;
}
if (data.price !== undefined) {
product.price = data.price;
}@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number): void {
this.productsService.remove(id);
}Метод обробляє запит DELETE /products/:id.
curl -X DELETE http://localhost:3000/products/2Декоратор @HttpCode(HttpStatus.NO_CONTENT) встановлює статус 204 No Content. Така відповідь не містить тіла.
Якщо ресурс не знайдено, сервіс викине NotFoundException, і клієнт отримає статус 404.
Контролер і сервіс потрібно зареєструвати в модулі:
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class AppModule {}controllers містить контролери, які обробляють HTTP-запити;
providers містить сервіси, які NestJS створює та передає через dependency injection.
Контролер отримує сервіс через конструктор:
constructor(private readonly productsService: ProductsService) {}NestJS автоматично створює екземпляр ProductsService, оскільки він доданий до providers.
Сервіс повинен повідомляти контролер або NestJS про ситуації, коли ресурс не знайдено:
throw new NotFoundException(`Товар з id ${id} не знайдено`);NestJS перетворює цей виняток на HTTP-відповідь:
{
"statusCode": 404,
"message": "Товар з id 99 не знайдено",
"error": "Not Found"
}Контролеру не потрібно вручну перевіряти кожен результат сервісу. Він лише викликає метод сервісу, а винятки обробляються фреймворком.
Статичні маршрути бажано розміщувати перед динамічними маршрутами:
@Get()
findAll() {
// ...
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// ...
}У цьому прикладі GET /products обробляє findAll, а GET /products/1 — findOne.
Для складніших маршрутів варто спочатку оголошувати конкретні шляхи, а потім маршрути з параметрами:
@Get('popular')
findPopular() {
// ...
}
@Get(':id')
findOne(@Param('id') id: string) {
// ...
}Так маршрут /products/popular однозначно розглядається як конкретний маршрут, а не як значення параметра id.
@Controller('products')У такому випадку метод @Get() відповідає маршруту /products, а не /.
idЗначення параметрів маршруту надходять як рядки:
@Get(':id')
findOne(@Param('id') id: string) {
return this.productsService.findOne(id);
}Якщо сервіс очікує число, це може призвести до некоректного порівняння. Використовуйте ParseIntPipe:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}Якщо ProductsService не доданий до providers, NestJS не зможе вставити його в контролер:
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class AppModule {}Контролер не повинен сам шукати, змінювати або видаляти елементи масиву. Цю логіку краще розміщувати в сервісі:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}Так контролер залишається простим, а сервіс можна буде повторно використати в інших частинах застосунку.
@Body()Якщо метод має отримувати JSON із запиту, потрібно явно використати @Body():
@Post()
create(@Body() data: CreateProductDto) {
return this.productsService.create(data);
}Без цього тіло запиту не буде передане в параметр методу.
CRUD-контролер описує HTTP-маршрути для створення, читання, оновлення та видалення ресурсу.
@Controller('products') задає спільний префікс маршрутів.
@Post(), @Get(), @Patch() і @Delete() відповідають HTTP-операціям.
@Body() використовується для отримання тіла запиту.
@Param() використовується для отримання параметрів маршруту.
ParseIntPipe перетворює ідентифікатор із рядка на число та перевіряє його формат.
Контролер передає роботу сервісу через dependency injection.
NotFoundException використовується, якщо ресурс не знайдено.
@HttpCode(HttpStatus.NO_CONTENT) дає змогу повернути статус 204 після успішного видалення.