During the last few years while working at Kaltura I have written, with my team members, many REST client libraries in many languages (including Python, Java, PHP, Javascript, ActionScript, C#, Erlang, ObjectiveC, and Ruby) against different servers. We also implemented REST servers in different languages (PHP, C#, NodeJS).
The REST standard is wide, and yet the common implementation of REST is very narrow and repeatedly follows the same mistakes already made by others.
I want to share Kalturians’ criticism of the common implementation and to offer a few solutions and tips that we developed through the years at Kaltura.
REST implementers commonly try to reflect nested objects using hierarchic paths.
For example, if you want to get the devices of a user that belongs to a household that belongs to an account, you might need to use a URL that looks something like this:
/api/account/[account-id]/household/[household-id]/user/[user-id]/device/[device-id]
I believe that if each device has a unique id, it’s better to use a simple form such as:
/api/device/[device-id]
If your system supports more than one type of device (for example, users’ devices and household devices), I still recommend one simple form such as:
/api/userDevice/[device-id]
Flattening your data model to a list of services, when each service represents a single data object type, will force you to choose simple data model for your server side as well.
Of course, flat models will be much easier for your integrators and customers to use.
Most implementations of REST are counting on different HTTP methods for different CRUD operations on the same object.
Examples include GET for list, PUT for update, POST for create, and DELETE for delete. I also saw implementations that use OPTIONS, HEAD, PATCH, INSERT, UPDATE and other non-standard methods.
I see few problems with this approach.
The solution I prefer is to add the operation to the URL and the HTTP request. This way, you can support both GET and POST with plain form data, in XML or JSON.
Here are a few examples (the examples are self-explanatory):
Using path (GET):
Using query string (GET):
Using POST (form data combined with path):
Using POST (JSON combined with query string):
{id: [device-id]}
{id: [device-id]}
{}
{ filter: { householdIdEqual: [household-id], statusEqual: [status] } }
{ device: { name: [device-name], description: [device-description], tags: [device-tags] } }
{ id: [device-id], device: { name: [device-name], tags: [device-tags] } }
Using POST (XML):
<[device-id]
[device-id]
[household-id] [status]
[device-name] [device-description] [device-tags]
[device-id] [device-name] [device-tags]
Looking at the examples, you can see how self-explained the actions are and how it’s simple to add a new action (deleteByEmail for example).
Assuming that you want to create a user and a device that associated with that user, you could perform both actions in a single HTTP request.
Note the token I entered into the example requests. {results:1:id} means use the ID attribute from result number one.
This empowers the multi-request by enabling the client to use the output of one action as the input of another action in the same HTTP request.
Here are a few examples:
Using POST (form data combined with path):
1[service]=user&1[action]=add&1[user][firstName]=[user-first-name]&1[user][lastName]=[user-last-name]&1[user][email]=[user-email]&2[service]=device&2[action]=add&2[device][userId]={results:1:id}&2[device][name]=[device-name]&2[device][description]=[device-description]&2[device][tags]=[device-tags]
Using POST (JSON combined with query string):
[{ service: "user", action: "add", user: { firstName: [user-first-name], lastName: [user-last-name], email: [user-email] } },{ service: "device", action: "add", device: { userId: "{results:1:id}", name: [device-name], description: [device-description], tags: [device-tags] } }]
Using POST (XML):
[user-first-name]; [user-last-name] [user-email]; {results:1:id} [device-name] [device-description] [device-tags]
Usually, the HTTP protocol is used to return HTTP error code that reflects the status of the request.
For example, 400 for bad request, 422 for unprocessable entity, 404 for entity not found, 403 for forbidden service or action, 401 for unauthorized object or action.
Also, the success error codes could be different. For example, 200 for successful list, 201 for successful creation, 202 for successful update, 204 for no content, 205 for successful deletion.
These are the issues I see with this approach:
The error codes that I want to reflect in the HTTP protocol are always HTTP errors, such as 200 OK, 404 Not Found, 500 Internal Server Error.
All other applicative errors will be returned with HTTP status 200 and the response content will reflect the error.
Few JSON request and response:
{ user: { firstName: [user-first-name], lastName: [user-last-name], email: [user-email] } }
Good response:
{ user: { id: [user-id], createdAt: [user-create-time], updatedAt: [user-update-time], firstName: [user-first-name], lastName: [user-last-name], email: [user-email] }, warnings: [ { code: 123456, message: "User first name [user-first-name] is too long", attributes: { firstName: [user-first-name] } }], executionTime: 0.6555 }
Bad response:
{ error: { code: 123457, message: "User e-mail [user-email] is not valid", attributes: { email: [user-email] } }, executionTime: 0.6555 }
[{ service: "user", action: "add", user: { firstName: [user-first-name], lastName: [user-last-name], email: [user-email] } },{ service: "device", action: "add", device: { userId: "{results:1:id}", name: [device-name], description: [device-description], tags: [device-tags] } }]
Response:
{ [{ user: { id: [user-id], createdAt: [user-create-time], updatedAt: [user-update-time], firstName: [user-first-name], lastName: [user-last-name], email: [user-email] }, warnings: [{ code: 123456, message: "User first name [user-first-name] is too long", attributes: { firstName: [user-first-name] } } },{ error: { code: 123458, message: "Device name [device-name] may not contain special charecters [&]", attributes: { name: [device-name] charecters: "&" } }, }], executionTime: 0.6555 }
As you can see, using error code and attributes you can translate the error message to any language in any structure.
Also, returning the errors in the response body you can support complex responses with detailed explanation for each failure.
Most REST API implementations do not support file uploads.
If you followed my recommendations above to always support both GET and POST, then when it comes to actions that include uploaded file, you’re required to use POST only.
In addition, the content-type of your request can’t be application/json, application/xml, text/xml, or even application/x-www-form-urlencoded. It must be always multipart/form-data.
If you still want to support JSON or XML content, you can always submit them as a form field, for example:
Content-Type: multipart/form-data; boundary=---------------------------90519140415448433659727646345634 -----------------------------90519140415448433659727646345634 Content-Disposition: form-data; name="json" user: { firstName: [user-first-name], lastName: [user-last-name], email: [user-email] } -----------------------------90519140415448433659727646345634 Content-Disposition: form-data; name="homepageContent"; filename="myHomepage.html" Content-Type: text/html -----------------------------90519140415448433659727646345634--
Most systems are required to also serve images, video files, textual content, and other binary content.
Many binary content entities are used in browsers such as images or used as URL to integrate with UI and external systems.
Therefore I believe it’s important to support also plain GET with path attributes or query string.
A few examples:
Note that I encountered few STB devices that accept feeds for video content that do not support query strings. That’s why supporting variables in path was important to me.
If your content should be protected, you can always add authorization token to the URL or tokenize the URL using a CDN token that expires after a configurable time.
At Kaltura, we easily generate different client libraries using clients generator we wrote. It’s open source and generates client libraries in many coding languages: Python, Java, PHP, Javascript, ActionScript, C#, Erlang, ObjectiveC, and Ruby.