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
ReferenceHTTP

APIsHTTP

Output and status

On this page

output: names what the route answers with: an Output type, an object made to be sent to clients. It must be exactly what the action returns, so the route shows its contract without anyone opening the action.

A route that succeeds answers 200, unless status: says otherwise:

example
POST / {    input: CreateTodo    output: TodoView    status: SuccessStatus.created    run: create_todo(input, current_principal.user)}
status: HTTP Needs
left out (SuccessStatus.ok) 200 output:
SuccessStatus.created 201 output:
SuccessStatus.accepted 202 output:; the work carries on in the background (Background work)
SuccessStatus.no_content 204 no output:; the action returns Unit

Only the output's declared fields are sent. IssuedToken and IssuedKey, the built-in types for an issued sign-in token and a service's key (Sign-in and identity), are sent as the objects they are: an id, started_at and ends_at.

A route never writes a failure's status. It comes from the kind of each failure the action can produce (Failures).

Map layers

A route may also answer with a map layer: a FeatureCollection<G, P>, a collection of shapes of geometry G, each with properties of type P (Geospatial). It is sent bare, as the whole response, in plain RFC 7946 GeoJSON, so a mapping library fetches it as its source. A GET gets one from a query (Reads and writes, "A query giving a map layer"); a route with another method gets one from an action that builds it.

  • Only a collection, only bare. A List or Page of layers, or a lone Feature<G, P>, is route.output_not_output. A mapping library takes a collection as its source, and nothing asks for the others.
  • What P may not hold is what an Output's fields may not: a caller, a password's hash or a secret value, however deep. P is an Object, which could hold any of them, so the route checks it: a layer whose properties hold one is route.output_not_output at output:, naming what and where. The same search looks inside a feature's properties wherever a layer appears, so an Output with a layer field whose properties hold a password's hash is refused at that field.
  • Field policy. A layer an action returns is shown to its caller, just as an Output the action builds is. So every return of a FeatureCollection in an action carries only fields every caller of the action may read. Otherwise it is policy.output_field_unproved at the value returned (Field restrictions), whichever route or action calls it.
  • Sent as GeoJSON. The response's body is the collection as json.write writes it, {"type":"FeatureCollection","features":[...]}. A route test reads it back through the same codec, so response.output is the layer the action returned.

Sending raw bytes

A route's body doesn't have to be JSON. sends: names the media type of a body of raw bytes, on a route whose Output has exactly one field of a type built on Bytes. That field is the body, with nothing around it. Here a land registry keeps a scan of each deed, and a client opens the PDF it is sent:

example
/// A scan of a deed, as a PDF.type DeedScan = Output {    pdf: Bytes}
example
use titles/entities { Deed }use titles/types { DeedRef, DeedScan }use titles/failures { DeedNotFound }/// A deed's scan.query scan_of(deed_ref: DeedRef) fails DeedNotFound -> DeedScan {    from: Deed    where: Deed.deed_ref == deed_ref    missing: DeedNotFound}
example
use titles/types { DeedRef, DeedScan }use titles/queries { scan_of }/// Scans of deeds, by reference.routes Deeds {    prefix: /deeds    /// A deed's scan, which never changes once it is made.    GET /{deed_ref: DeedRef} {        authentication: RouteAuthentication.none        output: DeedScan        sends: "application/pdf"        cache: immutable        run: scan_of(deed_ref)    }}

In a route test, response.output is the Output, its field the bytes sent:

