API Design Best Practices by Alibaba Researcher Gu Pu

Scope

This article mainly describes general API design and especially applies to the design of remote call APIs (RPC APIs or HTTP/RESTful APIs). However, problems specific to RESTful APIs are not discussed in this article.

API Design Rule: What Is a Good API?

In this section, we’ll try to summarize the features (or design principles) that a good API should have. Let’s summarize some basic principles. The so-called basic principles are those that, if followed, can reduce the chances that an API will have design deficiencies and problems during the subsequent API evolution process.

  • Provides a good mental model: An API is used for interaction between programs. However, how an API will be used and maintained depends on users’ and maintainers’ clear and consistent understanding of the API. It is actually very hard to have a clear understanding.
  • Simple: Make things as simple as possible, but no simpler. In most cases, APIs are designed to be too complicated instead of being too simple in a real system, especially when designers consider the fact that the system will evolve as the requirements of the system continuously increase.
  • Allows multiple implementations: This is a more concrete principle and my favorite one. This principle is emphasized a lot by Sanjay Ghemawat. Generally, the decoupling principle, or loose coupling, is often mentioned in API design. However, compared with loose coupling, this principle is more implementable: If an API allows multiple different implementations, that API has already had good abstraction and is not related to one of its specific implementations. Therefore, that API is usually not tightly coupled with external systems. This principle is more essential.

Best practices

This section gives some more detailed and concrete design suggestions on how to easily design APIs that follow the preceding basic principles.

A Good API Example: Posix File API

If only one API design example is listed in this article, then the POSIX File API is perhaps the most helpful and implementable design example. Therefore, we could also call this article “Learn best practices of API design from the File API”.

int open(const char *path, int oflag, .../*,mode_t mode */); 
int close (int filedes);
int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);
  • The File API has been around for decades (nearly 40 years since 1988). Although several generations of hardware and software systems have been developed, this set of core APIs remain stable. This is amazing.
  • This API provides a clear concept model that allows you to quickly understand the basic concepts behind this set of APIs: the definition of a File and its related operations (open, close, read, write).
  • The File API supports different file system implementations, which can even target different types of devices, including disks, block devices, pipes, shared memory, networks, and terminals. Some of these devices allow random access. Some only support sequential access. Some are persisted while others are not. However, all different devices and file system implementations can use the same interface so that the upper system layer does not have to pay attention to the differences in underlying implementations. This is a very powerful feature of this set of APIs.
int open(const char *path, int oflag, .../*,mode_t mode */);

Document Well

Write details in documents and keep documents updated. This is obviously important. However, the truth is that many API designers and maintainers do not pay adequate attention to the documentation.

Carefully Define the “Resource” of Your API

If applicable, define resources by combining operations and resources. Today, many APIs can be defined in an abstract manner. This definition method has many advantages and is also suitable for designing RESTful APIs that use HTTP. In the process of designing an API, one important prerequisite is to properly define the resource itself. What is a proper definition? A resource itself is the abstraction of a set of core API operation objects.

Choose the Right Level of Abstraction

Choose the right level of abstraction when designing objects. This is closely related to the previous question. Different concepts are often correlated. Again, take the File API for example. Multiple abstraction levels are available when we design such an API, such as:

  • Text and image hybrid object
  • “Data block” abstraction
  • “File” abstraction

Prefer Using Different Models for Different Layers

This principle is closely related to the previous one. However, it emphasizes that different layers have different models.

Naming and Identification of the Resource

After an API defines a resource object, a name and identification must be provided. Two options are available for the naming/ID (not the ID within the system, but the ID that will be exposed to users):

  • Use a free-form string as the ID (string nameAsId)
  • Use structured data to express the naming/ID
{
disk: string,
path: string
}
{
account: number
routing: number
}
  • Have a structured or string identity already been provided to consistently and uniquely identify an object? If the answer is yes, a numerical ID is not really necessary.
  • Is a 64-bit integer long enough to meet the requirement?
  • Numerical IDs may not be very user-friendly. Are numerical IDs really helpful for users?

What Are the Conceptually Reasonable Operations for This Resource?

