Contentstack named a CMS Leader by top analyst >
Read IDC Excerpt

Principles of Effective RESTful API Design

Jul 02, 2021, Hitesh Baldaniya

The RESTful style of API has been around for more than 20 years. It is one of the most common types of services that allow clients — including browser applications, mobile and IoT devices — to talk to the servers.

If you are in the process of developing an app and have reached a stage where you are ready to create public APIs, it is worth pausing to ensure that you are on the right track. It’s difficult to make drastic changes to your APIs once they are out. So it makes sense to get as much right as possible from the very beginning. And since these APIs would form the core of your application, they should be:

  • Secure, fast, and flexible for easy scaling
  • Built using common and widely accepted standards
  • Easily understandable and consumable, enabling quick integration
  • Supported with good documentation that explains semantics and syntax

Contentstack serves billions of API requests every month without any glitches, and it’s flexible enough to scale 10x the current volume in a flash. This blog post shares some of the best practices that we follow while developing our API. These tips should be helpful to anyone starting with building REST APIs.

Make Your APIs Secure

When developing APIs, security should come first. This is a must. Since you can execute an API call from the Internet, requests can come from anywhere.

When it comes to API security, there is no reason not to use encryption. Use SSL/TLS, always. SSL security is a very straightforward and least expensive method to make the request and response encrypted. TLS is a cryptographic protocol designed to provide secure, encrypted communication over a computer network. It encrypts data between an API client and an API server, preventing data from being read if intercepted between point A and point B. TLS ensures encryption of data in transit.

Another potential security threat could be long-lived authentication or authorization tokens for APIs. A best practice is to make them short-lived. You can do this by having custom API key management using low-overhead implementation protocols like OAuth or JWT. Short-lived API tokens are much easier to use and significantly more secure.

Define Requests Clearly

It all starts with defining your requests in a way that makes your API easy to use, reduces ambiguity, and brings some consistency. Let’s look at some of the ways that can help you achieve that.

Follow request path conventions

  • Make use of resource names

    Your request path should have the name of the resource with which the API is going to interact. For example, if your app provides “products,” use “/products” in the API request as a noun. Avoid using verbs with resource names in the request (e.g., /api/create-products) since the request’s type should define the verb, as explained below.

  • Use HTTP methods

    Most developers are familiar with the typical HTTP methods such as GET, POST, PUT, and DELETE. These are the type of common API requests developers make on the resources. So, something like “GET /products” indicates that the request is to fetch products.

  • Use plural forms

    You can use plural forms for all resource names. For example, “/products” can be used to fetch all products, whereas “/products/20” is used to fetch a single product with an ID of “20.” Plural forms make your requests more consistent and intuitive.

  • Use nested hierarchy

    You can provide a structured hierarchy for nested or related resources, so it’s easier for a larger group to work on a specific item or sub-item. But keep the depth level to a minimum. For example, for products that have reviews and ratings associated with them, you can define the relationship as follows:

    • GET /products/:product_id/reviews
    • POST /products/:product_id/reviews
    • PUT /products/:product_id/reviews/:review_id

Most of the cases satisfy the resource mapping with a path. Still, certain exceptions are relevant to functionality and do not have any resources associated with it directly, such as /search or /bulk-actions. We can define such a request with its associated actions.

Use Standard Exchange Format

REST can use many exchange formats such as plain text, XML, CSV, etc. But go with JSON. JSON is a lightweight data format, allowing for faster encoding and decoding on the server-side. It can be easily consumed by different channels such as browsers, mobile devices, IoT devices, etc., is available in many technologies and is now a standard for most developers.

To ensure that your API uses JSON, use “Content-type”: “application/json” in request and response.

For the request body fields, use consistent casing, such as lowercase (recommended). For other non-textual formats, use “multipart/form-data,” which you can use for sending files over HTTP. While it also allows sending textual or numerical data, restrict its usage to sending files; use JSON for textual data. For this format, you need to use “Content-type”: “multipart/form-data” in the request header. Response header may vary based on the type of file it receives (e.g., images, application, PDFs, and documents).

Provide Standard Headers

Requests headers are used to transfer additional information from clients. Follow the standard headers to share information like content-type, basic authentication, content-length, accept, accept-encoding, and user-agent.

We recommend transferring the security and authentication parameters, such as a user token or secret tokens, into the headers.

Version Your APIs

As you start introducing more features and update existing ones, you may have to make minor or major changes to your APIs. Changes are inevitable as you grow. The best way to maintain these changes is through versioning. Versioning your APIs ensures that you continue to support older APIs and release newer APIs systematically. It also prevents users from hitting invalid URLs.

Each change should be versioned, and your APIs should be backward compatible. While doing this, you must ensure users have a way to migrate to the latest version.

For significant changes, the version should be in the request URL. Examples:

  • GET /v1/products - Version 1 of products
  • GET /v2/products - Version 2 of products

For minor changes or fixes, pass the versions in the header, where you can use “date” to track the changes, as shown in the following example:

GET /v1/products

Header:

Accept-Version: “2019-11-01”

