# Build a REST API Without Writing a Backend ![banner](https://articles.aux4.blog/data/posts/aux4/build-a-rest-api-without-writing-a-backend/banner.png) ## 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 `400`s. We'll build a link shortener. ## Install the packages ```bash 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: ```json { "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`): ```yaml 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 ```bash aux4 api start --configFile config.yaml --port 8792 ``` ```text 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`: ```bash 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 ``` ```text HTTP/1.1 400 Bad Request {"error":"slug is required and url must be valid"} ``` A valid body is stored and returned with `200`: ```bash 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"}' ``` ```json [ { "id": "gh", "slug": "gh", "url": "https://github.com" } ] ``` Add one more, then list and fetch: ```bash aux4 curl request --url http://127.0.0.1:8792/api/links ``` ```json [ { "id": "gh", "slug": "gh", "url": "https://github.com" }, { "id": "aux", "slug": "aux", "url": "https://aux4.io" } ] ``` ```bash aux4 curl request --url http://127.0.0.1:8792/api/links/gh ``` ```json [ { "id": "gh", "slug": "gh", "url": "https://github.com" } ] ``` And delete: ```bash aux4 curl request --method DELETE --url http://127.0.0.1:8792/api/links/gh ``` ```json { "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 `400`s. Swap `links` for contacts, notes, or feature flags and you have an internal tool in an afternoon — no framework, no ORM, no server code. ## See Also - [aux4/api](https://hub.aux4.io/r/public/packages/aux4/api) - [aux4/repository](https://hub.aux4.io/r/public/packages/aux4/repository) - [aux4/validator](https://hub.aux4.io/r/public/packages/aux4/validator) - [aux4/curl](https://hub.aux4.io/r/public/packages/aux4/curl)