Introduction
Sometimes you need a small internal API — a link shortener, a contacts list, a notes service — but not a whole web backend with a framework, an ORM, and a deploy pipeline. This post builds a working CRUD API with aux4/api: HTTP routes map straight to aux4 commands, aux4/repository stores the data with no schema to migrate, and aux4/validator checks the request body. No server code, and it still returns real 400s.
We'll build a link shortener.
Install the packages
aux4 aux4 pkger install aux4/api aux4/repository aux4/validator aux4/curl
Step 1: the commands behind the API
The API is just aux4 commands, so write those first. aux4/repository is a schema-less JSON document store — write, read, delete by id, no CREATE TABLE. Create .aux4 with a links profile:
{
"profiles": [
{
"name": "main",
"commands": [
{
"name": "links",
"execute": [
"profile:links"
],
"help": {
"text": "Link shortener commands"
}
}
]
},
{
"name": "links",
"commands": [
{
"name": "list",
"execute": [
"set:n=!aux4 repository count links",
"when:${n}==0:log:[]",
"when:${n}==0:exit:0",
"when:${n}>0:aux4 repository read links"
],
"help": {
"text": "List all links"
}
},
{
"name": "get",
"execute": [
"set:p=json:nvl(params, '{\"id\":\"\"}')",
"set:sid=nvl(id, p.id)",
"set:n=!aux4 repository count links --expr \"slug = '${sid}'\"",
"when:${n}==0:log:[]",
"when:${n}==0:exit:0",
"when:${n}>0:aux4 repository read links --id ${sid}"
],
"help": {
"text": "Get a link by slug",
"variables": [
{
"name": "id",
"text": "Link slug",
"arg": true,
"default": ""
},
{
"name": "params",
"text": "Path params (injected by aux4/api)",
"default": ""
}
]
}
},
{
"name": "create",
"execute": [
"set:raw=!echo \"[\"value(body)\"]\" | aux4 validator validate --config validation --rules link --ignore --onlyValid",
"set:parsed=json:nvl(raw, '{\"slug\":\"\"}')",
"when:${parsed.slug}==:log:{\"statusCode\":400,\"headers\":{\"Content-Type\":\"application/json\"},\"body\":\"{\\\"error\\\":\\\"slug is required and url must be valid\\\"}\"}",
"when:${parsed.slug}!=:nout:echo value(raw) | aux4 repository write links --id ${parsed.slug}",
"when:${parsed.slug}!=:aux4 repository read links --id ${parsed.slug}"
],
"help": {
"text": "Validate a link body then store it",
"variables": [
{
"name": "body",
"text": "JSON body (injected by aux4/api)"
}
]
}
},
{
"name": "delete",
"execute": [
"set:p=json:nvl(params, '{\"id\":\"\"}')",
"set:sid=nvl(id, p.id)",
"nout:aux4 repository delete links --id ${sid}",
"log:{\"deleted\":\"${sid}\"}"
],
"help": {
"text": "Delete a link by slug",
"variables": [
{
"name": "id",
"text": "Link slug",
"arg": true,
"default": ""
},
{
"name": "params",
"text": "Path params (injected by aux4/api)",
"default": ""
}
]
}
}
]
}
]
}
Two things make this work as an API. aux4/api injects the request body as --body (read with value(body)) and path params as --params (a JSON object; ${p.id} after parsing). And create does the whole validate-then-store branch aux4-native: validate the body (--onlyValid emits the record only if it passes), and when: either returns a statusCode: 400 shape or writes to the repository and reads the record back for a 200.
Step 2: map routes to commands
This is the part that replaces the backend. One config.yaml maps HTTP routes to those commands (config.api) and holds the validator rules (config.validation):
config:
port: 8792
api:
"GET /links":
command: aux4 links list
"GET /links/{id}":
command: aux4 links get
"POST /links":
command: aux4 links create
"DELETE /links/{id}":
command: aux4 links delete
validation:
link:
slug:
path: $.slug
rule: required
url:
path: $.url
rule: required|url
That's the entire routing layer — four lines of route → command.
Step 3: start it and use it
aux4 api start --configFile config.yaml --port 8792
aux4 api started on http://0.0.0.0:8792
Routes are served under /api. Try to create a link with a bad URL — validation rejects it with a real 400:
aux4 curl request --method POST --url http://127.0.0.1:8792/api/links \
--header "Content-Type: application/json" --body '{"slug":"gh","url":"not-a-url"}' --showHeaders true
HTTP/1.1 400 Bad Request
{"error":"slug is required and url must be valid"}
A valid body is stored and returned with 200:
aux4 curl request --method POST --url http://127.0.0.1:8792/api/links \
--header "Content-Type: application/json" --body '{"slug":"gh","url":"https://github.com"}'
[ { "id": "gh", "slug": "gh", "url": "https://github.com" } ]
Add one more, then list and fetch:
aux4 curl request --url http://127.0.0.1:8792/api/links
[
{ "id": "gh", "slug": "gh", "url": "https://github.com" },
{ "id": "aux", "slug": "aux", "url": "https://aux4.io" }
]
aux4 curl request --url http://127.0.0.1:8792/api/links/gh
[ { "id": "gh", "slug": "gh", "url": "https://github.com" } ]
And delete:
aux4 curl request --method DELETE --url http://127.0.0.1:8792/api/links/gh
{ "deleted": "gh" }
Full CRUD — create with validation, read, list, delete — over HTTP. When you're done, aux4 api stop shuts the server down.
Conclusion
You built a REST API with no backend: aux4/api turns a config.yaml of routes into HTTP endpoints, each one an aux4 command; aux4/repository stores the records with no schema; and aux4/validator guards the body, returning genuine 400s. Swap links for contacts, notes, or feature flags and you have an internal tool in an afternoon — no framework, no ORM, no server code.