golang

Query‑first подход или как из SQL запросов или MongoDB контрактов получить готовое REST API

  • вторник, 7 июля 2026 г. в 00:00:07
https://habr.com/ru/articles/1056034/

Я уже давно думал об одной вроде небольшой, но на самом деле изрядно, по крайней мере меня, доставшей backend‑проблеме.

В зависимости от подхода, конечно, но зачастую, мы уже знаем, какие операции и с какими сущностями нам предстоит работать, создавая REST сервис. Но написание моделей и запросов к базе данных всегда проходит лишь как один из этапов написания слоистой архитектуры будущего приложения/микросервиса. Несмотря на опыт, всё же приходится многократно проходить ручную сборку одних и тех же слоёв по каждому домену, не говоря уже о нейминге эндпоинтов, хендлеров, хелперов и так далее...

Почти для каждой новой доменной модели мы проходим один и тот же путь:

— написать методы репозитория;

— добавить сервисный слой;

— создать HTTP handlers;

— описать request и response модели;

— реализовать auth и авторизацию с ролями для эндпоинтов;

— написать/обновить OpenAPI spec;

— добавить curl‑примеры; // по желанию, но мне часто нравилось работать с ними

— написать тесты;

— подключить логирование, Docker, health checks, auth и кучку middleware.

Большая часть работы повторяется из проекта в проект. Беря за основу любимый шаблон все равно мы долго топчемся над реализацией однотипных сущностей прыгая по этому snakeCase, фантазируя над наименованием очередного сервиса, интерфейса, хендлера, хелпера... кажется я уже повторяюсь:).

Подходов к началу создания REST сервиса, как мы знаем, немного:

— spec‑first

— code‑first

— whateverIusedToDo‑first — как привык, так пишу + зависит от настроения и погоды // самый частый не признанный, но всеми любимый

query‑first — мне всегда импонировал, но дать название этому подходу (по крайней мере в литературе не встречал сам — ни на что не претендую и готов признать, коли укажете, что такое есть) вдруг захотелось в этом году...а потом долго писал первые пару строк генератора, да-да, люблю писать сам...но только недолго:).

Что я называю query‑first

Идея простая: если есть модель и операция с базой данных уже описывает, что конкретно приложение должно делать с тем или иным json, можно использовать эту операцию для создания абсолютно (почти) всего REST приложения... Не претендую быть первым кто это придумал, а может и реализовал, ну разве что 1001ым:).

Для SQL проектов это выглядит так:

PostgreSQL schema + SQLC queries
        ↓
Generated endpoint inventory
        ↓
Repository → Service → HTTP handlers
        ↓
http server со всеми "примочками" + 
logging + OpenAPI + auth policies + 
tests + observability + docker + 
docker compose + ci/cd

Для MongoDB проектов идея похожая, только вместо SQLC используются явные YAML‑контракты:

MongoDB collection contracts
        ↓
Generated endpoint inventory
        ↓
Repository → Service → HTTP handlers
        ↓
http server со всеми "примочками" + 
logging + OpenAPI + auth policies + 
tests + observability + docker + 
docker compose + ci/cd

endpoint inventory — это внутренняя модель, которая знает:

— HTTP method и path;

— исходный SQL query или Mongo method;

— path/query/body параметры;

— auth policy;

— роли;

— системные routes вроде health, readiness, Swagger и metrics.

Генератор может создавать разные части приложения из одного источника правды. Handler, OpenAPI operation, curl‑пример, тест и auth policy не должны каждый отдельно угадывать, что такое endpoint. Они должны строиться из одного inventory.

В общем, реализовал я эту идею в open‑source Go CLI под названием rest. Не могу не сказать, что вдохновил меня изначально давний зарекомендовавший себя проект sqlc.

Именно этот проект вдохновил меня сначала начать, затем продолжить, начатое давно и наконец завершить первый релиз. SQLC превращает SQL в type‑safe Go код. rest делает следующий шаг: берёт SQLC output или MongoDB contracts и превращает их в запускаемое слоистое REST приложение.

