# Build an API

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](/guides/guide-store-and-change-a-record.md) and
[Query records](/guides/guide-query-records.md) 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 books

policy {
    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](/guides/guide-add-sign-in.md) 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](/guides/guide-handle-failures.md) 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.

```sh
jadpo api update
```

```text
Wrote api/main.jadpo_record
    version  030840e864d3

next: 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 1

POST /books
    authentication none
    input NewBook (body)
    output BookView
    status 201

GET /books/{book_id: Uuid}
    authentication none
    output BookView
    status 200
    fails book_not_found

GET /greetings
    authentication none
    output Greeting
    status 200

GET /openapi.json
    generated openapi
    authentication none
    status 200

output 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: Text

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 /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](/guides/guide-get-a-sign-off.md) covers that.

## Call it

```sh
jadpo dev
```

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

In another terminal, call the first route:

```sh
curl http://localhost:3000/greetings
```

```text
{"message":"Welcome to the catalogue"}
```

Add a book:

```sh
curl -i -X POST http://localhost:3000/books \
  -H 'content-type: application/json' \
  -d '{"title": "Middlemarch", "author": "George Eliot"}'
```

```text
HTTP/1.1 201 Created
content-type: application/json
x-request-id: req_449f27296665cedd
content-length: 129
date: 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:

```sh
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](/docs/route-groups.md),
[Paths and placeholders](/docs/paths-and-placeholders.md),
[One input](/docs/one-input.md),
[Output and status](/docs/output-and-status.md),
[How failures reach the client](/docs/how-failures-reach-the-client.md),
[The public API record](/docs/the-public-api-record.md) and
[OpenAPI](/docs/openapi.md).
