URL — это первое, что видит разработчик, который подключается к вашему API. Хорошо спроектированный путь читается как предложение: GET /orders/42/items — «дай мне позиции заказа №42». Плохо спроектированный заставляет открывать документацию на каждый шаг. В этой статье разберём, как NestJS устроен изнутри и какие правила помогут сделать API предсказуемым.
Как NestJS строит URL из кода
В NestJS каждый контроллер отвечает за свой префикс пути. Базовый адрес ресурса задаётся в @Controller, а конкретные эндпоинты — в декораторах методов.
@Controller('orders') // /orders
export class OrdersController {
@Get() // GET /orders
findAll() {}
@Get(':id') // GET /orders/:id
findOne(@Param('id') id: string) {}
}
Чтобы не писать /api/v1 в каждом контроллере вручную, настройте это один раз при запуске приложения.
Глобальный префикс и версионирование
В main.ts два вызова дают единый формат для всех роутов:
import { VersioningType } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
await app.listen(3000);
}
После этого контроллер @Controller('orders') автоматически отвечает по адресу /api/v1/orders.
Служебные пути — health, ready — должны быть вне этой цепочки, иначе балансировщик придётся настраивать с учётом версии:
@Controller('health')
export class HealthController {
@Get()
check() { return { status: 'ok' }; }
}
// → GET /health (без /api, без версии)
Чтобы контроллер не получил глобальный префикс, укажите { host } или переопределите версию декоратором @Version(NO_VERSION).
Как называть пути
Строчные буквы и дефисы
Стандарт для URL — kebab-case: всё строчными через дефис. Никакого camelCase, никакого snake_case.
@Controller('order-items') // правильно
@Controller('OrderItems') // неправильно — заглавные
@Controller('order_items') // неправильно — подчёркивание
@Controller('orderItems') // неправильно — camelCase
Без глаголов и расширений
Путь называет существительное — ресурс, а не действие. Действие выражает HTTP-метод.
@Get(':id') // правильно: /orders/:id
@Get('getAll') // неправильно: глагол в пути
@Get('list.json') // неправильно: расширение файла
Без финального слеша
NestJS не делает редирект с orders/ на orders. Если объявить путь со слешем — это будет отдельный роут, и без слеша вернётся 404.
@Get(':id') // правильно: /orders/:id
@Get(':id/') // неправильно: trailing slash
Коллекции и одиночные ресурсы
Коллекция — множественное число, одиночный ресурс — единственное. Правило простое: если по одному пути вернётся список — используйте множественное число.
@Controller('orders') // коллекция — /orders, /orders/:id
@Controller('products') // коллекция — /products, /products/:id
@Controller('order') // неправильно для коллекции
Если ресурс всегда один на пользователя — единственное число:
@Controller('profile') // один профиль текущего пользователя
export class ProfileController {
@Get() // GET /api/v1/profile
get() {}
}
Имя ресурса берите из предметной области: orders, а не purchases; customers, а не users. Когда слово из бизнеса совпадает с именем в коде — API понятен без дополнительных объяснений.
HTTP-методы
Каждый метод имеет чёткое назначение. Выбор метода — это часть контракта, который видит клиент.
@Controller('orders')
export class OrdersController {
@Get() // GET /api/v1/orders → 200
findAll() {}
@Get(':id') // GET /api/v1/orders/:id → 200
findOne(@Param('id') id: string) {}
@Post() // POST /api/v1/orders → 201
create(@Body() dto: CreateOrderDto) {}
@Put(':id') // PUT /api/v1/orders/:id → 200
replace(@Param('id') id: string, @Body() dto: ReplaceOrderDto) {}
@Patch(':id') // PATCH /api/v1/orders/:id → 200
update(@Param('id') id: string, @Body() dto: UpdateOrderDto) {}
@Delete(':id')
@HttpCode(204) // DELETE → 204 No Content
remove(@Param('id') id: string) {}
}
На что обратить внимание:
- POST при создании возвращает
201 Created— NestJS делает это автоматически. - DELETE по умолчанию в NestJS возвращает
200, но правильный ответ —204 No Content. Нужно явно добавить@HttpCode(204). - GET не должен менять данные. Если нужна доменная команда (например, «отменить заказ»), используйте POST с именем действия в пути:
@Get(':id/cancel') // неправильно: GET меняет состояние
cancel() {}
@Post(':id/cancel') // правильно: команда через POST
@HttpCode(200)
cancel() {}
Вложенность ресурсов
Когда один ресурс логически принадлежит другому, это можно выразить в пути:
@Controller('orders')
export class OrdersController {
@Get(':orderId/items') // GET /api/v1/orders/:orderId/items
getItems(@Param('orderId') orderId: string) {}
@Get(':orderId/items/:itemId') // GET /api/v1/orders/:orderId/items/:itemId
getItem(
@Param('orderId') orderId: string,
@Param('itemId') itemId: string,
) {}
}
Параметры именуются уникально: orderId и itemId, а не оба id — это требование инструментов документации вроде Swagger.
Максимальная глубина — два уровня вложенности. Три уровня и глубже делают URL нечитаемым и сложным для кэширования. Вместо этого используйте плоский ресурс с фильтрацией:
// неправильно: три уровня
@Get(':userId/orders/:orderId/items/:itemId')
// правильно: плоский ресурс с параметром запроса
@Get() // GET /api/v1/items?orderId=...
findAll(@Query('orderId') orderId: string) {}
Идентификатор ресурса передаётся в пути, не в теле запроса:
@Put(':id') // правильно
replace(@Param('id') id: string) {}
@Put() // неправильно: id в body
replace(@Body() dto: { id: string }) {}
Коротко
- Пути —
kebab-case, строчные, без глаголов, без слеша в конце. setGlobalPrefix('api')+VersioningType.URIдают/api/v1/...для всех контроллеров автоматически.- Служебные пути (
/health,/ready) — вне глобального префикса. - Коллекции — множественное число (
/orders), одиночный ресурс — единственное (/profile). - Имя ресурса — из предметной области, а не технические синонимы.
- GET не меняет данные; доменные команды — через POST.
- DELETE возвращает
204 No Content— нужен@HttpCode(204). - Максимум два уровня вложенности; глубже — плоский ресурс с фильтрацией.
- Параметры в пути именуются уникально (
orderId,itemId).
Что почитать дальше
- Версионирование — как устроен
enableVersioningи переход между версиями. - Alias и Action-эндпоинты —
me,latest, доменные команды в NestJS. - Query-параметры — DTO-классы,
@Query, пагинация. - OpenAPI и антипаттерны —
operationId, именование параметров.