REST API для управления промокодами: создание, получение, обновление, удаление и активация по email.
Стек: NestJS 10, Prisma 5, PostgreSQL 16, class-validator, Jest + Supertest
- Docker + Docker Compose
- Node.js 20+
# 1. Запустить PostgreSQL
docker-compose up -d
# Автоматически создаёт две БД: promocodes (основная) и promocodes_test (тесты)# 2. Установить зависимости
npm install
# 3. Скопировать переменные окружения (значения по умолчанию работают с docker-compose)
cp .env.example .env
# 4. Применить миграции
npm run db:migrate
# 5. Запустить сервер
npm run start:dev # hot reload# или
npm run build && npm startСервер запустится на http://localhost:3000.
| Переменная | Описание | По умолчанию |
|---|---|---|
DATABASE_URL | Подключение к основной БД | postgresql://postgres:postgres@localhost:5432/promocodes |
DATABASE_URL_TEST | Подключение к тестовой БД | postgresql://postgres:postgres@localhost:5432/promocodes_test |
PORT | Порт HTTP-сервера | 3000 |
NODE_ENV | Окружение | development |
npm test# все e2e-тесты
npm run test:coverage # с покрытиемВсе ошибки возвращаются в едином формате:
{
"error": {
"code": "PROMO_NOT_FOUND",
"message": "Promo code 'SUMMER20' does not exist."
}
}| Код | HTTP | Когда возникает |
|---|---|---|
VALIDATION_ERROR | 400 | Невалидные или отсутствующие поля запроса |
PROMO_NOT_FOUND | 404 | Промокод с таким кодом не найден |
PROMO_DUPLICATE_CODE | 409 | Промокод с таким кодом уже существует |
PROMO_ALREADY_ACTIVATED | 409 | Этот email уже активировал данный промокод |
PROMO_EXPIRED | 422 | Срок действия промокода истёк |
PROMO_LIMIT_REACHED | 422 | Достигнут лимит активаций |
INTERNAL_ERROR | 500 | Непредвиденная ошибка сервера |
Тело запроса:
| Поле | Тип | Обязательное | Ограничения |
|---|---|---|---|
code | string | да | 1–64 символа, без пробелов. Сохраняется в верхнем регистре. |
discount_percent | integer | да | 1–100 |
activation_limit | integer | да | ≥ 1 |
expires_at | ISO 8601 | нет | Дата истечения. Если не указана — промокод бессрочный. |
curl -X POST http://localhost:3000/promo-codes \
-H 'Content-Type: application/json' \
-d '{"code":"SUMMER20","discount_percent":20,"activation_limit":100,"expires_at":"2026-12-31T23:59:59.000Z"}'Ответ 201 Created:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "SUMMER20",
"discount_percent": 20,
"activation_limit": 100,
"expires_at": "2026-12-31T23:59:59.000Z",
"created_at": "2026-04-01T10:00:00.000Z",
"updated_at": "2026-04-01T10:00:00.000Z"
}Ошибки:VALIDATION_ERROR, PROMO_DUPLICATE_CODE
Query-параметры:
| Параметр | Тип | По умолчанию | Ограничения |
|---|---|---|---|
limit | integer | 20 | 1–100 |
offset | integer | 0 | ≥ 0 |
curl 'http://localhost:3000/promo-codes?limit=10&offset=0'Ответ 200 OK:
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "SUMMER20",
"discount_percent": 20,
"activation_limit": 100,
"expires_at": "2026-12-31T23:59:59.000Z",
"created_at": "2026-04-01T10:00:00.000Z",
"updated_at": "2026-04-01T10:00:00.000Z",
"activation_count": 42
}
],
"total": 1,
"limit": 10,
"offset": 0
}activation_count — текущее число выполненных активаций. Сортировка: по убыванию created_at.
Ошибки:VALIDATION_ERROR
curl http://localhost:3000/promo-codes/SUMMER20Ответ 200 OK:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "SUMMER20",
"discount_percent": 20,
"activation_limit": 100,
"expires_at": "2026-12-31T23:59:59.000Z",
"created_at": "2026-04-01T10:00:00.000Z",
"updated_at": "2026-04-01T10:00:00.000Z",
"activation_count": 42
}Поиск регистронезависимый: summer20, Summer20 и SUMMER20 вернут одно и то же.
Ошибки:PROMO_NOT_FOUND
Частичное обновление: передаются только изменяемые поля. Хотя бы одно поле обязательно.
Тело запроса:
| Поле | Тип | Ограничения |
|---|---|---|
discount_percent | integer | 1–100 |
activation_limit | integer | ≥ 1 |
expires_at | ISO 8601 / null | null — убрать срок действия (сделать промокод бессрочным) |
curl -X PATCH http://localhost:3000/promo-codes/SUMMER20 \
-H 'Content-Type: application/json' \
-d '{"discount_percent":25,"expires_at":null}'Ответ 200 OK: та же структура, что у GET /promo-codes/:code.
Ошибки:VALIDATION_ERROR, PROMO_NOT_FOUND
curl -X DELETE http://localhost:3000/promo-codes/SUMMER20Ответ 204 No Content: тело пустое.
Каскадно удаляет все связанные активации.
Ошибки:PROMO_NOT_FOUND
Каждый email может активировать один промокод только один раз. Активация атомарна: при одновременных запросах лимит не будет превышен.
Тело запроса:
| Поле | Тип | Обязательное | Ограничения |
|---|---|---|---|
email | string | да | Валидный email. Нормализуется к нижнему регистру. |
curl -X POST http://localhost:3000/promo-codes/SUMMER20/activate \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com"}'Ответ 201 Created:
{
"activation_id": "660e8400-e29b-41d4-a716-446655440111",
"promo_code": "SUMMER20",
"email": "user@example.com",
"discount_percent": 20,
"activated_at": "2026-04-01T11:00:00.000Z"
}Ошибки:VALIDATION_ERROR, PROMO_NOT_FOUND, PROMO_ALREADY_ACTIVATED, PROMO_EXPIRED, PROMO_LIMIT_REACHED
Приоритет ошибок (если одновременно выполняется несколько условий):
PROMO_ALREADY_ACTIVATED— email уже активировал этот кодPROMO_EXPIRED— срок действия истёкPROMO_LIMIT_REACHED— исчерпан лимит активаций