example
use titles/entities { Deed }use titles/types { DeedScan }use titles/actions { add_deed }test "a deed's scan is sent as it was kept" {    var deed: Deed = attempt add_deed()    var response: Response = request {        method: HttpMethod.get        path: "/deeds/{deed_ref}"        params: {            deed_ref: deed.deed_ref        }    }    assert response.status == 200    assert response.output == DeedScan {        pdf: hash.sha256("the scan")    }}test "a reference no deed has is not found" {    var response: Response = request {        method: HttpMethod.get        path: "/deeds/{deed_ref}"        params: {            deed_ref: "D999999"        }    }    assert response.status == 404    assert response.code == "deed_not_found"}
  • One field, always there. An Output of any other shape is route.sends_invalid, and so is a Bytes? field, a List or Page, a map layer or an AiStream. A body has no room for anything else, and nothing to send for none.
  • A media type it can write in a header. A lowercase type/subtype, with any parameters after a ;, as in text/csv; charset=utf-8. JSON isn't one: JSON is what an Output's own fields are sent as, so a route can't send a JSON body nothing declares. Nor is text/event-stream, which is what an AiStream is sent as.
  • What runs it is unchanged. output: is still exactly what the route runs returns. A GET runs a query or an action the compiler proves only reads, and any other method an action.
  • The response. The route's status, Content-Type as written, Content-Length and X-Content-Type-Options: nosniff, so a browser takes the body as what it says it is. A HEAD gets the same head, its length included, and no body.
  • Failures stay JSON. Everything but the success answers as any route's does, in the failure envelope, with Content-Type: application/json.
  • Not with settle:. That is route.sends_invalid: while the work carries on, settle: answers with how far it has got, which raw bytes can't say.
  • A repeat gets the same bytes. Under an Idempotency-Key, a route of another method keeps what it sent as any route does, to the byte.
  • The field is read as any field is. Policy and personal: marks apply whichever way it is sent: a field not every caller may read is policy.output_field_unproved, and a read of a marked field counts as one (Field restrictions, Marking personal fields). Bodies are never logged.
  • OpenAPI lists the success under the media type, with no schema, as OpenAPI 3.1 writes a binary body. x-jadpo-sends names the Output and the field, and Content-Length is one of its headers. Failures are listed as any route's are. An importer gives the operation no output:, since it reads only JSON.
  • The API record says sends application/pdf under the route, and the Output keeps its entry. A route that starts or stops sending raw bytes, or sends another media type, breaks whoever reads it, so it needs an acceptance: ApiChange.media_type_changed (Breaking changes).

Answers that never change

Nothing caches a route's answer unless the route says so. cache: immutable says the answer never changes, as a scan of a signed deed doesn't. The route's 200 then carries Cache-Control: public, max-age=31536000, immutable, so a browser or a CDN keeps it for a year and doesn't ask again. It goes with sends: or with JSON. A failure never carries the header, so a 404 for a deed not scanned yet isn't kept.

Anything else is route.cache_invalid:

  • Only immutable. It is the one thing cache: says.
  • Only a GET. No other method's answer can be kept.
  • Only a route anyone may call. A public cache would hand what one caller was sent to anyone who asked, so a route needing a signed-in caller can't say it.
  • Only an answer of 200. Any other status: is refused.
  • No personal data. No erasure reaches a public cache, so what the route runs can't give any, directly or through what it calls, and its output: can't say it holds any. It counts as the personal-data report counts: a field marked personal:, and a standard type that is personal data whatever holds it, such as PhoneNumber or Email, on a field with no mark as much as on its own (What the compiler checks).

OpenAPI says x-jadpo-cache: immutable on the operation and lists the header on the 200, and the API record says cache immutable under the route, as the record below shows. Adding it or taking it away breaks no client, so neither needs an acceptance.

example
// Generated by Jadpo from the routes of API main; update it with `jadpo api update`, never by hand.record api 1GET /deeds/{deed_ref: Text { pattern: "^D[0-9]{6}$" }}    authentication none    output DeedScan    sends application/pdf    status 200    cache immutable    fails deed_not_foundGET /openapi.json    generated openapi    authentication none    status 200output DeedScan    pdf: Bytesfailure deed_not_found    status 404    message "No deed has this reference."

Markdown for agents: /docs/output-and-status.md.Built from Jadpo main at aee2e8f.