Structure
The API is designed around the REST ideology, providing simple and predictable URL's to access and modify objects. Requests support standard HTTP methods like GET, PUT, POST and DELETE and standard HTTP status codes. Response bodies are always UTF-8 encoded JSON(-LD) objects unless explicitly documented otherwise.
Martin Fowler provided an excellent explanations of Richards model of REST maturity with the following steps:
- Use standard methods (GET, POST, etc)
- Use standard response codes (200, 302, 404, etc)
- Use standard header parameters (application/json)
- Use standard restful application markup language (RAML)
- Interlink as many of the resources as you can.
- Use HATEAOS to avoid clients having to know how to parse URI patterns.
Whenever you think RESTful web service you should think HTTP because it has all the features that support you to build great web services.
- GET for retrieving
- POST for creating
- PUT for updating
- DELETE for deleting
- PATCH for partial updates
Always use plurals when naming resources. Suppose we have a service that hosts a users resource. The following should be used to manage the resource.
- Create a user: POST /users
- Delete a user: DELETE /users/1
- Get all users: GET /users
- Get one user: GET/users/1
Response codes are very important to help clients handle exceptions properly. Some of the most common are the following:
- 200 - Success
- 302 - Temporarly moved (ex: new resource created)
- 304 - Not modified (ex: already cached)
- 400 - Bad request (ex: validation rule)
- 401 - Unauthorized (ex: when a user needs to sign in)
- 403 - Permission denied (ex: when a user tries to get a url they cant access)
- 404 - Page not found (ex: when a resource is not there)
- 500 - Server error (ex: generic exception)
Implementations
Filtering and Pagination
Modern frameworks offer a way to paginate results, but you can also customize your own. A common approach is to use LIMIT and OFFSET statements on your queries.
select * from people limit 5,10
That statement will retrieve the rows 6-16 from the database so you can provide a json response that give links to the first, next, previous and last page of that query based on the limit options.
{ "first": "/api/v1/people?page=1", "prev": "/api/v1/people?page=1", "next": "/api/v1/people?page=3", "last": "/api/v1/people?page=9", }
There are various ways to encode the propertname, the operator (such as eq, lte, gte) and the filter value for information you want to reduce. It is important to list all possible options for filtering in your API documentation and enforce strong validation on the input like checking if its a valid number, date, etc.
Use REST and HATEAOS
REST is a proven and battle-tested architectural approach to providing API's, however its good to provide context around the set of resources without having prior knowledge of the internal URI scheme with the help of HATEOAS. It enhances the response model of each resource by providing a set of relevant links so that it is easier to interact with the API. Without looking up a specification or other metadata service. One good format of HATEAOS is the HAL specification.
{ "_links": { "self": { "href": "/api/v1/people/1" }, "/rels/people": [{ "href": "/api/v1/people/84", "name": "Scott" },{ "href": "/api/v1/people/94", "name": "Mike" }] } }
Caching
It is easy to ignore the caching by including the header "Cache-control: no-cache" in responses of your API calls. HTTP defines a powerful caching mechanism that includes ETag header, If-Modified-Since header, and 304 Not Modified response code. They allow your clients and servers to negotiate always a fresh copy of the resource and through caching or proxy servers increas your applications scalability and performance.
The following ar ethe high level steps where the response header "ETag" along with conditional request header "If-None-Match" is used to cache the resource copy in the client brower:
- The server receives a normal HTTPD request for a particular resource, id=123 to get the details.
- The server prepares the response, but in order to help the brower with caching, it includes the header "ETag" with the value of the response: "ETag: 'Version1'".
- The server sends the response with the above header, the content of the project 123 in the boy with the status code of 200. The browser renders the resource and at the same time caches the resource copy along with the header information.
- Later the same browser makes another request for the same resource project 123 but with following conditional request headre "If-None-Match: 'Version1'".
- On receiving the request for project 123 along with "If-None-Match" header, the server logic checks if project 123 needs a new copy by comparing the current ETag identifier generated on the content of project 123 and the one that is received in the request header.
- If the request's If-None-Match is the same as the currently generated/assigned value of ETag on the server, then status code 304 (Not Modified) with the empty body is sent back and the browser that uses a cached copy of project 123.
- If the requests If-None-Match value doesnt match the currently generated/ assigned value of ETag (ex: version2) for project 123, then the server sends back the new content in the body along with the status code 200. The ETag header with the new value is also included in the response. The browser uses the new project 123 an dupdates its cache with th enew data.
Level of Granularity
Granularity is an essential principle of REST API design. As we understand, business functions divided into many small actions are fine-grained, and business functions divided into large operations are coarse-grained.
In some cases, calls across the network may be expensive, so to minimize them, coarse-grained APIs may be the best fit, as each request from the client forces lot of work at the server side, and in fine-grained, many calls are required to do the same amount of work at the client side.
Example: Consider a service returns customer orders in a single call. In case of fine-grained, it returns only the customer IDs, and for each customer id, the client needs to make an additional request to get details, so n+1 calls need to be made by the clients. It makes expensive round trips regarding its performance and response times over the network.
In a few other cases, APIs should be designed at the lowest practical level of granularity, because combining them is possible and allowed in ways that they suit the customer needs.
Example: An electronic form submission may need to collect addresses as well as, say, tax information. In this case, there are two functions: one is a collection of applicant's whereabouts, and another is a collection of tax details. Each task needs to be addressed with a distinct API and requires a separate service because an address change is logically a different event and not related to tax time reporting, i.e., why one needs to submit the tax information (again) for an address change.
- In general, consider that the services may be coarse-grained, and API's are fine-grained.
- Maintain a balance between the amount of response data and the number of resources required to provide that data. It will help decide the granularity.
- Read requests are normally coarse-grained. Returning all information as required to render the page; it won’t hurt as much as two separate API calls in some cases.
- On the other hand, write requests must be fine-grained. Find out everyday operations clients needs, and provide a specific API for that use case.
Transactional
In many cases, the experience API will be calling many service API's and if one of them should fail the whole rest call should fail and rollback, not just the single API.
For example, lets say we have a travel booking API which calls multiple other API's. The customer choose a flight, rental car and hotel with valid information and the system will send them an email after. What happens if the calls are not transactional and the flight, rental car and email were successful, but the hotel failed and never book. The user would receive an email expecting their vacation to be ready, and they will be frusterated when they finally get to their hotel. This full transaction needs to be either all commit succesfully or rollback.
The same could happen when registering a new orgainzation if the orgainzation succeded and sent and email to the user but their user object failed to save to the database for some reason.
These transaction cases are not easy to solve and really depend on the the context of the problem and if its required. Information about transactions across microservices and multiple architectures can help on a case by case basis. Two Phse Commits and Eventual Consistency are the two many ways to solve these issues.
MuleSoft
MuleSoft provides a front end for RESTful API's called Anypoint that helps get new innovations to production faster by providing a COTS software that can help visualize API end points and a drag and drop interface for creating them.
This platform can run the same way on premise as well as on cloud from the developer perspective and provides the ability to throttle, limit and monitor usage so you can know which API is most popular as well as limit the amount of usage.