Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
Guides: all 21

Guides

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 starts with the smallest route there is, then adds two to a book catalogue: one that adds a book and one that reads it back.

For those two 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. The first route needs none of them.

Start with one route

A route says what it sends back and what it runs. It goes in a route group, in a domain's routes/ folder, and the group's prefix: starts every path in it.

domains/books/routes/greetings.jadpo:

jadpo
use books/types { Greeting }use books/actions { greet }/// The catalogue's front door.routes Greetings {    prefix: /greetings    /// Says hello.    GET / {        authentication: RouteAuthentication.none        output: Greeting        run: greet()    }}

Every route needs a signed-in caller unless it says otherwise, and authentication: RouteAuthentication.none, built into Jadpo, lets anyone call this one. Greeting and greet are yours, in the files below.

What the route sends back is a type built on Output, which lists its fields.

domains/books/types/greeting.jadpo:

jadpo
/// What the catalogue says to anyone who asks.type Greeting = Output {    message: Text}

What it runs is an action.

domains/books/actions/greet.jadpo:

jadpo
use books/types { Greeting }/// Says hello.action greet() -> Greeting {    return Greeting {        message: "Welcome to the catalogue"    }}

An action takes its permission from the stored records it touches. This one touches none, so one line of policy.jadpo, at the root of the project, says who may run it.

policy.jadpo:

jadpo
use bookspolicy {    actions: {        books.greet: [Access.public]    }}

Say what comes in and what goes out

A book's routes take data in as well. 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/books/types/new_book.jadpo:

jadpo
use books/entities { Book }/// What a caller sends to add a book.type NewBook = Input {    title: Book.title    author: Book.author}

domains/books/types/book_view.jadpo:

jadpo
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

domains/books/routes/books.jadpo:

jadpo
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 for POST. A body that doesn't fit NewBook is answered 422 invalid_input, listing each field's problem, and never reaches the action.
  • status: SuccessStatus.created answers 201. Without it a route answers 200.
  • /{book_id: Book.id} is a path placeholder with its type. A value that isn't a valid id is a 422 too.
  • run: names the action or query the route runs, with the values it passes.
  • authentication: RouteAuthentication.none lets 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.

shell
jadpo api update
output
Wrote api/main.jadpo_record    version  030840e864d3next: git diff api/  shows what changed for the API's clients

api/main.jadpo_record (written by jadpo api update, never edited by hand):

text
// 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 /greetings    authentication none    output Greeting    status 200GET /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 }output Greeting    message: Textinput 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 /openapi.json 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

shell
jadpo dev
output
jadpo dev, started 10:46:31listening on http://localhost:3000    routes    4 routes    database  disposable PostgreSQL, gone when dev stopsserving: saves reload, press ctrl-c to quit

In another terminal, call the first route:

shell
curl http://localhost:3000/greetings
text
{"message":"Welcome to the catalogue"}

Add a book:

shell
curl -i -X POST http://localhost:3000/books \  -H 'content-type: application/json' \  -d '{"title": "Middlemarch", "author": "George Eliot"}'
text
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:

shell
curl http://localhost:3000/books/01a19191-7b3e-7b55-a9a4-9b723e457c37
text
{"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:

text
{"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/openapi/main.json, and the app serves the same document at /openapi.json. 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 aee2e8f. Jadpo's CI compiles this guide's code and runs its tests.