III The API

Pass Headers, Language and Host to an Operation

The catalogue shows prices in the visitor’s currency and titles in their language; a payment provider’s callback has to be verified by its signature header. An operation receives the body as its input and the path as its query, and knows nothing of HTTP. The map directives put the rest of a request — headers, the language, the host, the claims of a token — into input properties, so the operation gets them as plain arguments. The require directives state what a request must carry.

§A header as an input property

The catalogue is a component of its own, store.catalog; browse here is its operation that lists products, with an input of currency, language and storefront.

# components/store.catalog/manifest.toa.yaml
exposition:
  /:
    io:output: true
    GET:
      endpoint: browse
      map:headers:
        currency: x-currency
$ curl http://localhost:8000/store/catalog/ -H 'x-currency: EUR'

browse receives input.currency set to EUR. The rules are the same for every map directive:

  • The operation’s input must be an object.
  • A mapped property overwrites one the client sent. Whatever is mapped is the route’s to fill, not the caller’s.
  • A mapped property is not subject to io:input. A route may forbid every body property and still fill some itself.
  • In the answer to OPTIONS a mapped property is taken out of the input schema, because a client has nowhere to send it.

§Headers

map:headers maps input properties to request headers. Keys are input properties, values are header names:

map:headers:
  currency: x-currency
  agent: user-agent

A header sent several times arrives as one comma-separated value. A header that is absent leaves the property unset; make the header mandatory with require:header, or the property optional in the operation’s input.

Each mapped header is added to the reply’s vary. A browser on another origin may send any of them (CORS).

§Route variables

Route variables normally go to the query (or, for POST, to the input) on their own, see Map Methods to Operations. map:segments puts a variable into the input under a name of your choice. Keys are input properties, values are variable names. amend here is a transition of store.orders that changes one item of an order and takes the product in its input:

# components/store.orders/manifest.toa.yaml
exposition:
  /:id/items/:product:
    io:output: true
    PUT:
      endpoint: amend
      map:segments:
        product: product

PUT /store/orders/4c47…d6f4/items/kettle/ calls amend on the order 4c47…d6f4 with input.product set to kettle.

A variable that is mapped this way is still used for criteria and still seen by other directives. Prefix its name with ~ to take it out of both:

      map:segments:
        product: ~product

Use ~ when the variable is an argument and not a property of the entity. Without it, product==kettle would be added to the criteria, and an order has no such property.

The rest of a path matched by ** is mapped under the name '**'. pages here is a component whose render answers the page at input.path:

