III The API

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

KeySays
title, descriptionWhat the method is, from help:method.
routeThe route variables, as schemas.
queryThe parameters the method declares.
selectionWhat selects records: criteria, sort, limit, omit, search, with their bounds.
inputThe input schema, restricted by io:input, without what the route fills itself.
outputThe output schema, restricted by io:output. Absent where no body is returned.
errorsThe error codes the operation may return.
headersInput properties that map:headers fills, and their headers.
octetsWhat an upload takes: accept, limit, stream. See Upload and Download Files.
oncetrue where a retry under an idempotency-key is safe.
readonlyfalse where a GET is declared to write.
anonymousReaching it takes no credential.
authenticatedReaching it takes being someone, whoever.
privateReaching it takes being the identity it is about.
protectedReaching it takes a role.
systemThe role it takes is one that guards what the application runs on.

Details worth knowing:

  • A closed criteria is not listed in selection: the method does not take one.
  • An operation that declares no output is 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’s output.
  • A route variable handed to the input by map:segments appears under route by 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
  • routes is keyed by route template. Each entry is what OPTIONS on that path answers.
  • A key has no trailing slash. To call a route, append one: GET /store/orders/:id/.
  • A resource carries private, protected and system where 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, beside routes, is the address MCP is served at, where it has a host of its own. Where it is absent, MCP is at /.mcp of 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
DirectiveDescribes
help:nodeThe resource.
help:methodOne method of it.
help:routeRoute variables, by name.
help:queryDeclared 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:segments hands 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 :id taken 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

RequestAnswers
OPTIONS <path>/The methods of one resource the caller may reach.
OPTIONS /.discoveryEvery resource the caller may reach.
GET /.discovery/A page that shows the tree and calls its methods.
GET / from a browserThat page, unless the application declares a route at /.
Search
Search the documentation