Representational State Transfer (REST) APIs Part 2 of 4

Good Design Practices

A well designed cloud service uses HTTP verbs (methods) explicitly followed by nouns in URIs. The verbs HEAD, POST, GET, PUT, DELETE, and OPTIONS are all that is needed and are already defined by the protocol. This allows clients to be explicit about the operations they invoke. The API should not define more verbs or remote procedures, such as /addcroissant or /updatecroissant. The body of an HTTP request transfers resource state, it does not carry the name of a remote method or remote procedure to be invoked.

A RESTfull service such as AWS S3 exposes standard HTTP resources (objects). Each resource responds to one or more of the six standards verbs (methods). Every resource exposes the same interface and works the same way. For example, to get an object’s value an application issues a GET request to that object’s URI. To get just the object’s metadata the application sends an HEAD request to the same URI. In other words, to expose your service you do not have to create a new set of verbs or interject method names in the URI but you must think carefully about your resource design.

In the case of S3 you have three types of resources each having its own URI:

  1. List of buckets (https://s3.amazonaws.com).
  2. A particualar bucket (https://s3.amazonaws.com/{bucket name}).
  3. A particular object (https://s3.amazonaws.com/{bucket name}/{object name}).

The following code snippet example uses curl  to issue a HEAD request to the test service located at  www.httpbin.org.

$ curl -I www.httpbin.org  

The service returns the following information:

 
HTTP/1.1 200 OK
Connection: keep-alive
Server: gunicorn/19.7.1
Date: Fri, 27 Apr 2018 22:50:11 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 13129
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0
Via: 1.1 vegur

Stateless Design

Cloud REST services need to scale to meet high performance demands. Clusters of servers with load-balancing and failover capabilities, proxies, and gateways allow requests to be forwarded from one server to the other to decrease the overall response time of a service call. Using intermediary servers to improve scale requires clients to send complete, independent requests which include all data needed by a request so that the components in the intermediary servers may forward, route, and load-balance without any state being held locally in between requests.

A complete, independent request doesn’t require the service to retrieve any kind of application context or state. A client application includes within the HTTP headers and body of a request all the parameters, context, and data needed by the server-side component to generate a response.

A stateless service not only performs better, it shifts most of the responsibility of maintaining state to the client application. In a REST service, the server is responsible for generating responses and for providing an interface that enables the client to maintain the application state. For example, in the request for a multipage response, the client should include the actual page number to retrieve instead of simply asking for the next page as shown in the next figure.

A stateless service generates a response that links to the next page number in the set and lets the client do what it needs to keep this value around. This service design can be divided into two groups of responsibilities that clarifies how a stateless service can be created:

Service Responsibilities

  • Generate responses that include links to other resources. This allows client applications to navigate between related resources. This type of response embeds links.  If the request is for a parent or container resource, a typical REST response might also include links to the parent’s children or subordinate resources so that these remain connected.

  • Generate responses that indicate whether they are cacheable. This improves performance by reducing the number of requests and by eliminating some requests entirely. The service does this by including a Cache-Control and Last-Modified (a date value) HTTP response header.

Client Responsibilities

  • Use the Cache-Control response header. This determines whether to cache the resource (make a local copy). The client also reads the Last-Modified response header and sends back the date value in an If-Modified-Since header to ask the service if the resource has changed. In this conditional GET,  the service’s response is a standard 304 code (Not Modified) which omits the actual resource requested if it has not changed since last time it was modified. The client can safely use the cached resource, bypassing subsequent GET requests.

  • Send complete requests that can be serviced independently of other requests. The client uses HTTP headers as specified by the service and send complete representations of resources in the request body. The client sends requests that make very few assumptions about prior requests, the existence of a session on the server, the server’s ability to add context to a request, or about application state that is kept in between requests.

This collaboration between client application and service is essential to for a  stateless service. It improves performance by saving bandwidth and minimizing server-side application state.

Directory like URIs

The URIs determine how intuitive the REST service is and whether the service is going to be used in ways that the designers can anticipate.  URIs should be intuitive to the point where they are easy to guess. Ideally a URI should be self-documenting and must requires little explanation to understand how to access resources. In other words, a URI should be straightforward, predictable, and easy to understand.

One way to achieve this level of usability is to define directory like URIs that is the URI is hierarchical, rooted in a single path. Its branches are subpaths that expose the service’s main areas.  A URI is a tree with subordinate and superordinate branches connected at nodes. For example, in

http://www.sunshinebakery.com/croissant/{croissant-name}

The root /croissant (bucket) has a series of croissants (objects) such as chocolate-truffle, raspberry-jam, and so on, each of which points to a croissant type. Within this structure, it’s easy to get a croissant by typing the specific croissant name after /croissant/.

Additional URIs Guidelines

  • Hide the server-side scripting technology file extensions (.jsp, .php, .asp), if any, so you can port to something else without changing the URIs.
  • Keep everything lowercase.
  • Substitute spaces with hyphens or underscores (one or the other).
  • Avoid query strings as much as possible.
  • Provide a default page or resource as a response instead of using 404 Not Found code if the request URI is for a partial path.
  • URIs should also be static so that when the resource changes or the implementation of the service changes, the link stays the same. This allows bookmarking.
  • It’s also important that the relationship between resources that’s encoded in the URIs remains independent of the way the relationships are represented where they are stored.

XML or JSON Format

The representation of a resource reflects the current state of the resource and its attributes, at the time a client requests it. The representation is a snapshot in time.

Client and service exchange  a resource representation in the request/response payload or in the HTTP body using XML or JSON format. It is very important to keep things simple and human-readable. The following are some guidelines to keep in mind:

  • The objects in the data model are usually related and the relationships between data model objects (resources) should be reflected in the way they are represented for transfer to a client application.
  • The client applications should have the ability to request a specific content suited for them. So the service should use the built-in HTTP Accept header, where the value of the header is a MIME type.  This allows the service to be used by a variety of clients written in different languages running on different platforms and devices.

Using MIME types and the HTTP Accept header is a mechanism known as content negotiation. This lets clients choose which data format is right for them and minimizes data coupling between the service and applications.

See also Representational State Transfer (REST) APIs Part 3 of 4.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.