Build an API
You end with routes with typed input and output, the committed API record, and the OpenAPI document.
On this page
An API in Jadpo is a set of routes, each saying what it takes in, what it sends back and what it runs, and Jadpo does the rest. It checks every request against the input type, turns failures into HTTP errors, keeps a committed record of the API and writes an OpenAPI document for it. This guide adds two routes to a book catalogue: one that adds a book and one that reads it back.
You start from an app with a Book entity, an action add_book that
stores one, and a query book_by_id that reads one.
Store and change a record and
Query records show how to write those.
Say what comes in and what goes out
A route's input is a type built on Input, and its output a type built
on Output. Each lists its fields; a field can take its type, rules
included, from an entity's field, so an input refuses the same values
the database would.
domains/:
use books/entities { Book }/// What a caller sends to add a book.type NewBook = Input { title: Book.title author: Book.author}
domains/:
use books/entities { Book }/// A book, as the API sends it.type BookView = Output { pick: [Book.id, Book.title, Book.author, Book.added_at]}
pick: sends exactly the entity's fields it names, with their types. A
field you add to Book later stays private until you pick it.
Add the routes
Routes live in a route group, in a domain's routes/ folder. The
group's prefix: starts every path in it.
domains/:
use books/entities { Book }use books/types { BookView, NewBook }use books/actions { add_book }use books/queries { book_by_id }/// The library's catalogue.routes Books { prefix: /books /// Adds a book. POST / { authentication: RouteAuthentication.none input: NewBook output: BookView status: SuccessStatus.created run: add_book(input) } /// One book. GET /{book_id: Book.id} { authentication: RouteAuthentication.none output: BookView run: book_by_id(book_id) }}
input:is read from the request's JSON body forPOST. A body that doesn't fitNewBookis answered422 invalid_input, listing each field's problem, and never reaches the action.status: SuccessStatus.createdanswers201. Without it a route answers200./is a path placeholder with its type. A value that isn't a valid id is a{book_id: Book.id} 422too.run:names the action or query the route runs, with the values it passes.- Every route needs a signed-in caller unless it says otherwise.
authentication: RouteAuthentication.nonelets anyone call these two; Add sign-in takes it away again.
book_by_id can fail with BookNotFound, the failure Jadpo declares
for every entity, so the route answers 404 book_not_found for a book
that isn't there. Handle failures declares
failures of your own.
Write the API record
Each API keeps a committed record of its routes, fields and failure
codes, so every change a client would notice shows as a diff of one
file. You never write it: jadpo check fails while it disagrees with
the routes, and jadpo api update rewrites it.
jadpo api updateWrote api/main.jadpo_record version 9c0c5d256937next: git diff api/ shows what changed for the API's clients
api/ (written by jadpo api update, never edited by
hand):
// Generated by Jadpo from the routes of API main; update it with `jadpo api update`, never by hand.record api 1POST /books authentication none input NewBook (body) output BookView status 201GET /books/{book_id: Uuid} authentication none output BookView status 200 fails book_not_foundGET /openapi.json generated openapi authentication none status 200output BookView added_at: Instant author: Text { min_length: 1, max_length: 100 } id: Uuid title: Text { min_length: 1, max_length: 200 }input NewBook author: Text { min_length: 1, max_length: 100 } title: Text { min_length: 1, max_length: 200 }failure book_not_found status 404
The app declares no API of its own, so it has one, called main.
GET / is Jadpo's: it serves the API's OpenAPI document.
Run jadpo api update again after every change to a route or a type it
uses, and commit the record with the code. A change that breaks
clients, such as removing a field, also needs a person's sign-off;
Get a person's sign-off covers that.
Call it
jadpo devjadpo dev, started 10:46:31listening on http://localhost:3000 routes 3 routes database disposable PostgreSQL, gone when dev stopsserving: saves reload, press ctrl-c to quit
In another terminal, add a book:
curl -i -X POST http://localhost:3000/books \ -H 'content-type: application/json' \ -d '{"title": "Middlemarch", "author": "George Eliot"}'
HTTP/1.1 201 Createdcontent-type: application/jsonx-request-id: req_449f27296665ceddcontent-length: 129date: Sat, 31 Oct 2026 10:46:33 GMT{"id":"01a19191-7b3e-7b55-a9a4-9b723e457c37","title":"Middlemarch","author":"George Eliot","added_at":"2026-10-31T10:46:33.022Z"}
Read it back with the id it gave you; yours differs:
curl http://localhost:3000/books/01a19191-7b3e-7b55-a9a4-9b723e457c37{"id":"01a19191-7b3e-7b55-a9a4-9b723e457c37","title":"Middlemarch","author":"George Eliot","added_at":"2026-10-31T10:46:33.022Z"}A book that isn't there is a 404, in the error body every client
gets:
{"error":{"code":"book_not_found","message":"Not found.","request_id":"req_0ba7b3845b9a0a4d"}}The OpenAPI document
jadpo check writes the API's OpenAPI 3.1 document to
build/, and the app serves the same document at
/. It lists every route with its input, its output, its
success status and the failures it can reach, and each / comment
becomes a description. Generate a client from it, or open it in any
OpenAPI viewer.
Read more
Route groups, Paths and placeholders, One input, Output and status, How failures reach the client, The public API record and OpenAPI.
Markdown for agents: /guides/guide-build-an-api.md.Built from Jadpo main at e00e61f. Jadpo's CI compiles this guide's code and runs its tests.