Introduction
This is Visfleet's Services Policy Manual. Developers at Visfleet follow this guide in an attempt to keep our web services layer consistent and useful to both internal development, and customers consuming our APIs.
Heuristics
This policy manual is a guide to best practices, try to fit within the policy for web services described herein but be pragmatic with new challenges.
Restful Rails
Although the Rail's implementation of Restful continues to change, we still say that we use the conventional Restful Rails for our web services. Any deviation that's required is documented below. We break the deviations into two parts:
- Additional Protocol - Ways of handling data better than the de-facto, without breaking the de-facto.
- Exceptional Protocol - Ways of handling data that causes the de-facto (rails rest) implementation to be broken.
The additional protocol should be our Standard. That is, we should implement this protocol wherever we can. It's very useful stuff.
The exceptional protocol deviations are troublesome but occasionally necessary. The reasoning for them is outlined, and we try to compartmentalise them in our services by relying on special Mime-Types or extensions such as the .flex extension.
Additional Protocol
Representation of entities
Identity
All entities must carry an identity consisting of one or more field. The identity must be documented. In most cases, the field 'id' is more than adequate.
For read only lists, any unique identity will suffice. For writable entities, the identity must be the same as the one used for update, delete, and per-entity verbs, e.g.:
For the path: /myApp/widgets/:id
:id must match the identity provided in the read operations. Again, this does not have to be a single field called id, but in almost all cases it will be.
Standard Fare
In all cases the following fields (if not null) should be represented:
- created_at
- updated_at
- deleted_at (used to identify the record as deleted)
Note on deleted_at: Because we use soft deletion, it's possible to request deleted resources. 'deleted_at' is our way of knowing the resource is deleted.
Soft Deletion
Soft deletion involves keeping the record to be deleted in the database, but saving it with a delete_at matching the datetime it was deleted. Soft deletion should always modify the updated_at field as well.
Always Soft Delete
The only exception, is read only lists can use hard deletes, so long as it doesn't cause integrity issues.
Never damage data integrity
Keep this in mind for Add's as well.
To avoid such a scenario, try:
- Implement the deletion as business logic that performs other necessary modifications to model to keep integrity - This is the DEFAULT behaviour of our framework
- Throw an error which sends the consumer to read the docs or with detail in the error. The detail or docs should advise the consumer what else they have to do to get a successful delete (such as delete a dependency first). This is the fallback position for developer when constructing appropriate business logic is untenable.
Deltas
We implement list deltas, which are changes to collections and items in the collection. It could be extended to individual elements (get /resourceCollection/:id) but as we don't use the 'show' action very often, it has not been implemented.
The aim of delta's is to support an effective 'synchronisation server' for clients that keep a local cache of records.
Delta's hinge on the request parameters:
- changed_since (default null) - a datetime which asks the server to return all records changed on or after the specified datetime. A value of null return all records (excepting other filters)
- include_deleted (default false) - a boolean which asks the server (when true) to return soft-deleted records.
And upon the response values:
- last_modified (and Last-Modified) - a datetime which represents when the request began being handled by the server, to the second. It can also be the maximum datetime of all records updated_at but this is both less efficient, and runs the risk of a single invalid record (with an updated_at in the future) causing complete failure.
- delta_mode (and X-DELTA-MODE) (if null assume 'delta') - a string with the value of either 'delta' or 'new'. "delta" means this is a delta and as such we can assume we only received changed records. "new" means this is a complete list and you must abandon your current list in favour of it.
Notes about response values:
Depending on the format of the response, these will need to be added in different places.
Typically they should both be included as response headers. As a response header use the spelling: "Last-Modified" as this will help with caching as well as deltas.
In the case of flex responses, we cant read the response headers, so we expect "last_modified" and "delta_mode" in the root node of the XML.
Delta Example
First List Request (get /resourceCollection)
First request to a list (get /resourceCollection) must send the parameter changed_since as null, or not send it at all. Typically we also leave out include_deleted. Sometimes it will be requested (include_deleted = true) to get administrative lists. For example, if you want to enable undeletes for a user.
The response is typical rest, with either headers for Last-Modified and X-DELTA-MODE or root node attributes last_modifed and delta_mode. If these don't come back, then the client will usually assume a delta, and because all records are included, will merge it with the local collection.
Further List Requests
Now when a request to the collection is made, the parameter changed_since is sent with the most recent Last-Modified (or last_modified) response from the server. It's possible to get some records twice in this manner because they could have been added/modified in the same second as the last response. This is preferable to the alternative where delta's may be missed.
This request will also set include_deleted to true, indicating that it needs to know about records deleted since the last modification.
Nested Updates
In nested representations, modifying the content of nested entities is achieved in place, by sending the entire representation (including the outer entity) back to the server. E.g.:
If for get /items/4 you recieve:
Item:
id : 4
name: floogle
prices:
price:
id: 8
quantity: 1+
amount: 5.50
price:
id: 11
quantity: 10+
amount: 5.00
price:
id: 12
quantity: 100+
amount: 2.90
An update to the 1+ quantity would require a post /items/4 with:
Item:
id : 4
name: floogle
prices:
price:
id: 8
quantity: 1+
amount: 5.90
price:
id: 11
quantity: 10+
amount: 5.00
price:
id: 12
quantity: 100+
amount: 2.90
The order of nested elements may have meaning, depending on the resource. For example, the order of prices may affect the display order, so you might send them back in reverse order to have the list display with larger quantity prices first.
Nested Additions
Similar to updates, additions are simply a modification of the nested list where you include an extra nested entity without an ID. You can add as many items as you like at once this way.
post /items/4
Item:
id : 4
name: floogle
prices:
price:
id: 8
quantity: 1+
amount: 5.90
price:
id: 11
quantity: 10+
amount: 5.00
price:
quantity: 50+
amount: 4.10
price:
id: 12
quantity: 100+
amount: 2.90
Remember, the order of entities can have meaning.
Nested Deletes
Deletion is also an update, however instead of returning the representation of a nested entity, you only return it's ID and the special field '_deleted' with a truth value.
post /items/4
Item:
id : 4
name: floogle
prices:
price:
id: 8
quantity: 1+
amount: 5.90
price:
id: 11
quantity: 10+
amount: 5.00
price:
id: 45
_deleted: true
price:
id: 12
quantity: 100+
amount: 2.90
Again, you can mix deletions with other updates including additions and re-ordering.
Paging
If paging is handled by the server, then you can specify the page by appending the following parameters to the query string.
- page=[integer]. The page to return
- per_page=[integer]. The number of entries per page
The following attributes are included in the root element of the returned resource.
<events current_page="2" per_page="100" total_pages="10" total_entries="998">
...
</events>
Exceptional Protocol
Handling Errors
For any browser plugin client technology (e.g. Flex/flash) access to http status codes is out of reach, and handling errors presented in another format (such as html pages) is patently silly. When the server issues a status code error, or a handled exception, the results for flex marked up xml look like:
<errors status_code="301" >
<error>Content moved permanently</error>
</errors>
or
<errors>
<error>Resource must have a name</error>
<error>Resource could not be saved</error>
</errors>
Note: status_code is a suggestion, we haven't settled on this yet.
Handling Status Codes
Non-error status codes need to be converted to a 200 response or flex will ignore the response (think it's an error)
Rule of thumb for .flex support is to always return a 200 code in the headers.
Underscores
All .flex serialisation should use underscores, not dashes or camelCase:
this_is_good
whereas
this-is-not
thisIsNotEither
* this does not apply to headers, only body content.
Addendum
Flex Limitations
Status Codes
We cannot support http status codes because flex wont see them. It will see a '0' or some of the '200' codes on ie. Best not to rely on them for anything
Request Headers
Flex supports any request header, so long as:
1. There is a body attached to the request
2. The header is not one of the following:
Accept-Charset,
Accept-Encoding,
Accept-Ranges,
Age,
Allow,
Allowed,
Authorization,
Charge-To,
Connect,
Connection,
Content-Length,
Content-Location,
Content-Range,
Cookie,
Date,
Delete,
ETag,
Expect,
Get,
Head,
Host,
Keep-Alive,
Last-Modified,
Location,
Max-Forwards,
Options,
Post,
Proxy-Authenticate,
Proxy-Authorization,
Proxy-Connection,
Public,
Put,
Range,
Referer,
Request-Range,
Retry-After,
Server,
TE,
Trace,
Trailer,
Transfer-Encoding,
Upgrade,
URI,
User-Agent,
Vary,
Via,
Warning,
WWW-Authenticate,
x-flash-version.
Response headers
None are supported afaik. Try to embed them in the envelope of the body (e.g. root node of XML)
Request Method Header
Flex only supports Get and Post. Use the Rails request header: X_HTTP_METHOD_OVERRIDE