REST API сервис для пользователей, рецептов, ингредиентов, избранных рецептов и расширенного MongoDB-контента рецептов.
Проект реализован на C++ с использованием userver, PostgreSQL и MongoDB. API документирован через OpenAPI, для локального запуска подготовлены Dockerfile и docker-compose.yaml.
- Регистрация пользователя.
- Логин и получение JWT-токена.
- Получение текущего пользователя по JWT.
- Поиск пользователя по логину.
- Поиск пользователей по имени или фамилии.
- Создание рецепта.
- Получение и поиск рецептов.
- Добавление ингредиента в рецепт.
- Получение ингредиентов рецепта.
- Получение рецептов конкретного пользователя.
- Добавление рецепта в избранное.
- Хранение расширенного описания рецептов, комментариев и activity logs в MongoDB.
- Запись события активности рецепта через API.
- Получение полного рецепта из PostgreSQL + MongoDB.
- Создание и обновление MongoDB details рецепта.
- Добавление и получение комментариев рецепта.
- C++.
- userver.
- PostgreSQL 15.
- MongoDB 7.
- Docker Compose.
- OpenAPI 3.0.
- Python
unittest(smoke-тесты API) - Structurizr
.
├── config/
| └── config.yaml
├── docs/
| └── openapi.yaml
├── mongo/
| ├── validation.js
| ├── data.js
| └── queries.js
├── sql/
| ├── schema.sql
| ├── data.sql
| └── queries.sql
├── src/
| ├── auth/
| ├── v1/
| └── main.cpp
├── test/
| ├── README.md
| ├── test_api.py
| └── test_mongo.py
├── CMakeLists.txt
├── Dockerfile
├── docker-compose.yaml
├── requirements.txt
├── optimization.md
├── schema_design.md
└── README.md
Диаграмма показывает пользователей системы, внешние взаимодействия и основные контейнеры приложения.
Диаграмма контейнеров показывает взаимодействие между Recipe API, PostgreSQL, MongoDB и Swagger UI.
Диаграмма компонентов показывает внутреннюю структуру Recipe API: модули аутентификации, рецептов, ингредиентов, избранного и MongoDB-контента.
| Файл | Назначение |
|---|---|
sql/schema.sql |
Создание расширения pg_trgm, таблиц, PK, FK, constraints и индексов. |
sql/data.sql |
Тестовые данные |
sql/queries.sql |
SQL-запросы для всех операций варианта задания |
optimization.md |
Обоснование индексов, частые запросы, примеры EXPLAIN ANALYZE |
schema_design.md |
Документная модель MongoDB и обоснование embedded/references |
mongo/validation.js |
Создание коллекций MongoDB, $jsonSchema-валидация и индексы |
mongo/data.js |
Тестовые данные MongoDB, минимум 10 документов в каждой коллекции |
mongo/queries.js |
CRUD-запросы MongoDB и aggregation pipelines |
docs/openapi.yaml |
OpenAPI 3.0 спецификация REST API. |
В docker-compose.yaml файлы schema.sql и data.sql монтируются в /docker-entrypoint-initdb.d с числовыми префиксами, чтобы PostgreSQL сначала создал схему, а затем загрузил данные. queries.sql - файл с запросами.
MongoDB при первом запуске применяет mongo/validation.js, затем mongo/data.js. Скрипт mongo/queries.js можно выполнять вручную через mongosh.
Пользователи сервиса.
| Колонка | Тип | Ограничения |
|---|---|---|
id |
SERIAL |
PRIMARY KEY |
username |
VARCHAR(100) |
NOT NULL, UNIQUE, не пустая строка |
password_hash |
VARCHAR(255) |
NOT NULL |
first_name |
VARCHAR(100) |
NOT NULL, не пустая строка |
last_name |
VARCHAR(100) |
NOT NULL, не пустая строка |
created_at |
TIMESTAMP |
NOT NULL, DEFAULT CURRENT_TIMESTAMP |
Справочник ингредиентов.
| Колонка | Тип | Ограничения |
|---|---|---|
id |
SERIAL |
PRIMARY KEY |
name |
VARCHAR(200) |
NOT NULL, UNIQUE, не пустая строка |
unit |
VARCHAR(50) |
NOT NULL, не пустая строка |
created_at |
TIMESTAMP |
NOT NULL, DEFAULT CURRENT_TIMESTAMP |
Рецепты пользователей.
| Колонка | Тип | Ограничения |
|---|---|---|
id |
SERIAL |
PRIMARY KEY |
owner_id |
INTEGER |
NOT NULL, FK -> users(id) ON DELETE CASCADE |
name |
VARCHAR(255) |
NOT NULL, не пустая строка |
created_at |
TIMESTAMP |
NOT NULL, DEFAULT CURRENT_TIMESTAMP |
updated_at |
TIMESTAMP |
NOT NULL, DEFAULT CURRENT_TIMESTAMP |
Связь многие-ко-многим между рецептами и ингредиентами.
| Колонка | Тип | Ограничения |
|---|---|---|
id |
SERIAL |
PRIMARY KEY |
recipe_id |
INTEGER |
NOT NULL, FK -> recipes(id) ON DELETE CASCADE |
ingredient_id |
INTEGER |
NOT NULL, FK -> ingredients(id) |
quantity |
DECIMAL(10, 2) |
NOT NULL, DEFAULT 0, CHECK (quantity >= 0) |
created_at |
TIMESTAMP |
NOT NULL, DEFAULT CURRENT_TIMESTAMP |
Дополнительно: UNIQUE(recipe_id, ingredient_id).
Избранные рецепты пользователей.
| Колонка | Тип | Ограничения |
|---|---|---|
id |
SERIAL |
PRIMARY KEY |
user_id |
INTEGER |
NOT NULL, FK -> users(id) ON DELETE CASCADE |
recipe_id |
INTEGER |
NOT NULL, FK -> recipes(id) ON DELETE CASCADE |
created_at |
TIMESTAMP |
NOT NULL, DEFAULT CURRENT_TIMESTAMP |
Дополнительно: UNIQUE(user_id, recipe_id).
| Индекс | Назначение |
|---|---|
idx_users_first_name_trgm |
Поиск пользователей по маске имени. |
idx_users_last_name_trgm |
Поиск пользователей по маске фамилии. |
idx_ingredients_name_trgm |
Поиск ингредиентов по части названия. |
idx_recipes_owner_id |
Получение рецептов пользователя и поддержка FK. |
idx_recipes_name_trgm |
Поиск рецептов по маске названия. |
idx_recipe_ingredients_recipe_id |
Получение ингредиентов рецепта. |
idx_recipe_ingredients_ingredient_id |
JOIN по ингредиентам и поддержка FK. |
idx_user_favorites_user_id |
Выборки избранного по пользователю и поддержка FK. |
idx_user_favorites_recipe_id |
Выборки избранного по рецепту и поддержка FK. |
Для ILIKE '%text%' используется pg_trgm и GIN-индексы, потому что обычные btree-индексы плохо подходят для поиска по подстроке.
docker compose up --buildПосле запуска доступны:
- API:
http://localhost:8080 - Swagger UI:
http://localhost:8081 - PostgreSQL:
localhost:5432 - MongoDB:
localhost:27017
Остановить сервисы:
docker compose downПри первом запуске PostgreSQL автоматически применяет:
sql/schema.sqlsql/data.sql
MongoDB автоматически применяет:
mongo/validation.jsmongo/data.js
Если нужно пересоздать БД с нуля:
docker compose down
docker compose up --buildЕсли PostgreSQL уже запущен, можно применить схему и данные вручную.
PowerShell:
Get-Content sql\schema.sql | docker compose exec -T postgres psql -U app -d recipe_app_db
Get-Content sql\data.sql | docker compose exec -T postgres psql -U app -d recipe_app_dbПроверить количество записей:
docker compose exec -T postgres psql -U app -d recipe_app_db -c "SELECT 'users' AS table_name, count(*) FROM users UNION ALL SELECT 'ingredients', count(*) FROM ingredients UNION ALL SELECT 'recipes', count(*) FROM recipes UNION ALL SELECT 'recipe_ingredients', count(*) FROM recipe_ingredients UNION ALL SELECT 'user_favorites', count(*) FROM user_favorites;"Все запросы для варианта задания находятся в sql/queries.sql:
- создание пользователя;
- поиск пользователя по логину;
- поиск пользователей по маске имени или фамилии;
- создание рецепта;
- получение списка рецептов;
- поиск рецептов по названию;
- добавление ингредиента в рецепт;
- получение ингредиентов рецепта;
- получение рецептов пользователя;
- добавление рецепта в избранное.
MongoDB используется для документной части рецептов:
recipe_details- расширенное описание рецепта, шаги, фото, теги, нутриенты и заметки;comments- комментарии и оценки пользователей;recipe_activity_logs- события просмотра, комментирования, обновления и добавления в избранное.
Запустить MongoDB вместе с API:
docker compose up --buildОткрыть shell:
docker compose exec mongo mongosh recipe_app_mongoПроверить количество документов:
docker compose exec -T mongo mongosh recipe_app_mongo --quiet --eval "db.recipe_details.countDocuments(); db.comments.countDocuments(); db.recipe_activity_logs.countDocuments();"Выполнить примеры CRUD и aggregation:
docker compose exec -T mongo mongosh recipe_app_mongo < mongo/queries.jsPowerShell:
Get-Content mongo\queries.js -Encoding UTF8 | docker compose exec -T mongo mongosh recipe_app_mongoОписание выбора embedded documents и references находится в schema_design.md.
Описание оптимизаций находится в optimization.md.
В документе описаны:
- назначение каждого индекса;
- частые запросы приложения;
- примеры планов
EXPLAIN ANALYZE; - сравнение поведения до и после добавления индексов;
- вывод по партиционированию.
Партиционирование для текущей версии сервиса не требуется. В системе нет явно большой временной таблицы вроде журнала событий, заказов или истории просмотров.
Если в будущем появится крупная таблица событий, например recipe_views или recipe_history, ее можно будет партиционировать по created_at помесячно.
| Метод | URL | Описание | Auth |
|---|---|---|---|
POST |
/v1/register |
Регистрация пользователя | Нет |
POST |
/v1/login |
Логин и получение JWT-токена | Нет |
GET |
/v1/me |
Получение текущего пользователя | Да |
GET |
/v1/users/by-login/{username} |
Поиск пользователя по логину | Нет |
GET |
/v1/users/search?mask={mask} |
Поиск пользователей по имени или фамилии | Нет |
GET |
/v1/recipes |
Получение списка рецептов | Нет |
POST |
/v1/recipes |
Создание рецепта | Да |
GET |
/v1/recipes/search?name={name} |
Поиск рецептов по названию | Нет |
GET |
/v1/recipes/{recipe_id} |
Получение рецепта с MongoDB details | Нет |
PUT |
/v1/recipes/{recipe_id}/details |
Создание или замена MongoDB details | Да |
GET |
/v1/recipes/{recipe_id}/comments |
Получение комментариев рецепта из MongoDB | Нет |
POST |
/v1/recipes/{recipe_id}/comments |
Добавление комментария в MongoDB | Да |
GET |
/v1/recipes/{recipe_id}/ingredients |
Получение ингредиентов рецепта | Нет |
POST |
/v1/recipes/{recipe_id}/ingredients |
Добавление ингредиента в рецепт | Да |
GET |
/v1/users/{user_id}/recipes |
Получение рецептов пользователя | Нет |
POST |
/v1/users/{user_id}/favorites |
Добавление рецепта в избранное | Да |
POST |
/v1/recipes/{recipe_id}/activity-logs |
Запись события активности в MongoDB | Нет |
Полное описание схем запросов, ответов, статус-кодов и примеров находится в docs/openapi.yaml.
Сценарий регистрации пользователя.
Сценарий аутентификации пользователя и выдачи JWT.
Сценарий создания нового рецепта авторизованным пользователем.
Для аутентификации используется JWT-токен.
- Пользователь регистрируется через
POST /v1/register. - Пользователь логинится через
POST /v1/login. - Сервер возвращает
access_token. - Для защищенных endpoint токен передается в заголовке:
Authorization: Bearer <access_token>JWT проверяется в middleware для следующих endpoints:
GET /v1/mePOST /v1/recipesPUT /v1/recipes/{recipe_id}/detailsPOST /v1/recipes/{recipe_id}/commentsPOST /v1/recipes/{recipe_id}/ingredientsPOST /v1/users/{user_id}/favorites
Для защищенных операций сервис не только проверяет JWT, но и сверяет владельца ресурса с пользователем из токена. Если owner_id или user_id передан в запросе и не совпадает с JWT, API возвращает 403 Forbidden.
docker compose up --buildПосле запуска доступны:
- API:
http://localhost:8080 - Swagger UI:
http://localhost:8081 - PostgreSQL:
localhost:5432
Остановить сервисы:
docker compose downОбязательные поля: username, password, first_name, last_name.
curl -X POST http://localhost:8080/v1/register \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"secret123","first_name":"user","last_name":"name"}'curl -X POST http://localhost:8080/v1/login \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"secret123"}'curl http://localhost:8080/v1/me \
-H "Authorization: Bearer <access_token>"curl http://localhost:8080/v1/users/by-login/usercurl "http://localhost:8080/v1/users/search?mask=us"curl -X POST http://localhost:8080/v1/recipes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"name":"My recipe"}'owner_id можно не передавать: сервис берет владельца из JWT. Если передать owner_id, он должен совпадать с пользователем из токена.
curl http://localhost:8080/v1/recipescurl "http://localhost:8080/v1/recipes/search?name=Pasta"curl http://localhost:8080/v1/recipes/1Если рецепт есть в PostgreSQL, но для него еще нет документа в recipe_details, API возвращает 200 и поле details: null.
curl -X PUT http://localhost:8080/v1/recipes/1/details \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"title":"Pasta Bolognese","description":"Домашняя паста с мясным соусом","difficulty":"medium","cooking_time_minutes":45,"servings":4,"tags":["pasta","dinner"],"steps":[{"order":1,"title":"Подготовить овощи","text":"Нарезать лук и чеснок","duration_minutes":10}]}'curl -X POST http://localhost:8080/v1/recipes/1/comments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"comment":"Очень вкусно","rate":5}'user_id для комментария также берется из JWT. Явно переданный user_id должен совпадать с пользователем из токена.
curl http://localhost:8080/v1/recipes/1/commentsОбязательные поля: name, unit. Поле quantity опционально, по умолчанию используется 0.
curl -X POST http://localhost:8080/v1/recipes/1/ingredients \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"name":"sugar","unit":"g","quantity":120}'curl http://localhost:8080/v1/recipes/1/ingredientscurl http://localhost:8080/v1/users/1/recipesЕсли пользователя нет, endpoint возвращает 404 и тело user_not_found.
curl -X POST http://localhost:8080/v1/users/1/favorites \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"recipe_id":5}'user_id в URL должен совпадать с пользователем из JWT. Первое добавление возвращает 201, повторное добавление того же рецепта в избранное возвращает 200 и status: already_exists.
curl -X POST http://localhost:8080/v1/recipes/1/activity-logs \
-H "Content-Type: application/json" \
-d '{"user_id":5,"action":"view","metadata":{"source":"web","ip":"127.0.0.1","user_agent":"Mozilla/5.0"}}'Сначала нужно запустить сервис:
docker compose up --buildЗатем выполнить smoke-тесты:
python -m unittest discover -s test -p "test_*.py" -vПо умолчанию тесты обращаются к http://localhost:8080. При необходимости адрес можно изменить:
$env:API_BASE_URL = "http://localhost:8080"
python -m unittest discover -s test -p "test_*.py" -vOpenAPI-спецификация находится по явному пути docs/openapi.yaml. Описание MongoDB-модели находится в schema_design.md, оптимизации PostgreSQL - в optimization.md, инструкции по тестам - в test/README.md.
Swagger UI поднимается вместе с Docker Compose и доступен по адресу:
http://localhost:8081
Для локального просмотра и редактирования диаграмм используется Structurizr.
Запуск:
docker run -it --rm -p 8088:8080 `
-v ${PWD}:/usr/local/structurizr `
structurizr/structurizr localПосле запуска интерфейс будет доступен по адресу:
http://localhost:8088