After determining the resource/object, we need to define which operations should be supported. At this time, the major consideration is whether it is “conceptually reasonable”. In other words, consider whether the combination of operations and a resource sounds naturally reasonable (if the resource itself is accurately and reasonably named; this is of course a big if as it's not easy to do). Operations are not always limited to CRUD (create, read, update, delete).

  • Update quota and transfer quota

For Update Operations, Prefer Idempotence Whenever Feasible

Idempotence is “the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application” [3].

  • Idempotence of the Create operation
  • Idempotence of the Update operation
  • Incremental like the IncrementBy(3) semantics
  • SetNewTotal
  • Idempotence of the Delete operation: If the Delete operation does not implement idempotence, when an object is deleted and a second Delete operation is called, an error may occur because that object is not found. Generally speaking, this is not a problem: Even if the Delete operation is not idempotent, it has no additional effect. To implement idempotent delete operations, we can use the following methods: Archive -> Purge lifecycle management (to delete data step-by-step) or Purge Log.

Compatibility

API changes require compatibility. This is very important. Specifically, the compatibility in this case is backward compatibility that allows older client versions to access newer versions on the service side (if they belong to the same major version) and prevent incorrect behaviors so that the client does not experience downtime. Compatibility is especially important for remote APIs (HTTP/RPC APIs). Many articles have described compatibility. For example, the document [4] gives some compatibility suggestions.

  • Delete a method, field or enumerated numeric value.
  • Change the name of a method or field.
  • The method name field remains unchanged, but the semantics and behaviors are changed. This type of incompatibility is often likely to be ignored.
  • We usually do not know every client that uses a specific API.
  • The release process takes time and synchronous updates cannot effectively be implemented.
  • This model does not allow backward compatibility. It will be a very complicated issue if the new API requires rollback, because it is highly likely that no rollback plan has been prepared when using this release model and not every client will roll back even if a rollback plan is available.

Batch Mutations

How to design batch mutations is another common API design decision that developers have to make. Two methods are commonly used to design batch mutations:

  • Client-side batch mutations
  • Server-side batch mutations
  • Server-side batch mutations increase the complexity of API semantics and implementations. For example, semantics and status expression that are partially updated.
  • Even if we want to support batch transactions, we need to consider whether different back-end implementations can support transactions.
  • Batch mutations pose a great challenge to the server-side performance and may easily lead to interface abuse on the client side.
  • Client-side batching allows the load to be shared by different servers (see the preceding figure).
  • Client-side batching allows the client to decide failure retry policies in a more flexible way.

Be Aware of the Full Replacement Risks

Full replacement update refers to replacing an old object/resource with a completely new object/resource in a Mutation API. The API is probably written like this:

UpdateFoo(Foo newFoo);
UpdateFoo {
Foo newFoo;
boolen update_field1; // update mask
boolen update_field2; // update mask
}

Don’t Create Your Own Error Codes or Error Mechanisms

API designers sometimes want to create their own error codes or express different mechanisms for returning errors. They want to do so because they think that it may be helpful for users if details of each API is expressed and returned to users. However, this actually makes APIs more complicated and difficult-to-use.

  • Error handling is the job of a client. However, it is difficult for a client to notice all error details, especially in large amounts. Generally, error handling is divided into two more three types at most. For error handling, the thing that a client cares about the most is whether it should retry after an error or continue to return that error to the upper layer. It does not aim to recognize different error details. Multiple error code mechanisms only make error handling more complex.
  • Some people may think that providing more custom error codes is helpful for conveying information, but this is only meaningful if a separate mechanism is established for this purpose. If custom error codes are provided solely to convey information, a field in an error massage can serve the same purpose.

More

For more design patterns, see [5] Google Cloud API guide and [6] Microsoft API design best practices. Many questions described in this article are also discussed in the documents listed in the References section. In addition, these reference articles describe common API design specifications such as versioning, pagination, and filter. The related content is not repeated in this article.

References

[1] File https://en.wikipedia.org/wiki/Computer_file

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alibaba Cloud

Alibaba Cloud

Follow me to keep abreast with the latest technology news, industry insights, and developer trends. Alibaba Cloud website:https://www.alibabacloud.com