III The API

Build an API for a Component

A client of the shop lists the orders, creates one, reads one, approves it. Each of these is one line in the exposition section of the manifest of store.orders, the component Create a Component writes. The gateway, a service of the runtime, serves what the section declares.

§More routes and methods

This is the section as Expose It over HTTP writes it:

# components/store.orders/manifest.toa.yaml
exposition:
  /:
    anonymous: true
    io:output: true
    GET: enumerate
    POST: create
    /:id:
      GET: observe
      PUT: approve
RequestCall
GET /store/orders/store.orders.enumerate
POST /store/orders/store.orders.create, with the body as input
GET /store/orders/4c47…d6f4/store.orders.observe of that order
PUT /store/orders/4c47…d6f4/store.orders.approve of that order

Creating an order sends its properties as the body:

POST /store/orders/ HTTP/1.1
host: localhost:8000
content-type: application/yaml
accept: application/yaml

customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
items:
  - product: kettle
    quantity: 1
total: 4200
HTTP/1.1 201 Created
content-type: application/yaml

id: 99988d785d7d445cad45dbf8531f560b

The route variable :id selects the order: it becomes the id of the call’s query.

GET /store/orders/99988d785d7d445cad45dbf8531f560b/ HTTP/1.1
host: localhost:8000
accept: application/yaml
HTTP/1.1 200 OK
content-type: application/yaml

id: 99988d785d7d445cad45dbf8531f560b
customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
status: pending
total: 4200

enumerate and observe come from the generic prototype; create and approve are the component’s own operations.

§Two defaults to remember

  • A method that declares no io:output answers no body. The operation runs and the request succeeds with 204 No Content. io:output: true lets the whole reply through; a list names the properties a client receives. See Choose What a Route Accepts and Returns.
  • A route that declares no access rule is answered 401 Unauthorized. anonymous: true lets anyone call it. Real rules are declared with auth: directives, see Access.

Both are written once on / above and apply to every method below it. Keys of the form family:name are directives; a directive on a node applies to that node and everything nested in it.

§Where a component is mounted

The routes of a component are mounted under /{namespace}/{name}, or /{name} for a component without a namespace. Route keys in the manifest are relative to that prefix.

# components/store.orders/manifest.toa.yaml
exposition:
  /: ...
  /:id: ...
  /:id/items: ...
/store/orders/
/store/orders/:id/
/store/orders/:id/items/

A request path ends with a slash. The route key in the manifest does not.

GET /store/orders HTTP/1.1
HTTP/1.1 404 Not Found

"Trailing slash is required"

The only paths without a trailing slash are those of stored files, see Upload and Download Files.

§Try it

Restart the application after a change to a manifest, and send a request with curl:

$ npm run restart
$ curl -i http://localhost:8000/store/orders/ 
    -H 'content-type: application/json' 
    -d '{"customer": "b70a7dbca6b14a2eaac8a9eb4b2ff4db", "items": [], "total": 4200}'

Or keep the requests as a scenario, written with the steps the application came with; they are explained in Write a Test. A value captured from one response is used in the next request:

# features/orders.feature
Feature: Orders

  Scenario: Create an order and read it back
    When the request is sent:
      """
      POST /store/orders/ HTTP/1.1
      content-type: application/yaml
      accept: application/yaml

      customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
      items:
        - product: kettle
          quantity: 1
      total: 4200
      """
    Then the response is received:
      """
      201 Created

      id: ${{ order.id }}
      """
    When the request is sent:
      """
      GET /store/orders/${{ order.id }}/ HTTP/1.1
      accept: application/yaml
      """
    Then the response is received:
      """
      200 OK

      status: pending
      total: 4200
      """

A scenario starts the components itself and sends its requests to the gateway, which is part of the sys process:

$ npm run sys
$ npm run features

§The host

context.toa.yaml is the Context: what is true of the application as a whole. It names each host the application is served on, under an identifier. toa create wrote one, for a local run:

# context.toa.yaml
exposition:
  authorities:
    local: localhost:8000

authorities is the only required option of the Context’s exposition. Hosts per environment and the other options are in Run and Configure the Gateway.

§Routes declared in the Context

The Context may declare routes of its own under the / key of its exposition. They are mounted at the root, not under a component’s name, so this is where an application gives a resource an address that does not follow /{namespace}/{name}:

# context.toa.yaml
exposition:
  authorities:
    local: localhost:8000
  /:
    /pending:
      anonymous: true
      io:output: [id, total]
      GET:
        endpoint: store.orders.enumerate
        query:
          criteria: status==pending

GET /pending/ now lists pending orders. An endpoint in the Context is written in full: namespace.component.operation, or component.operation for a component without a namespace.

Where a route of the Context and a route of a component have the same path, the Context’s wins.

Directives placed on the Context’s / apply to the whole application, since every component’s tree is nested under it:

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

languages is the shortcut of map:languages.

§The long form

exposition is a shortcut. The same declaration can be written as an extension of the manifest, and the Context’s part as an annotation:

# components/store.orders/manifest.toa.yaml
extensions:
  '@toa.io/extensions.exposition':
    /:
      GET: enumerate
# context.toa.yaml
annotations:
  '@toa.io/extensions.exposition':
    authorities:
      local: localhost:8000

Use the shortcut.

§A component without exposition

A component that declares no exposition is not reachable over HTTP and is otherwise complete: other components call it, receivers and scheduled work run in it. Expose what clients need, and leave the rest inside.

Nothing is registered by hand: a component that starts brings its routes with it, and the gateway is not restarted when a manifest changes. Why a component holds no server of its own is in Resources.

Search
Search the documentation