Цель не в том, чтобы сгенерировать финальный продукт. Цель простая — автоматизировать — убрать скучную и склонную к ошибкам рутину, чтобы разработчик стартовал не с пустой папки, а с хорошего каркаса приложения и дальше писал настоящую бизнес‑логику. В простых проектах это может дать почти готовое приложение. В более сложных — всё равно снять первые 80–90% повторяющейся работы.

SQL пример

Представим таблицу:

CREATE TABLE studies (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    patient         TEXT NOT NULL,
    study_type      TEXT NOT NULL,
    time_beginning  TIMESTAMP,
    surgeon         TEXT NOT NULL,
    deleted         BOOLEAN NOT NULL DEFAULT FALSE
);

И sqlc query:

-- name: GetStudies :many
SELECT * FROM studies
WHERE deleted = false
  AND (
    sqlc.narg('type')::text IS NULL
    OR study_type = sqlc.narg('type')
  )
  AND (
    sqlc.narg('surgeon')::text IS NULL
    OR surgeon = sqlc.narg('surgeon')
  )
ORDER BY time_beginning DESC;

Далее должно идти выполнение команды sqlc generate самостоятельно или автоматическое в рамках самой команды rest gen при активной опции auto_sqlc: enable в файле настроек rest.yaml

SQLC создаёт type‑safe Go код для этого запроса. rest может на его основе вывести endpoint вроде:

GET /studies?type=CT&surgeon=Ivanov

Опциональные sqlc.narg(...) значения становятся optional query parameters. Result type становится частью response model и OpenAPI schema.

MongoDB пример

