TL;DR: run a fast read-only JSON API for PostgreSQL in minutes.
Endpoints:/api/indexand/api/count. Contract defined in YAML.
YrestAPI is a declarative REST engine in Go for read-heavy PostgreSQL APIs.
You describe models, relations, and response shapes in YAML, and YrestAPI serves JSON without ORM code or custom read handlers.
Versioning: this project publicly follows Semantic Versioning 2.0.0.
- Offload heavy list endpoints from your main API (
PHP,Ruby,Node, etc.). - Remove ORM and server-side business code from read-only API paths.
- Serve nested JSON with filters, sorts, pagination, and relation traversal.
- Shape response fields after fetch with YAML formatters instead of hand-written view code.
- Localize field values and enum-like codes through locale dictionaries, not controller logic.
- Keep the contract explicit in YAML instead of burying it in code.
The repository includes a ready-to-run smoke stack: PostgreSQL, schema, seed data, and bundled YAML models.
# 1) clone
git clone https://github.com/SergePauli/YrestAPI.git
cd YrestAPI
# 2) start
docker compose up --build
# 3) call the API
curl -sS -X POST http://localhost:8080/api/index \
-H 'Content-Type: application/json' \
-d '{"model":"Project","preset":"with_members","limit":2,"sorts":["id ASC"]}'Useful checks:
curl -sS http://localhost:8080/healthz
curl -sS http://localhost:8080/readyz
curl -sS -X POST http://localhost:8080/api/index \
-H 'Content-Type: application/json' \
-d '{"model":"Contragent","preset":"item","sorts":["id ASC"],"limit":2}'
curl -sS -X POST http://localhost:8080/api/count \
-H 'Content-Type: application/json' \
-d '{"model":"Person"}'Notes:
compose.yamlstarts PostgreSQL and applies the bundled smoke schema and seed automatically.- The default compose setup uses
MODELS_DIR=/app/test_dbso the seeded database matches the shipped YAML models. - If port
8080is busy, runHOST_PORT=8081 docker compose up --build.
Each model is a YAML file that maps a logical API model to a PostgreSQL table and declares relations, aliases, computable fields, and presets.
Example:
table: people
aliases:
org: "contragent.organization"
relations:
contacts:
model: Contact
type: has_many
through: PersonContact
presets:
item:
fields:
- source: id
type: int
- source: contacts
type: preset
preset: itemPresets define the exact JSON shape returned to the client. They are intentionally client-shaped, not table-shaped.
Example:
presets:
card:
fields:
- source: id
type: int
- source: "{person_name.value} {last_name}[0]."
type: formatter
alias: short_label
- source: org
type: preset
preset: shortThis is one of the strongest parts of the engine.
formatterfields let you compose display values from fetched data after SQL execution- formatter expressions can traverse nested fields, slice strings, and use ternary logic
localize: truelets you map raw DB values to locale dictionaries fromcfg/locales/<locale>.yml- together they let YAML describe not only data selection, but also client-facing presentation fields
That means you can build fields like:
- short names
- labels assembled from related objects
- localized statuses and enum values
- compact display strings for nested relations
See details:
Supported relation types:
has_manyhas_onebelongs_tothrough- polymorphic
belongs_to
Recursive/self relations are supported, but cyclic traversal must be explicit via reentrant: true and bounded via max_depth.
Requests can:
- filter by scalar fields and dotted relation paths
- sort by direct, related, alias, or computable fields
- paginate with
offsetandlimit - combine conditions with
and/or
Example filter keys:
name__cntorg.name__eqpersons.last_name__eqid__instatus_id__null
All API requests are POST with JSON bodies.
Returns a JSON array of items shaped by the requested preset.
Payload:
{
"model": "Person",
"preset": "card",
"filters": {
"name__cnt": "John",
"org.name_or_org.full_name__cnt": "IBM"
},
"sorts": ["org.name DESC", "id ASC"],
"offset": 0,
"limit": 50
}Response:
[
{
"id": 1,
"short_label": "John Smith S.",
"org": {
"name": "IBM"
}
}
]Behavior notes:
- relation traversal uses dotted paths such as
org.name - aliases can shorten long paths
- string filters are case-insensitive by default
- invalid payloads return
400 - SQL/build/runtime errors return
500
Returns a single integer count for the same filter semantics.
Payload:
{
"model": "Person",
"filters": {
"org.name__cnt": "IBM"
}
}Response:
{
"count": 123
}Authentication is optional and controlled by AUTH_ENABLED.
When AUTH_ENABLED=true, YrestAPI expects:
Authorization: Bearer <token>
Supported JWT validation modes:
HS256RS256ES256
Supported claim validation:
issaudexpnbfiat
Example HS256 configuration:
AUTH_ENABLED=true
AUTH_JWT_VALIDATION_TYPE=HS256
AUTH_JWT_ISSUER=auth-service
AUTH_JWT_AUDIENCE=yrest-api
AUTH_JWT_HMAC_SECRET=replace-with-strong-shared-secret
AUTH_JWT_CLOCK_SKEW_SEC=60Example RS256 configuration:
AUTH_ENABLED=true
AUTH_JWT_VALIDATION_TYPE=RS256
AUTH_JWT_ISSUER=auth-service
AUTH_JWT_AUDIENCE=yrest-api
AUTH_JWT_PUBLIC_KEY_PATH=/etc/yrestapi/keys/auth_public.pem
AUTH_JWT_CLOCK_SKEW_SEC=60CORS:
- default
CORS_ALLOW_ORIGIN=* - default
CORS_ALLOW_CREDENTIALS=false - for production, set explicit origins instead of
* - only enable credentials when you really need browser cookies or auth propagation
Example localized field in YAML:
fields:
- source: status
type: int
localize: true- structured JSONL logs are written to
log/app.log - request logging includes method, path, and status
- startup failures are also emitted to stderr with a short reason
GET /healthzreturns200 OKwhen the HTTP process is aliveGET /readyzreturns200 OKonly when the model registry is initialized and PostgreSQL is reachable
These endpoints are unauthenticated and intended for container or orchestrator probes.
Build locally:
docker build -t yrestapi:local .The repository also includes a release workflow that publishes a Docker image for tags v*.
Configuration is read from environment variables:
| Env var | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP port |
POSTGRES_DSN |
postgres://postgres:postgres@localhost:5432/app?sslmode=disable |
PostgreSQL DSN |
MODELS_DIR |
./db |
Directory with YAML model files |
LOCALE |
en |
Default locale |
AUTH_ENABLED |
false |
Enable JWT auth |
AUTH_JWT_VALIDATION_TYPE |
HS256 |
JWT algorithm |
AUTH_JWT_ISSUER |
empty | Required issuer |
AUTH_JWT_AUDIENCE |
empty | Required audience |
AUTH_JWT_HMAC_SECRET |
empty | Shared secret for HS256 |
AUTH_JWT_PUBLIC_KEY |
empty | Inline PEM public key |
AUTH_JWT_PUBLIC_KEY_PATH |
empty | PEM public key path |
AUTH_JWT_CLOCK_SKEW_SEC |
60 |
Allowed clock skew |
CORS_ALLOW_ORIGIN |
* |
Allowed CORS origin(s) |
CORS_ALLOW_CREDENTIALS |
false |
Send Access-Control-Allow-Credentials: true |
ALIAS_CACHE_MAX_BYTES |
0 |
Alias cache limit, 0 = unlimited |
Model directory resolution:
- if
MODELS_DIRis explicitly set, that path is used - otherwise the service tries
./db - if
./dbhas no model.ymlfiles, it falls back to./test_db
In production you will typically:
- mount your own model directory
- point
MODELS_DIRat it - provide
cfg/locales/<locale>.ymlfor the selectedLOCALE
- the public API surface is intentionally small:
/api/index,/api/count,/healthz,/readyz - release notes are generated from CHANGELOG.md
- versioning follows VERSIONING.md
- detailed engine documentation lives in DOCS.md
make test runs unit and integration tests:
make testBefore running tests:
- Ensure PostgreSQL is available on
localhostor127.0.0.1. - Ensure
POSTGRES_DSNis valid. - Ensure
APP_ENVis notproduction.
Integration test behavior:
- test bootstrap derives a test DSN from
POSTGRES_DSN - it creates database
test - it applies migrations from
migrations/ - it drops database
testafter the run - non-local DB hosts are rejected for safety
See CONTRIBUTING.md.
YrestAPI is licensed under the GNU General Public License v3.0 or, at your option, any later version (GPLv3+).
See LICENSE.txt.
This section is only a practical summary for engineering discussions. It is not legal advice, not a separate license, and not an additional permission beyond LICENSE.txt.
- YrestAPI is a copyleft project under
GPLv3+. - Internal evaluation and internal use are usually the least complicated scenarios.
- Distribution of the software, modified versions, or products that include GPL-covered code is where GPL obligations usually become relevant.
- If your planned use is commercial and you need a separate licensing conversation, open a GitHub issue with the
licensinglabel. - Dual licensing is not offered by default today. It may be considered later, but only after an explicit, deliberate maintainer decision.