Skip to content

The OpenAPI documents

Both published APIs describe themselves in OpenAPI 3.1, generated from the Go types the handlers use, so the schemas cannot drift from the implementation.

Both published APIs serve a complete OpenAPI 3.1 description of themselves.

ServiceDocument
Roosthttps://roost.ceagle.cc/api/openapi.json
Accountshttps://account.ceagle.cc/api/openapi.json

These docs render them at Roost and Accounts.

The developer console is not among them. Its API is not published, so there is no document to fetch.

curl -s https://roost.ceagle.cc/api/openapi.json > roost.json

Each is served with a permissive cross-origin header and a short cache, so a browser-based tool can fetch one directly.

They are generated#

The descriptions are not written by hand and kept in step. Each service builds its document at startup by reflecting over the same Go types its handlers bind and return.

That has a few consequences worth relying on:

Schemas cannot drift

A field added to a response type appears in the document without anyone remembering to add it.

Validation becomes constraints

The validation tags on request types become required, maxLength, minimum, enum and format in the schema.

Property order is preserved

Fields appear in struct order rather than alphabetically, so a generated client reads like the Go type.

It matches what is actually running

Paths appear only when their routes are registered, so the document never advertises something that is not there.

What the reflection produces#

A request type like this:

type listingRequest struct {
	Title      string   `json:"title" validate:"required,min=1,max=64"`
	Summary    string   `json:"summary" validate:"omitempty,max=140"`
	Categories []string `json:"categories" validate:"required,min=1,max=4,dive,required,max=32"`
	Hidden     bool     `json:"hidden"`
}

becomes:

{
  "type": "object",
  "properties": {
    "title": { "type": "string", "minLength": 1, "maxLength": 64 },
    "summary": { "type": "string", "maxLength": 140 },
    "categories": {
      "type": "array",
      "minItems": 1,
      "maxItems": 4,
      "items": { "type": "string", "maxLength": 32 }
    },
    "hidden": { "type": "boolean" }
  },
  "required": ["title", "categories", "hidden"]
}
How tags are read
required
Adds the field to required.
omitempty
Leaves it optional, as does a pointer field or a JSON omitempty.
min, max
minLength/maxLength for strings, minimum/maximum for numbers, minItems/maxItems for arrays.
oneof
Becomes an enum.
email, url, uuid, hostname
Become the matching format.
dive
Applies the rules after it to the array's items rather than the array.

Embedded structs are flattened exactly the way encoding/json flattens them, so a response type that embeds another produces one object rather than a nested one.

Generating a client#

The documents are ordinary OpenAPI 3.1, so any generator works.

bunx openapi-typescript https://roost.ceagle.cc/api/openapi.json -o roost.d.ts

Keeping a generated client honest#

  1. Commit the document you generated from

    Alongside the generated code, so both move in one commit.

  2. Re-fetch it on a schedule

    A daily job that downloads the document and diffs it against the committed copy turns a silent schema change into a pull request.

  3. Ignore unknown fields

    Fields are added within v1 and never removed or repurposed, so a client that tolerates unknown fields keeps working across additions.

Extensions used#

The documents carry a few vendor extensions that renderers understand and generators ignore:

x-tagGroups
Groups tags into sections in the sidebar.
x-codeSamples
Hand-written examples on operations where the generated one is not the useful one.
x-badges
Small labels shown next to an operation name.
CEagle documentation