Offer Ways to Filter, Paginate, Sort, and Search

  • Filtering: Design your APIs to filter or query the stored data via specific parameters. For example, users may want to filter products by tags, categories, or price range. You can provide filters like this: /v1/products?query={tags: ["tag1", "tag2"]}.
  • Pagination: Provide the ability to paginate responses so that users can request just what is required. For example, a mobile user may want the first five entries to show on the homepage, whereas web users may want 25 entries in a single request.

There are various ways to provide pagination. The more flexibility you offer, the better it will be for users. The “skip,” “limit,” and “per_page” are some of the most common parameters that provide enough flexibility for paginations. It is, however, also important to define certain restrictions—for example, set a max limit on items that can be fetched “per page”—so the users do not exploit your services. /v1/products?skip=100&limit=10&per_page=10

  • Sorting: It provides the ability to get the list of items in the desired order. You can provide options to sort in ascending and descending order based on values of certain fields, such as updated_at, price, etc. The users can then combine these options for more flexibility. For example /v1/products?sort=-price,updated_at.
  • Searching: Searching is different from filtering or passing basic queries. It should give the ability to perform a full-text search, helping users find relevant results faster. So, for example, when the user runs this /v1/products?text={{search}}, your API should check for relevant terms in the values of all the fields.

Field Selections

Your APIs should provide your users the flexibility to fetch only the required details instead of everything. No over-fetching or under-fetching. Field selections are helpful for low-computing devices (such as mobile or IoT) as it allows them to save on bandwidth and improve the overall network transmission speed.

You can do this by offering ways within the request to include or exclude data of specific fields. For example: /v1/products?skip_fields[]=created_at,updated_at and /v1/products?select_fields[]=title,price,color

Also, you can extend this further by offering ways to include details of other related items in the single request to save on the network traffic. For example: /v1/products?include=categories,colors,brands&select_fields[categories]=title,price,color

Structure Responses the Right Way

Your API requests need to have an appropriate response. You can achieve this by following certain standard practices. Here are some of the important ones.

Response Status Codes

Use the standard HTTP status codes in the API responses to inform the users about success or failure. While there are broad categories for these codes, each code has a meaning and should indicate the exact definition. Here are the categories of codes that you can use:

  • Informational responses (100–199)
  • Successful responses (200–299)
  • Redirects (300–399)
  • Client errors (400–499)
  • Server errors (500–599)

You can find more details on status codes here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status.

Response Body

  • Request errors should deliver proper error messages so that the user is informed about the exact issue.
  • Attach each resource with its system-provided metadata, such as created_at, updated_at, created_by, and updated_by.
  • Each successful response should be wrapped via its proper wrapper according to the path, to include the additional meta details about the response. Example:
GET /v1/users?limit=10

{
   “users”: [{
     first_name: “Amar”,
     last_name: “Akbar”,
     …
   }, {
     first_name: “Alpha”,
     last_name: “Beta”,
     …
   }],
   “skip”: 0,
   “limit”: 10,
}

Headers

Response headers play an essential role in providing information about the server’s behavior to respond to the request. Make sure your headers support the following before you make your APIs public:

  • RequestID

  • This response header helps debug the specific request instead of asking the customer different questions such as the request type, time, etc. It can trace down the entire request lifecycle on the server. You can use the “X-Request-Id: {{TRACE_ID}}” header in the response.

  • CORS

  • Your APIs should have support for CORS if they are available for public consumption. CORS (cross-origin resource sharing) is an HTTP-header-based mechanism that allows a server to indicate any other origins than its own from which a browser should permit the loading of resources.

    If your APIs are not consumable publicly or are restricted, it is best to keep them closed by providing restricted CORS values. You can find more information about CORS here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

  • Rate Limit

  • To ensure your system is secure from unwanted attacks and abuse, add rate limiting to your APIs. Rate limiting restricts users from making more than the defined requests in a given time period. While implementing this, a good practice is to provide a way for users to know about the time limits through the following headers.

    • X-RateLimit: Max requests allowed in a given time period
    • X-RateLimit-Remaining: Requests remaining in a given time period
    • X-RateLimit-Reset: Time remaining in the current period
  • Compression

  • Your API should support all types of compression on the response. This compression saves a lot of time for the network transmission of the data. Some of the popular compression types are gzip and brotli.

  • Cache Headers

  • Cache headers are mostly provided on GET calls to give information about the behavior of the request, more specifically to determine if the request was served from the cache or the origin server. You can use the following header for this: X-Cache: “HIT/MISS.” Let’s look at some of the other headers you can use:

    • Cache-control
    • This header allows the client to keep the resource stored on their end and not make requests until the cache is cleared or expired.

    • Etags
    • When generating a response, include an HTTP header Etag containing a hash or checksum of the representation. If an inbound HTTP request contains an If-None-Match header with a matching Etag value, the API should return a 304 Not Modified status code instead of the output representation of the resource.

Have Adequate Documentation and Tools in Place

Most developers would prefer reading the documentation before trying out your APIs or attempting any integration. It is, therefore, crucial to make your docs publicly available and easy to understand. There are tools like Swagger that help you generate the documentation API during the development phase. Ensure that your docs record change logs, version details, and deprecation notices whenever required.

You can provide examples of requests and responses so users know what to expect. You can also provide Postman collections that allow users to configure the environment and try out the API requests through their accounts to make things easier.

Since APIs are used by developers, it is important to follow good design practices to deliver a well-designed and effective developer experience.