How to Design a Good API ?

The term “API,” which stands for Application Programming Interface, has been a fundamental concept in computer science from its inception. Whenever you develop a function, create a class, or establish an entry point, you are essentially defining an API. However, the crucial question is: Who will be using your APIs?

Who consume APIs ?

Developers consume APIs. But there are two kinds of developers. Those who work on the project, and those who will consume your project. If the only developers that will consume your API are the one of your team, there is nothing much to say. This is generally the case when you develop pure UI based application. Try to keep your code clean, respect the SOLID principles and good development practices. However, in scenarios like developing REST-based web services, C++ libraries, or Python packages, the target audience includes external developers who are not part of the project. In such cases, it becomes essential to differentiate between Internal APIs, designed for team usage, and Exposed APIs, which are meant for external consumption.

Internal API vs Exposed API

When developing a new component, you’ll come across methods that handle internal behavior, while others will act as entry points for the component’s users to call.

In most Object-Oriented languages like C++, C#, or Java, there’s a clear distinction between Private, Public, and sometimes Protected members, defined explicitly using keywords. In Python, this distinction is more of a convention where members starting with an “_” are considered private. The best practice is to make public methods serve as entry points, allowing external users to interact with the component, while keeping implementation details hidden and designated as private.

In real world, many public members are public because another class need it, and are not aimed to be called by external users. That’s the distinction between Internal and Exposed API. Internal API can be a little tricky, but there will always be a developer in your team to explain you how to use such and such method.

However, Exposed API should be as user friendly as possible. It’s really the Frontend of your product. And no matter how exceptional your product may be, if users find it challenging to utilize, it’s just garbage.

How to Design an API ?

The key to designing a user-friendly API is to prioritize writing simple examples. Instead of diving straight into the implementation of a feature and then considering how to expose it, begin by crafting an example—a concise code snippet that demonstrates how the feature will be utilized. This approach may remind you of Test-Driven Development (TDD).

TDD serves not only as a means to test your software but also as an effective way to define your API and illustrate its usage. I discussed this concept in my article titled “Tests Are Good. But What Are Good Tests?”

By practicing TDD, you ensure low coupling between modules, which promotes modularity and maintainability. Moreover, it guarantees that your API will be easy to use. After all, who would want to write dozens of initialization lines before even getting to the actual testing? TDD allows you to focus on the essential aspects of your API, making it more intuitive and developer-friendly.

To create a user-friendly API, it’s crucial to have it used by people who didn’t work on its development. Your own bias, as the creator, can hinder accurate judgment of its usability. External feedback is invaluable for making necessary improvements and ensuring a more effective design. To do so, you can do API Review.

How to Organize an API Review ?

The best way to do API review is to write a code snippet that use your API, to present it to some developers and see if they understand what the code do.

It’s essential not to reveal the methods’ implementation during this review, as the focus is on the API design, not its implementation. It’s better to do a first API review before writing the implementation anyway. Keep in mind this will be an iterative process. The API you imagine may not be implementable. So you’ll have to iterate and do other API reviews as long as your API will converge.

Good and Bad practices

If you practice C++, Qt has a very good API Design guidelines. And below are some examples I’ve encountered in my career that can be applied to any language or framework:

An object should always be in a consistent state, and thus from its creation.

Do not define init(), create(), build(), etc… methods that must be called just after object creation:

var serviceClient = new ServiceClient();
serviceClient.initialize();
serviceClient.connect("http://myservice.com");

Prefer instead:

var serviceClient = new ServiceClient("http://myservice.com");

Or using a factory, so you can return null , or a Null Object Pattern if creation fail for any reasons:

var serviceClient = ServiceClient.create("http://myservice.com");

You API should be stateless.

Not only for REST API, but for any kind of API. Creating an object, or calling a method, should not depend on current state of application. In particular, it should not be needed to call unrelated method before using a function or creating an object:

LogManager.init();
ConfigManager.init()
Database.registerConfigManager(ConfigManager.create())
// I don't know what the line above do but if I don't call them, the line below crash.
var icone = new Icone();

If your object do need some other object to be initialized, request these objects as argument:

var configManager = ConfigManager.create();
var icone = Icone.create(configManager)

As a corollary, avoid using global variable or Singleton pattern, which makes your API stateful.

Another example are what I call the “Stateful getters”, where you need to call some initialization method before you can call a getter:

object.computeSize();
object.getSize();

Solution is to use a lazy evaluation approach:

Object::getSize()
{
  if(m_size.empty())
    m_size = computeSize();
  return m_size;
}

If a method is present, it should be callable

Avoid throwing “Not Implemented” or “Not Supported” exception. It’s very frustrating to find the exact function we look for using autocompletion just to figure out at runtime that we cannot use it.

Image readOnlyImage = Image.open(ïmage.png", "r");
readOnlyImage.write(someData); // throw NotSupported

If possible, prefer splitting in Interfaces:

ReadImage readOnlyImage = Image.openRead("image.png");
readOnlyImage.write(someData); // write is no part of ReadImage interface

Do not ask for parameters that can be deducted from others.

You just look like an idiot if you request for something you can deduct by yourself. It also avoids the need to handle inconsistent parameters:

Server.download(url="http://myserver.com/folder/image.png",
                protocol="http", 
                filePath="folder/image.png", 
                fileExtension=".png")

And good luck to handle the following case 😊:

Server.download(url="http://myserver.com/folder/image.png",
                protocol="ftp", 
                filePath="folder/sound.mp3", 
                fileExtension=".jpg")

Avoiding redundancy avoid you a lot of trouble:

Server.download(url="http://myserver.com/folder/image.png")

Makes the nominal use case trivial to do

90% of your user will do the same things. Makes this 90% doable in a single line of code. Don’t provide a super generic API only to makes life easier for the remaining 10%.

var imgDims = image.getDimensions();
var widthDim = imgDims.find(dim) => dim.axis == WIDTH_AXIS);
var width = widthDim.range();

Even if an image can be a 3D multispectral time series video, 99% of peoples handle 2D images:

var width = image.getWidth();

Expose only what is strictly necessary.

Anything you expose can, and will, be used by user. And user will complain about anything that don’t work. So don’t expose that nice stuff you did during lunch or that internal function that you use to debug some specific cases. Exposing a feature is the PO’s responsibility, not the developer one.

Avoid doing thing if the user hasn’t explicitly asked you.

Some language enables you to call function at startup, when the library is loaded for example. Python has __init__.py, C# has[RuntimeInitializeOnLoadMethod()] tag, etc… Be very careful when using it. There is nothing more frustrating than experiencing crashes or error logs from a third-party when you haven’t explicitly invoked any of its functions.

Also, keep this in mind:

Not because you did something complicated means you did something smart

Home


Similar topics

Leave a comment