Fork me on GitHub
Motivation
We believe that hybrid solutions are more robust than ultimate ones.
Trying to have a balance between simplicity and power we found out this convention. The major idea is to provide API making patterns with respect to the Server side and comfortable for the Client side.
Note
|
Make sure you checked the Best Practices before usage. There are many answers to questions that you may have. |
Documentation convention
Italic - local definitions or from glossary
BOLD - technologies or definitions with external documentation
/entity/{id}
- id
is variable part of URI path
JSON is used as serialization format for examples bellow
{
"field": "value"
}
Glossary
- Identity
-
unique identifier
- Structure
-
object without identity
- Entity
-
object with identity field (e.g.
id
)
- Model
-
contract which describes which fields are in Structure or Entity
- View
-
read-only Model which is used to represent Entity from other point of view
- Filters
-
a set of conditions to reduce response.
Filters are passed via GET request parameters:<field>-<filter-keyword>=<value>
.
filter-keywords have next mapping to comparison operations: eq (=
), gt (>
), lg (<
), gte(>=
), lte (⇐
).
value must have the same tape with filtered fieldExample 2. Filtering comments by datesGET request
example.org/api/comments?createdAt-gte=2000-01-01&createdAt-lt=2000-01-10
There can be two additional steps: order and limit. It’s assumed that limit is evaluated always as the last step.
Order is GET request parameters:
order=<field1-asc>[,<field2-desc>]
;
Limit is GET request parameter:limit=<count-of-results>
Example 3. Filtering with order and limitGET request
example.org/api/comments?createdAt-gte=2000-01-01&limit=1&order=createdAt-desc,id-asc
- Filtrator
-
a special Model to filter and navigate across arrays. It’s returned as a response when more than one entity was queried.
Example 4. Response with Filtrator in a body{ "items": [{…},{…},…], (1) "hasMore": true, "filter" : { (2) "limit" : 20 } }
-
found entities according to filter
-
filter used in request (including implicit)
-
- Counter
-
any field which represent an amount of items.
Also can be used as a View. See Counter example for more details.
- Operation
-
a system level Entity to track long term processes or complex object transformations. It’s returned as a response for operation requests. See Operations oriented part
Example 5. Operation model{ "id": "uuid-string", "status": "status-string", (1) "parameters": { (2) "inputValue1": 42 }, "result": { (3) "updatedValue1": "42" } }
-
one of set of statuses agreed in your team
-
input operation parameters
-
the result of operation. May contain intermediate state.
-
Data & Transfer conventions
-
URI path must use 'kebab-case'
-
URI path entries are used in plural form, e.g. /comments/{id}
-
Field naming convention must be either 'snake_case' or 'camelCase' (but the same across the whole application)
-
Entity cannot contain another Entity, i.e. nesting isn’t allowed
-
Arrays inside Entity can contain only primitives (numbers, chars, strings) or structures
Object related part
This part describes CRUD calls on Entities. All operations usw the same Entity model (except Views)
Entity addressing
Common rule to address entity is <entity name in plural>/<optional entity identity>
books/42
comments/5da69dfa-055f-11e9-8eb2-f2801f1b9fd1
Entity address cannot contain more than one identity, in other words nested addresses are prohibited
right: /comments/{comment-id}
wrong: /posts/{post-id}/comments/{comment-id}
Handling Entities
C4S is using subset of HTTP verbs to manipulate an Entity
Create
POST request with body on entity path
Request POST example.org/api/comments
{
"threadId": 42,
"content": "42"
}
Response:
{
"id": 42,
"threadId": 42,
"content": "42"
}
Read by Identity
GET request on entity path with Identity
Request GET example.org/api/comments/42
Response:
{
"id": 42,
"threadId": 42,
"content": "42"
}
Update
PUT request with body on entity path. Given entity will override previous one. Not passed field means unset (or set the null value)
Suppose we have profile Entity like this
{
"userId": 42,
"birthDay": "1970-01-01",
"firstName": "Joni",
"middleName": "Jerry",
"lastName": "Doe"
}
Request PUT example.org/api/comments
{
"userId": 42,
"birthDay": "1970-01-01",
"firstName": "John",
"lastName": "Doe"
}
Response:
{
"userId": 42,
"birthDay": "1970-01-01",
"firstName": "John",
"lastName": "Doe"
}
The first name was changed and middle name was unset (removed)
Delete
DELETE request on entity path with Identity
Request DELETE example.org/api/comments/42
Response:
204 status-code [No Content] in case of success
Read list of Entities
GET request on entity path returns Filtrator
Request GET example.org/api/comments
Response:
{
"items": [
{
"id": 42,
"threadId": 42,
"content": "new message"
}
],
"hasMore": false,
"filter" : {
"limit" : 20
}
}
Setup relations between Entities
The documentation bellow operates such definitions like first (was created 'before'), second (was created 'after'), one and many which describes corresponding parts of relation types.
One-to-One
POST request to the second entity with Identity of the first in a body.
POST request example.org/api/profiles
{
"userId": 42,
"birthDay": "1970-01-01"
}
Response:
{
"id": 43,
"userId": 42,
"birthDay": "1970-01-01"
}
Note
|
If bidirectional link is required (to filter the first by identity of the second) it is allowed to set identity of the second into the first entity implicitly during the operation. |
One-to-Many
POST request to the many entity with Identity of the one in a body.
POST request example.org/api/likes
{
"commentId": 42,
"type": "positive"
}
Response:
{
"id": 43,
"commentId": 42,
"type": "positive"
}
Note
|
If bidirectional links are required it is allowed to append identity of many to the array in one entity implicitly during the operation. |
You may notice that process of connecting One-to-One and One-to-Many are quite similar.
Many-to-Many
This type of relation is difficult to manage and filter. Try to avoid this case in resource model by hiding behind "One-to-Many" if really need to.
Views for Entities
Views are useful for extending (with additional info) or reducing (to produce lightweight representation) Entities.
View of an Entity may me requested with dot extension
in a path, e.g. <entity-path>.<view>
/comments/42.lite
/comments.with-likes-count
View can be created for particular cases or be generic like <entity>.count
which adds total amount to Filtrator response.
Filters are also applicable to views like to entities.
GET request example.org/api/comments.count?createdAt-lte=1970-01-01
Response:
{
"items": [],
"hasMore": false,
"filter" : {
"createdAt-lte": "1970-01-01",
"limit": 20
},
"count": 0
}
Operations oriented part
This path describes operations (or remote procedure calls) on the entities.
Operations are designed for long term calls or transformations which cannot be done on entity like an object. All calls (except create) use the Operation model. The creation uses form with input parameters which are individual for every operation.
Operation addressing
Because entity path may have long prefix (due to routeing rules), operation URI part in a path it’s separated from object part by token /-/
(means: not an Identity)
/prefix/entity/-/operation
/api/microservice/v42/entity/-/make-backup
Operation URI path can contain only operation identity, and all parameters must be passed in a body.
Scheme of operation URI:
/<any-server-prefix>/<entity>/-/<operation>/<operation-id>
Handling Operations
Create
POST request with body on operation path to create/start. This call returns object of operation instead of sent parameters
Request POST example.org/api/comments/-/archive
{
"threadId": 42
}
Response:
{
"id": "5725fb91-755e-44ca-877b-d633a128a492",
"status": "PENDING",
"parameters": {
"threadId": 42
},
"result": { }
}
Read by Identity
GET request on operation path with Identity
Request GET example.org/api/comments/-/archive/5725fb91-755e-44ca-877b-d633a128a492
Response:
{
"id": "5725fb91-755e-44ca-877b-d633a128a492",
"status": "RUNNING",
"parameters": {
"threadId": 42
},
"result": { }
}
Abort operation
DELETE request on operation path with Identity
This request should return operation in current state. There are no guarantees about immediate aborting (or rollback) because it depends on the server implementation. This is a way just say to server that the result of its operations already doesn’t matter for client.
Request DELETE example.org/api/comments/-/archive/5725fb91-755e-44ca-877b-d633a128a492
Response:
{
"id": "5725fb91-755e-44ca-877b-d633a128a492",
"status": "ABORTION"
"parameters": {
"threadId": 42
},
"result": {
"archivedCommentIds" : [42]
}
}
Note
|
You should consider having a deprecation policy for complete operations and thair results. |
Read list of operations
GET request on operation path. Request may contain filters.
Request GET example.org/api/comments/-/archive?status-eq=ABORTED
Response:
{
"items": [
{
"id": "5725fb91-755e-44ca-877b-d633a128a492",
"status": "ABORTED",
"parameters": {
"threadId": 42
},
"result": {
"archivedCommentIds" : [42]
}
}
],
"hasMore": false,
"filter" : {
"status-eq" : "ABORTED",
"limit" : 20
}
}
Sync Views for Operations
Having the same Views for operations like for entities makes no sense. However clients may want to have a synchronous (blocking) request and got the result of operation w/o intermediate states.
For this reason we have a special View for operations: .sync
.
It says to server that client wants to get the result of operation immediately.
Of course, it’s not always possible to do it (for example, if operation is long-term or asynchronous by nature),
is such cases server must return error (see Errors processing).
Request POST example.org/api/comments/-/archive.sync
{
"threadId": 42
}
Response:
{
"archivedCommentIds" : [43, 44, 45]
}
Errors processing
To make the convention complete we must define Structure to describe Errors from the server. There can be problems related to the client input (4xx error-codes) or server (5xx error-codes) but all of them must conform the next model:
{
"status": <integer code>,
"error": "string error code",
"requestId": "string uuid"
}
This model is enough for server error codes and can be extended by yourself
{
"status": 503,
"error": "REMOTE_SERVER_UNAVAILABLE",
"requestId": "054a71e0-0cfd-11e9-ab14-d663bd873d93"
}
{
"status": 400,
"error": "BAD_REQUEST",
"requestId": "28f828da-0cfd-11e9-ab14-d663bd873d93",
"description": "Several constraint over the entity was violated",
"fields": {
"comment": {
"error": "CONTENT_IS_TOO_LONG",
"description": "Comment message is too long",
},
"title": {
"error": "CONTENT_IS_TOO_SHORT",
"description": "title must have at least 5 symbols",
"parameters": {
"minLength" : 5
}
}
}
}
Or keep it as simple as possible
{
"status": 409,
"error": "OPERATION_IS_ALREADY_ABORTED",
"requestId": "63de8a9c-0cfe-11e9-ab14-d663bd873d93"
}
Best Practices
Nested structures to avoid duplication
{ //...
"homeAddress": {
"street": "Aviation",
"building": "1"
}
}
instead of:
{ //...
"homeAddressStreet": "Aviation",
"homeAddressBuilding": "1"
}
Implicit filters to reduce unnecessary server loads
You can add default limit for Filtrator queries:
Request GET example.org/api/comments
Response:
{
"items": [
{
"id": 42,
"threadId": 42,
"content": "42"
}
],
"hasMore": false,
"filter" : {
"limit" : 20 // implicit filter should be always returned
// with response to avoid confusion
}
}
Better than Pagination
The concept of Filtrators is created to overcome drawbacks of classic Pagination. When you are using pagination you provide the limits (a page number and number of items on the page) and orders as filters. Such functionality is easy but can lead to duplication due to prepended entities, so it can lead to expensive count queries to count total amount of pages.
On hot database tables the consistent count queries are not easy tasks but the result often is not so important for users. Instead of this we suggest to use infinity scrolling or powerful filtration system (all counts can be counted on demand if they really needed)
Pagination request GET …/comments?page=0&limit=20
Response:
{
"content": [{...},{...},...],
"page": 0,
"limit": 20,
"totalPages": 42
}
Filtration request GET …/comments?id-gte=0&limit=20
Response:
{
"items": [{…},{…},…],
"hasMore": true,
"filter" : {
"id-gte": "0",
"limit" : 20
}
}
So as you can see for the first page changes are pretty simple.
Suppose we have serial Identities for our entities. To navigate to the following results use next queries
Pagination request GET …/comments?page=1&limit=20
Response:
{
"content": [{...},{...},...],
"page": 1,
"limit": 20,
"totalPages": 42
}
Filtration request GET …/comments?id-gt=42&limit=20
where 42 is the Identity of the last item of previous response
Response:
{
"items": [{…},{…},…],
"hasMore": true,
"filter" : {
"id-gt": "42",
"limit" : 20
}
}
But the real power appears when you want navigation by date, priority, name or whatever you Entity can hold. With Filtrators you are safe from pre- or appending new items regardless of order.
Short-living operation for non-object transformations
If you want to make non-object related change, let’s say update amount of votes for the comment, you should use Operation. Such operations can use Sync View and provide final result as response.
Suppose we have comment with 5 votes:
{
"id": 42,
"threadId": 42,
"content": "popular comment",
"votes": 5
}
Because comments can be voted simultaneously by many users we cannot set new value. We use operation increment on this Entity which check constraints (like one like from one user) and add 1 vote. Developers should decide what the result payload of operation will be returned. In this case we decide return the current amount of votes.
Request POST example.org/api/comments/-/upvote.sync
Response:
{
"id": "5725fb91-755e-44ca-877b-d633a128a492",
"status": "DONE",
"parameters": {},
"result": {
"votes": 7
}
}
The values 7 in this case means someone has upvoted comment between initial page load and voting for comment by us.
With time a database will accumulate many such Short-living operations so there can be deprecation policy to remove them.