This is a technical document about how routing in Amphora is performed. It also informs any further technical decisions about the growth of the library, and contains any ideas that we're adhering to for consistency.
For a broader and less specific overview of routing, please see the project's readme document
/components
/components/:name
/components/:name@:version
/components/:name.html
/components/:name.json
/components/:name@:version.html
/components/:name@:version.json
/components/:name/instances
/components/:name/instances/:id
/components/:name/intsances/:id.html
/components/:name/intsances/:id.json
/components/:name/instances/:id@:version
/components/:name/instances/:id@:version.html
/components/:name/instances/:id@:version.json
GET /components
will return a list of the available known components. This will be all the components defined in the /components
folder of the current application, and also any components installed through the node package manager (npm).
Example: GET /components
[
"text",
"image",
"paragraph",
"article"
]
GET /components/:name/instances
will return a list of the available instances within a particular component type.
Example: GET /components/text/instances
[
"/components/text/instances/abc",
"/components/text/instances/def",
"/components/text/instances/def@published"
]
You can grab the default data for a component, data for a specific instance, or even data for a specific version (of an instance). GETs and PUTs to these endpoints behave as you would expect from a RESTful api (they return what you send them). GETs and PUTs to the .json
and .html
extensions also work, but they'll return the composed (i.e. a component and its children) JSON and HTML, respectively.
GET /components/:name[/instances/:id][@:version[.:extension]]
will return a component. The form the data is based on the extension (.html, .json, .yaml), or the Accepts header of the request.
Requesting data without a version will return the latest saved version. Some versions have special rules (see Propagating Versions), but any other version name can be used to specially tag any particular version.
PUT /components/:name[/instances/:id][@version]
will save the data such that GET
ing the same uri will return exactly what was put there.
Components may have pre-render and pre-save hooks, which do logic before rendering and saving, respectively. To add these, create a model.js
file in the component's folder, and export render
and/or save
methods. Both of these methods should return objects with the component data, which may be wrapped in promises.
Note: It is much better to do component logic when saving rather than every time it wants to render.
module.exports.render = function (ref, data, locals) {
// do logic
return data; // will be sent to the template
}
module.exports.save = function (ref, data, locals) {
// do logic
return data; // will be sent to the database
}
You may pass componenthooks=false
as a query param when doing API calls if you want to completely bypass the model.js
(e.g. if you've already run through the logic client-side).
This feature is deprecated as of amphora v2.11.0 and will be removed in the next major version (it is supplanted by the isomorphic model.js
files). Legacy server-side logic lives in a server.js
or index.js
file in the component's folder, and has a few differences from the model.js
:
- the default exported function (run on
GET
) does not automatically receive data from the database, and must fetch it manually - the
put()
should return a database operation or array of operations (which may be wrapped in promises), rather than the component data itself - these files are only run server-side, and will not be optimised to work in kiln
module.exports = function (ref, locals) {
//return Promise with data
return Promise.resolve({"hey": "hey"});
};
module.exports.put = function (ref, data) {
//return Promise with operations to be performed in a batch
return Promise.resolve([{
type: 'put',
key: '/components/text',
value:'{"hey": "hey"}'}
]);
};
/pages
/pages/:name
/pages/:name.html
/pages/:name.json
/pages/:name@:version
/pages/:name@:version.html
/pages/:name@:version.json
Pages consist of a layout and a list of areas. Each area defined in a page maps to a area in the layout template. For example:
{
"layout": "/components/feature-layout",
"center": ["/components/article/instances/3vf3"],
"side": [
"/components/share",
"/components/newsletter"
]
}
The layout component in this example has two areas: center and side. When the page is displayed, the components listed here will be placed into the matching areas in the layout.
GETs and PUTs to pages work similarly to components. API calls without an extension will update/return data for that page, while the .json
and .html
extensions will return the composed (page and layout, and their child components) JSON and HTML, respectively.
Publishing a page has a special convenience behavior where all components referenced by the page will also be published. (Example: #78)
/uris/<base64(:path)>
A URI is used to redirect some slug or URI to another page or component. They can also redirect to other uris (establishing a 301 redirect), or several uris can point to the same resource. URIs are stored as Base64, so:
example.com
is/uris/ZXhhbXBsZS5jb20=
=>/pages/jdskla@published
example.com/other/
is/uris/ZXhhbXBsZS5jb20vb3RoZXI=
=>/pages/4revd3s@published
A URI is assumed to be pointing at the @published
version if another version is not provided. Therefore, only published content or specially tagged versions can be publicly exposed through URIs.
/lists
/lists/:id
Lists are a temporary solution until search functionality is discussed. It is currently used for the tags component and the autocomplete behavior as a temporary place to store lists of information. It can store any list of data on a PUT
, and return back the same list of data with a GET
.
/users
/users/:id
Each user has a username
and provider
, which determines how they authenticate over oauth. Users can also have other data, including name
, imageUrl
, and title
.
/schedule
/schedule/:id
Schedule is a list of pages or components that will be published in the future. Items in this list will be published when the time passes. Each item has two properties: at
, which is a UNIX timestamp, and the publish
property that is a reference to what will be published after that time.
When an item is scheduled, a @scheduled
version is created on the item that will be published. When the item is eventually published, the @scheduled
version will be removed. The @scheduled
version is a reference to the actual schedule uri that can be deleted to cancel the scheduled publication.
GET /schedule
will return a list of scheduled items, such as:
[
{
"_ref": "domain.com/some-path/schedule/3f-abc",
"at": 44392893402093,
"publish": "abc"
}
]
POST /schedule
will add an item to be published in the future. The format of the item should be of
{
"at": 44392893402093,
"publish": "<uri to be published>"
}
where the at
is a UNIX timestamp and the publish
is a ref to a page or component. On success, it will return a 201 with a reference to the schedule uri that was created.
DELETE /schedule/:some-id
will remove the scheduling of a publish in the future. On success, it will return a 200 with
{
"at": 44392893402093,
"publish": "<uri that was to be published>"
}
Some versions have a special behavior called propagating, which any reference within their data is changed to be the same version as itself.
The current propagating versions are:
- published
- latest
We try to follow the REST standard when making decisions about changes to the API. In particular, we are adhering to
-
Individual resources are identified by URI. Therefore, there is no need to return an id as part of the data of a request if that id was used to fetch the data. In many environments (production, development, staging) hostnames can be dynamic, so we can optionally omit the hostname and assume that the resource is available at the same host as a referring resource.
-
When a client holds a representation of a resource, it has enough information to modify the resource. Any random ordering of
GET
andPUT
requests to the same uri with the returned data will not result in any change of state (beyond the separate audit trail that such operations took place). -
Messages always include enough information to process a resource, including MIME type and cacheability. All resources are returned with appropriate Content-Type, Cache, and Vary headers. That means resources that should be cached, like published content, are marked appropriately as cacheable.
-
HATEOAS: no assumptions about additional actions are made without being described in the resource itself. Excluding the basic HTTP method calls, we tell the client about additional resources available related to the current resource.
Currently, we link to child resources with
{ _ref: 'domain.com/full/resource/uri' }
. There is no assumption that the client knows about these child resources implicitly. More importantly, we don't expect the client to know about where resources are, their types, or that they should be linked together in any particular way, so we always reference to linked resources with a full uri.For more information about the design decisions of
_ref
, see #108
It might be a good idea to namespace this API behind versions, such as /v1/
or /v2/
, or /services/
, or even /clay/
. This can be done by adjusting the hostname, or through a reverse proxy like Varnish. Putting different versions behind Varnish might be particularly useful so that previous versions of an application can be run at the same time as newer versions to service old and new clients until previous versions can be deprecated.
The API must follow a specific ordering for the priority of errors to return. Even though multiple things might be wrong with a request, consistent testability requires that the type of error returned for any particular request should not change without deliberate thought. The current order of errors are:
- Component invalid: There is no resource (component) by that name returns 404 for the entire route (including all sub-routes).
- Allow header: That method is not allowed
- Accept header: The request's acceptable data type is not supported by this route.
- Resource missing: A resource with this specific identifier does not exist.
We are not going to support returning 304s for several reasons:
- Most people will have a reverse cache like Varnish in front of byline endpoints in production.
- The ROI for supporting 304s is very low for non-browser non-html clients.