Build a REST API Without Writing a Backend

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.

See Also