Для MongoDB нет прямого аналога SQLC, поэтому я использовал явные YAML‑контракты. Mongo contracts лежат здесь: rest_config/rest_mongo/*.yaml.

Каждый entity file может описывать:

— collection name;

— fields;

— indexes;

— CRUD methods;

— custom methods вроде find_one, find_many, update_one, delete_one и aggregate;

— HTTP path/method mapping.

Сгенерированное Mongo app включает repositories, services, handlers, OpenAPI, handler tests, auth middleware, Docker support и runtime health/readiness paths. Mongo domain layer сделан достаточно гибким для document data.

Что генерируется

В зависимости от конфигурации rest.yaml команда rest gen может сгенерировать:

cmd/main.go;

— domain models;

— PostgreSQL или MongoDB repositories;

— service layer;

— HTTP handlers;

— request/response models;

— handler tests;

— OpenAPI и Swagger UI;

— JWT или Basic Auth;

— RBAC route wrapping;

— security headers;

— rate limiting;

— production‑safer CORS defaults;

— request IDs;

— panic recovery;

— body size limits;

— Zap logging;

— optional Prometheus metrics;

— Dockerfile;

— optional Docker Compose;

.env.example;

— Makefile;

— init_db shell script для локального postgres

— CI/CD workflow templates;

— curl examples.

— DEPLOYMENT.md next steps guide

— ARCHITECTURE.md c текущей схемой архитектуры проекта

— README шаблон с инструкцией по запуску проекта

Сгенерированный Go код форматируется линтерами автоматически, поэтому imports и общий стиль кода сразу приводятся к нормальному виду.

Auth generation

Auth оказался одной из самых сложных частей дизайна. Поддерживается пока 2 варианта JWT и BasicAuth.

Flow такой:

1. Указать auth: enable в rest.yaml.

2. Запустить rest gen.

3. Генератор найдёт endpoints и создаст rest_config/auth_rest.yaml.

4. Разработчик выбирает вариант auth и отмечает, какие endpoints публичные, какие защищены, а какие требуют роли.

5. Снова запустить rest gen.

6. Генератор обновит middleware, route wrapping, auth handlers и OpenAPI security.

Для SQL/JWT приложений генератор может создать signup/signin handlers, password hashing, token service, claims configuration и protected routes на основе настроенной user model.

Для Basic Auth и Mongo apps генерируется соответствующий middleware и role checks.

rest doctor

Да, и такая есть фича. Её можно запускать на любом этапе:

— после rest init;

— после редактирования YAML файлов;

— после rest gen;

— перед попыткой запустить сгенерированное приложение.

Она проверяет согласованность конфигов, наличие нужных tools, YAML‑ошибки, готовность Docker, OpenAPI/auth configuration, generated files и наличие необходимых библиотек.

Это как раз та вещь, которой мне не хватало много лет назад, в самом начале пути: команда, которая говорит, чего не хватает и что делать дальше... в точности, как сейчас любой ai powered cli.

Что проект не пытается решить

rest не знает вашу бизнес‑логику.

Он не будет генерировать:

— сложную domain validation;

— custom workflows;

— billing logic;

— external integrations;

— сложные transactions;

— product‑specific authorization rules.

— неудобный реальный SQL query с которым не справится sqlc generate или «крайне сложный» Mongo contract

Генератор нужен, чтобы создать сильную и согласованную стартовую точку. После этого разработчик продолжает обычную разработку. К генератору может возвращаться только тогда, когда появляются новые domain models или queries, для которых снова нужно собрать все слои и добавить к существующим — об этом позаботится скромная опция safe_reload: enable из rest.yaml, которая спросит вас, сохранить ли ваш кастомный код при повторной генерации.

Как попробовать

Install:

go install github.com/repomz/rest/cmd/rest@latest

SQL пример:

rest init --example sql # создает рабочий пример модели и операций для sqlc
rest gen
go run ./cmd/main.go # go test ./... 

MongoDB пример:

rest init --example sql # создает рабочий пример модели и операций для sqlc
rest gen
go run ./cmd/main.go # go test ./... 

Реальный workflow:

rest init
# Создаёт rest_config/ с базовыми настройками генератора.
# Перед генерацией нужно заполнить rest.yaml и добавить контракты:
# SQLC schema/queries для PostgreSQL или YAML-контракты для MongoDB.

rest gen 
go run ./cmd/main.go # go test ./... 

После генерации проекта можно взглянуть на схему ендпоинтов:

rest list endpoints # всегда покажет все актуальные ендпоинты с их текущими опциями

или просто заглянуть в swagger ui: http://localhost:8080/swagger

В README.md вас будет ждать пара шильдиков и текущая архитектура вашего приложения:

my-app/
├── cmd/
│   └── main.go                  # entrypoint приложения
├── internal/
│   ├── config/                  # загрузка env/config
│   ├── db/                      # SQLC models/queries или Mongo client
│   ├── repository/              # доступ к данным
│   ├── service/                 # бизнес-слой
│   ├── transport/
│   │   └── http/                # handlers, routes, middleware
│   ├── auth/                    # JWT/Basic Auth, RBAC
│   ├── logger/                  # Zap logging
│   └── observability/           # health, readiness, metrics
├── api/
│   └── openapi.yaml             # Swagger/OpenAPI схема
├── examples/
│   └── curl/                    # curl-примеры запросов
├── rest_config/
│   ├── rest.yaml                # основные настройки генерации
│   ├── auth_rest.yaml           # политики доступа к endpoints
│   ├── rest_sqlc/               # SQLC schema/queries/config
│   │   ├── rest_sqlc.yaml
│   │   ├── schema.sql
│   │   └── queries.sql
│   └── rest_mongo/              # Mongo contracts, если используется MongoDB
│       ├── rest_cheatsheet.yaml
│       └── user.yaml
├── Dockerfile
├── docker-compose.yaml
├── Makefile
├── .env.example
├── go.mod
└── go.sum

Почему и зачем

Я не утверждаю, что query‑first — единственный правильный способ строить API, но я думаю это просто УДОБНО. Spec‑first отлично подходит, когда OpenAPI является центральным контрактом. Свой Code‑first удобен для многих команд и у каждого beckend'ера конечно возможно есть свой личный кастомный шаблон/генератор. Ручной код остаётся правильным ответом, когда главная сложность — в бизнес‑логике.

Но свой whateverIusedToDo‑first я точно апгрейдил и с удовольствие делюсь с вами, не предтендуя ни на что ни в коем разе, а наоборот, имея большое желание получить нескромный совет по доработке, улучшению и определенно по борьбе с bug issues. Ну, а кому проект возможно сэкономит хоть сколько то времени в его работе и станет, возможно, очередным полезным инструментом, могу с улыбкой только пожелать — Give yourself a little rest.

Если идея кажется интересной, WELCOME TO COLLABORATE and rest together.

Write queries. Get an application. Add business logic.