III The API

Lay Out the Routes

The shop’s API grows: the orders of one customer, the pending ones, the items of an order. Each gets a path. The orders are those of store.orders, the component Create a Component writes, and Expose It over HTTP gives it its first routes. The exposition of a component is a tree of paths, and each node of the tree may hold three kinds of keys:

  • routes — keys that start with /, each holding a nested node;
  • methods — keys named after HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, LOCK, UNLOCK;
  • directives — keys of the form family:name, such as io:output or cache:control.
# components/store.orders/manifest.toa.yaml
exposition:
  /:                       # route
    io:output: true        # directive
    GET: enumerate         # method
    /:id:                  # nested route
      GET: observe

Any other key is refused when the manifest is read: RTD parse error: unknown key 'hello'.

§Nested and flat routes

Routes nest, and a route key may hold several segments. These two declarations expose the same paths:

exposition:
  /:
    /pending:
      GET: ...
    /:id:
      /items:
        GET: ...
exposition:
  /pending:
    GET: ...
  /:id/items:
    GET: ...

They differ in what directives reach, see Directives are inherited.

A route key has no trailing slash. A node without nested routes must have at least one method.

§Route variables

A segment that starts with a colon is a variable. It matches any single segment of the path:

exposition:
  /:id:
    io:output: true
    GET: observe
GET /store/orders/4c4759e6f9c74da989d64511df42d6f4/ HTTP/1.1

A variable named id selects the entity: it becomes query.id of the call. Any other variable is matched against the entity property of the same name. A route listing the orders of one customer needs nothing but the variable:

exposition:
  /of/:customer:
    io:output: [id, status, total]
    GET: enumerate

GET /store/orders/of/b70a7dbc…f4db/ lists the orders whose customer is b70a7dbc…f4db. For POST, variables go to the input instead. Both rules are detailed in Map Methods to Operations, and a variable can be handed to the operation as an input property with map:segments.

A segment holds at most one variable, and the whole segment after the colon is its name: in /:sender-:recipient there is a single variable called sender-:recipient.

§Wildcards

* matches any single segment without naming it. ** matches the rest of the path, slashes included.

# components/pictures/manifest.toa.yaml
exposition:
  /*:
    GET: ...             # /pictures/anything/
  /thumbnails/*/small:
    GET: ...             # /pictures/thumbnails/anything/small/
  /archive/**:
    GET: ...             # /pictures/archive/2024/05/receipt/

* is what file routes use for the name of a stored file, see Upload and Download Files. The value matched by ** is available to map:segments under the name **.

§Which route answers

A route with a literal segment wins over a route with a variable in the same place:

exposition:
  /:id:
    GET: observe
  /pending:
    GET:
      endpoint: enumerate
      query:
        criteria: status==pending

GET /store/orders/pending/ reaches the second route, whichever is declared first. Between routes of the same specificity, the one declared first wins.

The same holds between components. Where the mount point of one coincides with a variable of another — a component store that declares /:id beside the component store.orders — the literal mount point wins, and /store/orders/ reaches store.orders.

§Forwarding

A route whose value is a path, not a node, is forwarded to that path. The destination is written in full, from the root:

# components/store.orders/manifest.toa.yaml
exposition:
  /of/:customer:
    io:output: [id, status, total]
    GET: enumerate
  /mine: /store/orders/of/b70a7dbca6b14a2eaac8a9eb4b2ff4db
  /by/:customer: /store/orders/of/:customer

GET /store/orders/by/b70a…f4db/ is answered as GET /store/orders/of/b70a…f4db/ would be. A variable of the forwarding route fills the variable of the same name in the destination.

§Intermediate nodes

A node that has a / route under it is intermediate: a request for its path is answered by the / node. Methods written on the intermediate node itself are unreachable and are refused.

exposition:
  /items:
    GET: enumerate       # invalid: unreachable
    /:
      POST: create

Write the methods under /, or drop the / and write them on the node.

§Directives are inherited

A directive applies to the node it is written on and to every node and method nested in it.

exposition:
  /of/:customer:
    auth:id: customer
    io:output: [id, status, total]
    GET: enumerate
    /:id:
      cache:control: max-age=60
      GET: observe

GET /store/orders/of/:customer/:id/ runs under auth:id, io:output and cache:control. A directive written on a method applies to that method only.

Where a node and something nested in it both state the same directive, the outcome depends on the family. Most take the nearest value: a nested cache:control replaces the outer one, a nested mcp:tool: false withdraws one method from a published subtree. io:output narrows instead: a method answers only the properties that both lists name. Each family’s article says which.

help: directives are the exception to inheritance: they describe the node they are written on and nothing under it, see Describe the API.

§Avoiding inheritance

To keep a nested path out of reach of its parent’s directives, declare it flat, beside the parent:

exposition:
  /of/:customer:
    auth:id: customer
    GET: enumerate
  /of/:customer/public:
    anonymous: true
    GET: ...

/of/:customer/public is not nested in /of/:customer, so auth:id does not apply to it.

Or mark the nested node isolated:

exposition:
  /of/:customer:
    auth:id: customer
    GET: enumerate
    /public:
      isolated: true
      anonymous: true
      GET: ...

An isolated node inherits no directives from the nodes above it — including those the Context declares on its /. It starts clean, so it states everything it needs itself: its access rule, its io:output.

§Shortcuts

Some directives are used often enough to have a short name, where the family is implied:

ShortcutDirective
inputio:input
outputio:output
languagesmap:languages
anonymousauth:anonymous
anyoneauth:anyone
idauth:id
roleauth:role
ruleauth:rule
inceptauth:incept
exposition:
  /:
    input: [customer, items, total]
    output: [id]
    POST: create

§Sharing declarations

Routes of one component often return the same set of properties. The manifest is YAML, so an anchor keeps the list in one place:

# components/store.orders/manifest.toa.yaml
exposition:
  /:
    io:output: &order [id, customer, items, total, status, VERSION]
    GET: enumerate
    /:id:
      GET: observe
  /of/:customer:
    io:output: *order
    GET: enumerate

§Mistakes in a tree

A method on an intermediate node, a leaf without a method and an unknown key are each refused when the manifest is read. A mistake in a tree stops the component from starting; it does not surface as a wrong answer later.

A path that no running component declares answers 404 with Route not found. That includes the paths of a component that was removed from the application, half an hour after it stopped.

Search
Search the documentation