Describe the API
The front-end team asks what each route takes and returns; a partner wants a list of everything they may call. Nobody writes that document: the API answers the question itself, from the same declarations that serve the requests, filtered by what the caller may reach.
There are three parts: OPTIONS on a resource, the whole tree at /.discovery, and the help directives with which an application describes its resources in its own words.
§Asking a resource
Send OPTIONS to any path. This one is of store.orders, whose routes Expose It over HTTP declares; the reply below is for
routes that answer id, status and total of an order, as in Describing resources:
$ curl -X OPTIONS http://localhost:8000/store/orders/ -H 'accept: application/yaml' OPTIONS /store/orders/ HTTP/1.1
accept: application/yaml HTTP/1.1 200 OK
Allow: GET, POST
GET:
selection:
criteria:
type: string
sort:
type: string
limit:
type: integer
minimum: 1
maximum: 100
default: 10
omit:
type: integer
minimum: 0
maximum: 1000
default: 0
output:
type: array
items:
type: object
properties:
id:
type: string
pattern: ^[a-fA-F0-9]{32}$
status:
type: string
enum: [pending, approved, cancelled]
total:
type: integer
POST:
input:
type: object
properties:
customer:
type: string
items:
type: array
total:
type: integer
required: [customer, total]
output:
type: object
properties:
id:
type: string
pattern: ^[a-fA-F0-9]{32}$ The reply has one key per method the caller may reach, and Allow lists the same methods. The
path may be a real one or the route template itself: OPTIONS /store/orders/:id/ answers too.
§What is said of a method
| Key | Says |
|---|---|
title, description | What the method is, from help:method. |
route | The route variables, as schemas. |
query | The parameters the method declares. |
selection | What selects records: criteria, sort, limit, omit, search, with their bounds. |
input | The input schema, restricted by io:input, without what the route fills itself. |
output | The output schema, restricted by io:output. Absent where no body is returned. |
errors | The error codes the operation may return. |
headers | Input properties that map:headers fills, and their headers. |
octets | What an upload takes: accept, limit, stream. See Upload and Download Files. |
once | true where a retry under an idempotency-key is safe. |
readonly | false where a GET is declared to write. |
anonymous | Reaching it takes no credential. |
authenticated | Reaching it takes being someone, whoever. |
private | Reaching it takes being the identity it is about. |
protected | Reaching it takes a role. |
system | The role it takes is one that guards what the application runs on. |
Details worth knowing:
- A closed
criteriais not listed inselection: the method does not take one. - An operation that declares no
outputis described by what its type returns: a transition, an observation and an assignment are said to answer the entity, or a list of entities. A computation and an effect are not guessed at. To have an accurate description, declare the operation’soutput. - A route variable handed to the input by
map:segmentsappears underrouteby the name of the property it fills. - The access keys follow from the route’s
auth:directives, see Access.
§What a caller sees
The answer is for the caller who asked:
- a method they may not reach is not in the reply and not in
Allow; - a resource whose every method they may not reach answers
403; - signing in is what adds to it.
A route that is public is described to anyone, whether or not the request carries a credential.
An OPTIONS request that carries access-control-request-method is a CORS preflight and is answered as one. Without that
header it is introspection, also from a browser page.
§The whole tree
OPTIONS /.discovery answers everything the caller may reach, at once:
OPTIONS /.discovery HTTP/1.1
accept: application/yaml HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS
cache-control: private, max-age=1800
routes:
/store/orders:
title: Orders
GET:
title: Every order
selection: ...
output: ...
POST:
input: ...
/store/orders/:id:
protected: true
GET:
protected: true
route:
id: { type: string, title: Which order }
output: ...
PUT:
protected: true
once: true
errors:
- NOT_PENDING routesis keyed by route template. Each entry is whatOPTIONSon that path answers.- A key has no trailing slash. To call a route, append one:
GET /store/orders/:id/. - A resource carries
private,protectedandsystemwhere any of its methods does. - A method the caller may not reach is absent, and a resource with no reachable method is not listed.
mcp, besideroutes, is the address MCP is served at, where it has a host of its own. Where it is absent, MCP is at/.mcpof the same origin.
The reply may be kept for half an hour and is private: it is what one identity may reach.
Use it to generate a client, to check what a role can see, or to find the name of a procedure. A scenario keeps the check. This one holds where
an order is read and approved by staff only: the routes by :id are declared flat, beside /,
so that its anonymous: true does not reach them, and are
given a role:
# components/store.orders/manifest.toa.yaml
exposition:
/:
anonymous: true
io:output: true
GET: enumerate
POST: create
/:id:
auth:role: store:orders
io:output: true
GET: observe
PUT: approve The step that checks what a response lacks is not among the generated ones; add it:
// features/steps/http.ts
Then('the response does not contain:', function (this: World, text: string) {
this.agent.responseExcludes(text)
}) # features/discovery.feature
Feature: What a visitor is shown
Scenario: An anonymous visitor does not see order routes
When the request is sent:
"""
OPTIONS /.discovery HTTP/1.1
accept: application/yaml
"""
Then the response is received:
"""
200 OK
"""
And the response does not contain:
"""
/store/orders/:id
""" $ npm run env
$ npm run sys
$ npm run features npm run sys starts the gateway the requests go to; the scenario starts the components itself,
see Write a Test.
§The page
GET /.discovery/ is a page that shows the tree, served by the gateway from the same origin as
the API.
- It lists resources and methods with their titles and descriptions, and schemas of what each takes and returns.
- A method can be called from it: what the method takes is a form, and the reply is shown in its place. The call is made as whoever is reading, with the same effect as any other request.
- It opens without a credential, also for a client whose token has expired, and shows what that reader may reach.
- Where the application publishes tools, it shows the address to give an MCP client.
Opening the application’s address in a browser leads here: a GET / whose accept prefers a
page, or asks for nothing in particular, is sent to the page. A client that asks for a format
the gateway answers gets what it asked for, and a route the application declares at / keeps
answering /.
§Turning it off
Discovery is served wherever a gateway is, and no option turns it on or off. An application that
does not want to publish its routes blocks /.discovery at the ingress. Since the tree is
filtered by access, what an anonymous reader sees is only what is open to everyone anyway.
§Describing resources
Schemas say what a resource takes. The help directives say what it is, in the application’s
words. One declaration is read everywhere a resource is described: OPTIONS, the tree, the
page, and the tools a model sees.
# components/store.orders/manifest.toa.yaml
exposition:
/:
help:node: Orders
io:output: [id, status, total]
GET:
help:method: Every order
endpoint: enumerate
/:id:
GET:
help:method:
title: One order
description: The order by its id, with its status and total.
help:route:
id: Which order
endpoint: observe | Directive | Describes |
|---|---|
help:node | The resource. |
help:method | One method of it. |
help:route | Route variables, by name. |
help:query | Declared query-string parameters, by name. |
All take the same kind of value. A string is a title: short, something a client has a place to
show. An object may hold a title, a description, or both:
help:method:
title: Approve
description: >-
Approves a pending order. Refused with NOT_PENDING if the order was approved
or cancelled before. help:route and help:query take an object keyed by parameter name, each value a string or
such an object:
# a component with an operation `quote` whose input is { product, quantity }
exposition:
/quote:
GET:
endpoint: quote
query:
parameters: [product, quantity]
help:query:
product: Product id
quantity:
title: How many
description: A whole number of units, at least one. help:query describes the parameters a method declares.
The standard ones — criteria, sort and the rest — are reported as selection and are not
described here.
§Where descriptions appear
help:node appears beside the methods, help:method inside its method, and a parameter’s in
its schema:
title: Orders
GET:
title: Every order
query:
product:
type: string
title: Product id - Describe a parameter by the name it has in the answer. A variable that
map:segmentshands to an input property is listed under the property’s name, so describe it under that. - Describing a route variable puts it into the answer even where the operation does not declare
it — an
:idtaken by the query, for instance. - A name that matches nothing is ignored.
§Help is not inherited
Every other directive applies to the nodes below it. help applies only to the node it is
written on: carried down, it would say of every nested resource that it is the one above.
exposition:
/:
help:node: Orders # describes /store/orders, not /store/orders/:id
GET: enumerate
/:id:
GET: observe The exception is a node whose / answers in its place, because the two are one resource:
exposition:
/archive:
help:node: Archived orders # describes /store/orders/archive/
/:
GET: enumerate help:node on a node that serves nothing is refused when the manifest is read.
§Hiding a method
help:method: null hides a method from every description:
# a component with an operation `check`
exposition:
/health:
GET:
help:method: null
endpoint: check The method is in no answer: not in OPTIONS, not in the tree, not among tools. It is reached
exactly as a described one is. Use it for what a monitor calls and what a person has no reason to
look for. A resource whose every method is hidden answers 403 to OPTIONS and is not in the
tree.
Hiding is not protecting. Access is decided by auth: directives only.
§Titles are not names
A title is for people. The address of a method — its path and verb, or its procedure name store.orders._id.GET — is what code uses, and it does not change when a title does.
An operation’s own description in the component’s manifest is not used here: one operation mounted on two routes is two methods that need two descriptions.
§Summary
| Request | Answers |
|---|---|
OPTIONS <path>/ | The methods of one resource the caller may reach. |
OPTIONS /.discovery | Every resource the caller may reach. |
GET /.discovery/ | A page that shows the tree and calls its methods. |
GET / from a browser | That page, unless the application declares a route at /. |