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 | Request | Call |
|---|---|
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:outputanswers no body. The operation runs and the request succeeds with204 No Content.io:output: truelets 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: truelets anyone call it. Real rules are declared withauth: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.