What are the types of APIs and their differences? API lifecycle management
Tip

16 REST API design best practices and guidelines

Common guidelines for API design lead to better functionality and flexibility. Follow these REST API design best practices to help you tend to your burgeoning API garden.

APIs often provide development teams the support needed to deal with many microservices-specific problems. For instance, they can act as a mechanism that decouples the business logic from the user interface, replicate application functions for reuse, or even isolate functionality for simplified testing and deployment.

Unfortunately, APIs can also introduce problems. Frequent deployment of individual APIs can quickly create a software system that resembles an overgrown garden, weed-ridden with bugs, broken integrations and ill-fitted use cases. Bloated collections of APIs make it hard to make changes without causing failures, or even gain the visibility to recognize opportunities for improved functionality.

Proper design can help mitigate these common problems. These API design guidelines apply specifically to REST, and are primarily for developers and architects that already manage a varied collection of API implementations, methods and languages. From high-level design to interface standards to API testing, these tips will help you tend to your burgeoning API garden.

Define and document your API in one place

A concerning number of organizations have no central repository that contains a catalogue of their existing APIs, documentation on how to use them, and records of versioning and changes. Instead, every team maintains its own stash of APIs, relying on siloed developer knowledge and bulky corporate codebases.

Instead, development leads should create a policy that adds new APIs to some kind of centralized, editable system, such as a wiki. Create a visual map that lists API dependencies, and add links to a wiki page describing the API for each node within the map. These descriptions should use a standard template format that describes the API, the consumers, the maintainers, its location in the code and the tests that serve as examples. For instance, the OpenAPI open source language and tool is commonly used to define and document interfaces for APIs.

A strict API definition includes the associated commands, HTTP protocols, URLs, status, error codes and payload structure. Below is a sample definition for OpenAPI, taken from Swagger, an OpenAPI tool. Redoc and DapperDox also take this sort of API documentation and create rich, discoverable webpages.

    paths:
      /users/{userId}:
        get:
          summary: Returns a user by ID.
          parameters:
            - name: userId
              in: path
              required: true
              description: Parameter description in CommonMark or HTML.
              schema:
                type : integer
                format: int64
                minimum: 1
          responses:
            '200':
              description: OK

Use HTTP protocols to define actions

You don't need to use the /getorder command if you already use HTTP GET to make calls. Instead, this URL can be as short and simple as /order/12345 -- the resulting payload will be the information about the call.

Typically, you'll want to use these common HTTP requests:

  • POST (create an order for a resource);
  • GET (retrieve the information);
  • PUT (update and replace information);
  • PATCH (update and modify information); and
  • DELETE (delete the item).

You also can use PUT to create IDs if the client itself selects the ID number.

Make all features CRUD compatible

Create, read, update and delete (CRUD) elements combine to enable developers to create new features and testing. It's common, however, that an API does not require the full CRUD structure. For example, a mobile app may depend specifically on the read function. Or, a developer may add create but think, "I'll add delete later." Sometimes, later never comes.

Save yourself time in the long run. Implement full CRUD capability in the same deployment.

Define policy for role-based authorization

Some users can create their own account within a particular application, but do not have permission to delete it. Other users might have read permission, but not permission to update, create or delete a resource. Authentication proves someone can log in, while authorization says they can access a resource. Generally, RESTful services do this either through interaction with Lightweight Directory Access Protocol (for employees) or a customer profile object for software products with customers.

Foster sub-second end-to-end response times

REST APIs are designed to be synchronous. Aim for 100 milliseconds for internal services with no HTTP dependencies, and an upper bound of around one second for complex services inside the data center. If a function call takes too long, such as account creation, don't just let it run long. Instead, return with an accountID, or at least with a token that the client can use later to look up the account. Create guidelines for delay, and try to avoid a polling process that only records the time that a process ended and not when it started.

Nest hierarchy

Structured data exists in relationships, or hierarchy. These relationships allow you to pull a larger group, or break into that group for a specific item. Within that item there may be more detail or sub-items.

Use a nested hierarchy to define attributes. For instance, imagine you have a GET request for a product called /products/:productid. The GET request for all subsequent reviews should be /products/:productid/reviews, and a GET request for a specific review should read /products/:productid/:reviewid. This could be done with /reviews/:reviewid, and you could enable both -- using this convention consistently will improve ramp up speed and decrease the potential for error, as well as improve discoverability and prevent contrasting arguments.

Apply consistent formatting

Be diligent in your API design formatting choices to help keep your API designs organized and clean, and avoid confusion or errors.

It's often a good idea to use standard conventions, such as nouns instead of verbs in the URLs you create: /tasks/, /todos/, /orders/, and so on. Verbs such as create, get, update, or delete are defined in the type of http request (POST, GET, etc.). Adding them to the URL is redundant and makes the URL unpredictable.

The standard convention is /objecttypes/ for the object, then the unique identifier for the object. For example, a GET on /tasks/123456 would get the information about task 123456.

Another good idea is to format all URLs in lowercase letters, and force the server to follow suit. A call to /products should return the same results as /Products or /PRODUCTS. Follow the ancient UNIX adage: Be specific in how you process results, but generous in the input you accept.

Allow sort and filter