# components/pages/manifest.toa.yaml
exposition:
  /**:
    io:output: true
    GET:
      endpoint: render
      map:segments:
        path: '**'

GET /pages/help/shipping/ calls render with input.path set to help/shipping.

§Language

map:languages lists the languages the application supports, and map:language puts the one that best matches the request’s accept-language into an input property:

# components/store.catalog/manifest.toa.yaml
exposition:
  /:
    map:languages: [en, fr]
    io:output: true
    GET:
      endpoint: browse
      map:language: language
GET /store/catalog/ HTTP/1.1
accept-language: fr-CA, fr;q=0.9, en;q=0.5
HTTP/1.1 200 OK
content-language: fr
vary: origin, accept-language, accept

browse receives input.language set to fr. If none of the supported languages matches, the first of the list is used. The reply carries content-language and varies on accept-language.

The list is usually the same for the whole application. Declare it once, in the Context, with its shortcut:

# context.toa.yaml
exposition:
  authorities:
    local: localhost:8000
  /:
    languages: [en, fr]

§Client address and country

map:ip and map:country put the client’s address and country into an input property, as the gateway reads them from the headers ip and country name:

# components/store.catalog/manifest.toa.yaml
exposition:
  /:
    POST:
      endpoint: record
      map:ip: ip
      map:country: country

The property is null where the request has none, and is set on every call, so a body that names it is overwritten rather than believed.

§The host

An application may be served on several hosts. The Context gives each one an identifier, an authority. toa create wrote one, local: localhost:8000; a shop served on two hosts names both instead:

# context.toa.yaml
exposition:
  authorities:
    shop: api.store.example
    outlet: api.outlet.example

map:authority puts the identifier of the host a request was sent to into an input property:

# components/store.catalog/manifest.toa.yaml
exposition:
  /:
    io:output: true
    GET:
      endpoint: browse
      map:authority: storefront

A request to api.outlet.example calls browse with input.storefront set to outlet. This is how one deployment serves several storefronts with different catalogues.

A host that is not among the authorities is passed as it is, lowercased: api.unknown.example. A host that is not a host name (with an optional port) is answered 400.

An identity and its credentials belong to one authority, so renaming an identifier invalidates them; see Authorities. Hosts are given per environment in Run and Configure the Gateway.

§Token claims

map:claims maps the claims of the request’s Bearer token to input properties. Keys are input properties, values are claim names. accounts is the component of customers that Sign Users Up and In writes; open here is an operation of it that opens an account for the holder of the token:

# components/accounts/manifest.toa.yaml
exposition:
  /:
    POST:
      endpoint: open
      map:claims:
        email: email
        verified: email_verified

If the request is not authenticated with a Bearer token, or the token has no such claim, the property is not set. Which tokens are accepted is described in Credentials.

§The raw body

map:buffer hands the body to the operation as it was sent, unparsed. The value is the input property that receives it. payments.webhooks is the component that receives what a payment provider sends, and payments.gateway.decode the operation that checks the provider’s signature; Receiving Webhooks writes both:

# components/payments.webhooks/manifest.toa.yaml
exposition:
  /:
    POST:
      anonymous: true
      map:buffer: payload
      map:headers:
        signature: x-signature
      io:output: false
      endpoint: ingest
// components/payments.webhooks/operations/ingest.ts
import type { Context, IngestInput, State } from '../types/index.d.ts'

export async function transition (input: IngestInput, entry: State, context: Context) {
  const notification = await context.remote.payments.gateway.decode({
    input: { payload: input.payload, signature: input.signature ?? '' }
  })

  if (notification instanceof Error) return notification

  // ...
}

This is what a webhook needs: a signature is computed over the exact bytes, which parsing and re-encoding would not preserve.

  • The body arrives as a UTF-8 string.
  • It is not parsed, so the request may carry any content-type.
  • The route receives no parsed body: the input holds the buffer and whatever else is mapped.
  • A route with map:buffer is served over HTTP only. It is not available as a procedure.

§The body as a stream

map:stream hands the body to the operation as a stream, for content that has to flow through an operation without being held in memory. It is described in Stream Through the API. A route cannot declare both map:stream and map:buffer.

§The process

map:instance names the route variable that carries the process a stateful operation is called on. It is described in Stream Through the API.

§Requiring headers

require:header refuses a request that lacks a header. require:headers takes a list:

# components/store.orders/manifest.toa.yaml
exposition:
  /:id:
    require:header: if-match
    io:output: [id, status]
    PUT: approve
PUT /store/orders/4c4759e6f9c74da989d64511df42d6f4/ HTTP/1.1
accept: text/plain
HTTP/1.1 400 Bad Request

Header required: if-match

Requiring if-match makes every write to the route conditional on the version the client read, so no client can overwrite a change it has not seen; see Prevent lost updates.

require:headers: [if-match, x-currency]

Like every directive, a requirement on a node applies to all methods under it.

§Summary

DirectiveValueFills the property with
map:headers{ property: header }The header’s value.
map:segments{ property: variable }The route variable; ~variable also removes it from criteria.
map:languageslist of language codesNothing: declares the supported languages.
map:languagepropertyThe best supported language for accept-language.
map:authoritypropertyThe identifier of the host.
map:ippropertyThe client address, or null.
map:countrypropertyThe client’s country, or null.
map:claims{ property: claim }The claim of the Bearer token.
map:bufferpropertyThe unparsed body, as a string.
map:streamproperty, or an objectThe body as a stream.
map:instancevariableNothing: names the process to call.
require:headerheaderNothing: refuses a request without it.
require:headerslist of headersNothing: refuses a request that lacks one of them.
Search
Search the documentation