Пошук уроків, статей та іншого контенту
Визначите ресурси, маршрути й HTTP-операції для послідовного та передбачуваного REST API.
REST API організовує HTTP-запити навколо ресурсів. Ресурсом може бути:
користувач;
завдання;
товар;
замовлення;
коментар.
Кожен ресурс має URL, а операція над ним визначається HTTP-методом.
Наприклад, для ресурсу tasks можна визначити такі маршрути:
GET /tasks — отримати список завдань;
GET /tasks/:id — отримати одне завдання;
POST /tasks — створити завдання;
PATCH /tasks/:id — частково оновити завдання;
DELETE /tasks/:id — видалити завдання.
Такий підхід робить API передбачуваним: клієнту не потрібно запам’ятовувати окремі назви на кшталт /getTasks або /createTask.
Перед створенням контролерів варто визначити основні ресурси системи.
Наприклад, для застосунку керування завданнями ресурсами можуть бути:
tasks — завдання;
users — користувачі;
projectsРесурс у маршруті зазвичай записують іменником у множині:
/tasks
/users
/projectsМножина підкреслює, що маршрут представляє колекцію об’єктів. Конкретний об’єкт цієї колекції позначають ідентифікатором:
/tasks/42
/users/7
/projects/3Використовуйте послідовні правила:
іменники замість дієслів;
множину для колекцій;
ідентифікатор після назви ресурсу;
однаковий стиль для всіх ресурсів.
Добре:
GET /tasks
POST /tasks
GET /tasks/10
PATCH /tasks/10
DELETE /tasks/10Гірше:
GET /getTasks
POST /createTask
POST /tasks/delete/10HTTP-метод уже описує дію, тому додавати дієслово до URL зазвичай не потрібно.
GET використовують для отримання даних.
GET /tasksПовертає колекцію завдань:
[
{
"id": 1,
"title": "Вивчити NestJS",
"completed": false
}
]Запит до конкретного ресурсу:
GET /tasks/1повертає один об’єкт:
{
"id": 1,
"title": "Вивчити NestJS",
"completed": false
}GET не повинен змінювати дані на сервері.
POST використовують для створення нового ресурсу.
POST /tasksТіло запиту:
{
"title": "Створити REST API"
}Успішне створення зазвичай повертає статус 201 Created і створений ресурс.
PUT призначений для повної заміни ресурсу.
PUT /tasks/1Клієнт передає повний стан ресурсу:
{
"title": "Оновлена назва",
"completed": true
}Якщо API не підтримує повну заміну, не потрібно використовувати PUT лише як іншу назву для часткового оновлення.
PATCH використовують для часткового оновлення.
PATCH /tasks/1Можна передати лише поле, яке потрібно змінити:
{
"completed": true
}Інші властивості ресурсу мають залишитися без змін.
DELETE видаляє ресурс:
DELETE /tasks/1Якщо видалення виконано успішно і сервер не повертає тіло відповіді, доречним є статус 204 No Content.
У NestJS маршрути описують у контролерах за допомогою декораторів.
@Controller('tasks') задає спільний префікс маршруту;
@Get() обробляє GET /tasks;
@Get(':id') обробляє GET /tasks/:id;
@Post() обробляє POST /tasks;
@Patch(':id') обробляє PATCH /tasks/:id;
@Delete(':id') обробляє DELETE /tasks/:id.
Приклад контролера для ресурсу tasks:
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
interface Task {
id: number;
title: string;
completed: boolean;
}
interface CreateTaskDto {
title: string;
}
interface UpdateTaskDto {
title?: string;
completed?: boolean;
}
@Controller('tasks')
export class TasksController {
private readonly tasks: Task[] = [
{
id: 1,
title: 'Вивчити NestJS',
completed: false,
},
];
private nextId = 2;
@Get()
findAll(): Task[] {
return this.tasks;
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Task {
const task = this.tasks.find((item) => item.id === id);
if (!task) {
throw new NotFoundException('Завдання не знайдено');
}
return task;
}
@Post()
create(@Body() body: CreateTaskDto): Task {
const task: Task = {
id: this.nextId++,
title: body.title,
completed: false,
};
this.tasks.push(task);
return task;
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateTaskDto,
): Task {
const task = this.tasks.find((item) => item.id === id);
if (!task) {
throw new NotFoundException('Завдання не знайдено');
}
if (body.title !== undefined) {
task.title = body.title;
}
if (body.completed !== undefined) {
task.completed = body.completed;
}
return task;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number): void {
const taskIndex = this.tasks.findIndex((item) => item.id === id);
if (taskIndex === -1) {
throw new NotFoundException('Завдання не знайдено');
}
this.tasks.splice(taskIndex, 1);
}
}Цей контролер можна додати до модуля NestJS:
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
@Module({
controllers: [TasksController],
})
export class AppModule {}Для запущеного NestJS-застосунку контролер надає такі маршрути:
GET /tasks
GET /tasks/:id
POST /tasks
PATCH /tasks/:id
DELETE /tasks/:idУ прикладі дані зберігаються в масиві в пам’яті. Після перезапуску застосунку вони втратяться. Це не змінює структуру API: пізніше джерело даних можна замінити на базу даних або сервіс.
Параметр :id є динамічною частиною маршруту:
@Get(':id')
findOne(@Param('id') id: string) {
// ...
}За замовчуванням параметри маршруту мають тип string. Якщо ідентифікатор повинен бути числом, використовуйте ParseIntPipe:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id уже має тип number
}Якщо клієнт надішле некоректне значення, наприклад:
GET /tasks/abcNestJS поверне помилку, замість того щоб передати рядок у код, який очікує число.
Дані для створення або оновлення зазвичай передають у тілі запиту та отримують через @Body():
@Post()
create(@Body() body: CreateTaskDto) {
return body;
}Для різних операцій доцільно використовувати різні типи:
interface CreateTaskDto {
title: string;
}
interface UpdateTaskDto {
title?: string;
completed?: boolean;
}У CreateTaskDto поле title обов’язкове, а в UpdateTaskDto усі поля необов’язкові, оскільки PATCH змінює лише передані властивості.
Типи допомагають описати очікувану структуру даних у коді. Самі по собі вони не перевіряють дані, які надійшли від клієнта. Наприклад, тип string не забороняє клієнту надіслати число під час виконання програми. Для повної перевірки вхідних даних у NestJS пізніше використовують DTO-класи та ValidationPipe.
Іноді ресурс належить іншому ресурсу. Наприклад, завдання може належати проєкту:
GET /projects/3/tasks
GET /projects/3/tasks/10Такі маршрути читаються як:
список завдань проєкту з ідентифікатором 3;
конкретне завдання 10 у проєкті 3.
Вкладені маршрути варто використовувати, коли зв’язок між ресурсами важливий для операції. Не потрібно без потреби створювати дуже глибокі URL:
/projects/3/tasks/10/comments/2Для початкового API достатньо одного або двох рівнів вкладеності. Якщо маршрут стає складним, варто перевірити, чи справді всі батьківські ресурси потрібні для виконання операції.
Колекції часто потрібно фільтрувати, сортувати або розбивати на сторінки. Для цього використовують query-параметри, а не створюють нові маршрути:
GET /tasks?completed=false
GET /tasks?search=NestJS
GET /tasks?limit=10&offset=20Шлях визначає ресурс, а query-параметри уточнюють, які саме елементи потрібно отримати.
У NestJS query-параметри отримують через @Query():
import { Controller, Get, Query } from '@nestjs/common';
@Controller('tasks')
export class TasksController {
@Get()
findAll(@Query('completed') completed?: string) {
if (completed === 'true') {
return 'Повернути виконані завдання';
}
if (completed === 'false') {
return 'Повернути невиконані завдання';
}
return 'Повернути всі завдання';
}
}Значення query-параметрів також надходять як рядки. Якщо потрібне число або логічне значення, його потрібно перетворити та перевірити.
Статус відповіді пояснює клієнту результат операції.
Найпоширеніші статуси для REST API:
200 OK — запит успішно виконано;
201 Created — ресурс створено;
204 No Content — операцію виконано, тіло відповіді відсутнє;
400 Bad Request — запит має неправильний формат або дані;
404 Not Found — ресурс не знайдено;
409 Conflict — операція конфліктує з поточним станом даних;
500 Internal Server Error — внутрішня помилка сервера.
NestJS автоматично використовує типові статуси для багатьох декораторів:
GET зазвичай повертає 200;
POST зазвичай повертає 201;
DELETE зазвичай повертає 200, якщо явно не вказати інший статус.
Для статусу 204 можна використати @HttpCode():
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: number): void {
// Видалення ресурсу
}Якщо ресурс не знайдено, контролер може кинути NotFoundException:
throw new NotFoundException('Завдання не знайдено');NestJS перетворить це на HTTP-відповідь зі статусом 404.
Передбачуваний API має однакові правила для всіх ресурсів.
Наприклад, якщо для tasks використовуються такі маршрути:
GET /tasks
GET /tasks/:id
POST /tasks
PATCH /tasks/:id
DELETE /tasks/:idто аналогічну структуру варто використовувати для projects:
GET /projects
GET /projects/:id
POST /projects
PATCH /projects/:id
DELETE /projects/:idПослідовність зменшує кількість спеціальних правил і спрощує роботу клієнтів API.
Не варто створювати окремі маршрути:
GET /getTasks
POST /createTask
POST /updateTaskКраще використати ресурс і HTTP-метод:
GET /tasks
POST /tasks
PATCH /tasks/:idНе використовуйте GET для видалення або зміни даних:
GET /tasks/delete/1Зміна стану сервера повинна виконуватися відповідним методом:
DELETE /tasks/1Непослідовно:
/tasks
/user
/projectsКраще вибрати один стиль:
/tasks
/users
/projectsPOST для всіх операційЯкщо всі операції виконуються через POST, клієнту важче зрозуміти призначення маршрутів:
POST /tasks/get
POST /tasks/update
POST /tasks/deleteДля стандартних операцій використовуйте HTTP-методи та ідентифікатори ресурсів.
Перед оновленням або видаленням потрібно перевірити, що ресурс існує. Якщо його немає, API має повернути 404 Not Found, а не мовчки завершити операцію.
Для операції над конкретним ресурсом ідентифікатор зазвичай є частиною маршруту:
PATCH /tasks/10а не лише тілом:
{
"id": 10,
"completed": true
}Так URL одразу описує, який ресурс змінюється.
REST API проєктують навколо ресурсів, а не дій.
Для колекцій використовують маршрути на кшталт /tasks.
Для конкретного ресурсу додають ідентифікатор: /tasks/:id.
GET отримує дані, POST створює ресурс, PATCH частково оновлює, PUT повністю замінює, а DELETE видаляє.
У NestJS маршрути описують декораторами контролера.
Параметри маршруту отримують через @Param(), тіло запиту — через @Body(), query-параметри — через @Query().
Послідовні назви маршрутів і правильні HTTP-статуси роблять API передбачуваним для клієнтів.