You can implement sort and filter on the URL itself with query parameters. /products?name="*testing*" returns all products that contain the word testing. The documentation shows what parameters are available. Use ?sort=ASC or ?sort=DESC to tell how to return the results, which may get more complex.

In essence, filter implements search, while sort allows the query to change the order preference. These are two of the most common features in e-commerce, or in any database. While extreme programming says "You Aren't Gonna Need It" (YAGNI), at least consider how to construct and published the URLs, so that anyone can implement them later without changing their original behavior.

Make pagination programmable

This is similar to sort and filter. The simplest way to achieve this might be /products?name="*testing*"?limit=10?offset=10. That would return the second set of ten results. This allows the programmers to change the length of pages with parameters. Use filterNames wherever possible to simplify the creation of database queries.

Use JSON for the payload

JavaScript Object Notation (JSON), is the de facto standard for REST APIs. An organization that widely uses Microsoft technologies might opt to use Simple Object Access Protocol (SOAP), which supports the Web Services Description Language (WSDL). If you point your client application to a WSDL file, you can write code against the API almost as if it were a code library.

REST, however, requires no specific interface definition, and offers wider support for data output types. If your API will interact with any non-Microsoft technology, SOAP may cause some interoperability issues.

Use standard JSON libraries

Resist the temptation to manipulate objects as strings, or use regular expressions to get the data you need. Instead, set up a JSON library for every language you use. Programmers can still maintain language flexibility, but they will use the library to either extract data from or add data to the payload. Below is a simple client program in Python that gets a response object, examines it for an error, and prints a specific element from the JSON.

import json
import requests

response = requests.get('https://jsonplaceholder.typicode.com/todos/1')
if response.status_code != 200:
# This means something went wrong.
        raise ApiError('GET /todos/ {}'.format(resp.status_code))
jsonobj = json.loads(response.text)
print jsonobj['title']

The API is a public API, JSONPlaceholder, so the code works natively in most modern versions of Python. On a Mac or Linux system, you can save this as a text file called "get.py" and then run pything get.py from the command line to see it execute. On a PC, you'll need to install Python to do this.

Use networking and REST libraries

Programmers must develop a standard way to work on the client and server, share code and collaborate. The days of rolling your own networking using the sockets library in C are long gone.

The code above uses Response, an object in Python that records and stores a server's response to an HTTP request. Express.js is another REST API framework for Node.js that enables developers to create a response object and program custom responses to GET, PUT, DELETE. Then, they can simply call a specific port number using the .listen command.

Always use a native language parser

When you consume a third-party data format that is not JSON, it can be tempting to do straight string manipulation. Instead, if that data is a format such as XML, load the string into a data structure designed to process that format. The string manipulations and regular expressions might work today, but if you rely on enough of them regularly, and the XML format changes, the code could break in unexpected ways.

For example, consider this snippet of code:

<Employee>
  <EmployeeID>12345</EmployeeID>
  <FirstName>Matthew</FirstName>
  <LastName>Heusser</LastName>
</Employee>

A simple string manipulation might look for the third line, strip out the XML and put it into a Firstname variable, then look at the fourth, do the same and put it into Lastname variable. When the other application introduces a MiddleName field, the application will not error; instead it will report the LastName as the middle name.

Require tests

Tests act as an alternative documentation; they express what the software should do by example. Various API testing tools take these examples and capture them. For smaller projects, wrap a command-line tool such as curl.

The following proof of concept for API testing takes an array as input. It includes the URL to call, what to compare, the data to expect and a comment. You can find the full system in GitHub; the core Ruby function appears below.

def execute_a_check(row)
    data = row.split(",")
    url = data[0]
    compare = data[1]
    match = data[2]
    comment = data[3]
    command = "curl -s -H " + '"' + 'Accept: application/json" ' + url
    output = `#{command}`
    output = output.gsub("\n","")
    output = output.gsub("\r","")
    output = output.gsub(",","|")
    if match == "Y" then
      escaped =  Regexp.new(Regexp.escape(compare)).to_s()
      result = (output=~/#{escaped}/)
      @tapoutput.ok(result, comment)
    else
      @tapoutput.ok(output == compare, comment)
    end
  end
end

Isolate test execution

As the number of tests increases, you need to ensure you process the correct test data consistently. One strategy for this is to prep the tests with a set of known good test data, perhaps loaded in before the suite runs and destroyed after the test is complete. As the codebase grows, you may want to concurrently run tests with real data and those known good data sets, but be sure to isolate those test runs with segmented data. Accounts or incremented user IDs are two ways to do this.

Brush up on modern API designs

It's always a good idea to bolster your knowledge about API design decisions. O'Reilly's REST API Design Rulebook and Manning Publication's API Design Patterns take a look at interface standards of REST. Manning's Microservices Patterns discusses how to string these designs together to create complex hybrid patterns, such as Backends for Frontends and the API gateway.

Another helpful resource is the Twelve Factors, a set of rules to build SaaS applications on the web. They're not specifically about API design, but include good guidance for handling the codebase, deployments, infrastructure, configurations, dependencies and more.

Dig Deeper on API design and management

Software Quality
Cloud Computing
TheServerSide.com
Close