DEV Community

loading...
Cover image for Practical Advice to Good API Design

Practical Advice to Good API Design

johannea profile image Johanne Andersen Updated on ・6 min read

During a rare productive youtube session, I came across a talk on How to build good APIs and why it matters by Joshua Bloch (author of Effective Java). After watching it, I knew I had to take notes because the talk was too good to forget. So good in fact, that I wanted to share them with you.

Joshua managed to squeeze many topics into an hour, hitting both higher-level characteristics of a good API, the process of building one and some practical tips to building an API. So let’s jump right in.

Firstly, let’s go over real quick what an API is. This is not covered in the video, so if you already know, feel free to skip this part.

An API (Application Programming Interface) can be considered as a contract of how to communicate with the software behind the API. It defines what data you can fetch, what format it is in and what operations you can do on that data. Which means an API can be anything from a fully-fledged REST API or a set of methods you can call to operate on a list.

According to Bloch, there are certain characteristics you can aim for to design a good API.

Characteristics of a good API

  • Easy to learn
  • Easy to use, even without documentation
  • Hard to misuse
  • Easy to read and maintain code that uses it
  • Sufficiently powerful to satisfy requirements
  • Easy to evolve
  • Appropriate to the audience

While these characteristics are quite abstract and hard to implement, they can be used as a guideline. How to achieve these characteristics is what the rest of the post is about.

Process of building an API

The first step of building an API is to start with the requirements. However, beware of proposed solutions by stakeholders and try to extract use cases instead. Figure out the exact problem you are trying to solve instead of how the user wants it solved.

Once you have the requirements in place, start small. Write up a maximum one-page specification.

Anything larger than that and your ego becomes invested. Sunk cost fallacy kicks in and you won’t feel comfortable scrapping it.

It is low effort to make changes and it’s easy to rewrite it once you start getting feedback. Only when you start to better understand the problem you are trying to solve, should you flesh out the specification more.

As counter-intuitive as it sounds, you should start coding against your API immediately. Create interfaces and don’t bother with the implementation until you have everything specced out. Even then, keep coding against your API to make sure it behaves as you would expect. This allows you to clarify and prevent surprises.

These code snippets could some of the most important code you will write for your API. They can live on as examples and you should spend a lot of time on those. Once your API is in use, it’s the example code that gets copied. Having good examples means good use of your API, so they should be exemplary.

However, the most important thing when building an API is

When in doubt, leave it out.

Especially if you are building a public API, it becomes near impossible to remove functionality once users started using it.

Practical tips

Small heads up, many of these examples are based on Java and OOP (Object Oriented Programming). Most of it is still applicable outside of Java and OOP though.

An API should do one thing and do it well
The functionality should be easy to explain. If it’s hard to name, it’s generally a bad sign. A good API should read like prose.
Be open to splitting things up when you’re trying to do too many things in one place or putting them together when you’re doing similar things.

An API should be as small as possible but no smaller
Satisfy the requirements, leave everything else out. You can always add but never remove.
Consider the number of concepts to learn to understand the API. You should think about the conceptual weight of having to learn the API and try to keep it as low as possible.
One way to do that is to reuse interfaces where possible. By reusing interfaces, the user only has the learn that interface once.

Don’t put implementation details in the API
You should not expose implementation details to the client. It makes the API harder to change if you want to change the implementation.
An example is throwing exceptions. You might be throwing a SQL exception, but in a later version also want to implement another form of data storage. Now you have to throw an SQL exception even if you’re trying to write to a file because the users are expecting and handling the SQL exception.

Minimize accessibility of everything
Make as much private as possible. It gives you the flexibility to change names and implementations without impacting your client's implementation.

Names matter a lot
The names should be largely self-explanatory, you should consider an API like a small language. This means it should be consistent in it’s naming. The same words should mean the same thing and the same meaning should be used to describe the same thing.

// Does the same thing, but different names are used
fun remove()
fun delete()
Enter fullscreen mode Exit fullscreen mode

Documentation matters
The components of the API that are documented well are more likely to be reused. Document religiously, especially when dealing with state or side effects. The better the documentation, the fewer errors your users will experience.

Never warp an API for performance
Good API design coincides with good performance usually. Things like making types mutable or using implementation types instead of an interface can limit performance.
By bending your API to achieve better performance, you run the risk of breaking the API. For example, by making an immutable class, mutable to use less memory. While the underlying performance issue will be fixed, the headaches are forever.

Minimize mutability
Classes should be immutable unless there is a very good reason to do so otherwise. If mutability is necessary, keep the state space as small as possible.

Subclass only where it makes sense
Subclass only if you can say with a straight face that every instance of the subclass is an instance of the superclass. If the answer isn’t a resounding yes, use composition instead. Exposed classes should never subclass just to reuse implementation code.

Design and document for inheritance or else prohibit it
This applies to OOP. Avoid the fragile base class problem, which occurs when changes to a base class could break the implementation of a subclass.
If it cannot be avoided, document thoroughly how methods use each other. Although as much as possible, try to restrict access to instance variables and use getters and setters to control the implementation of the base class.

Don’t make the client do anything the module could do
Let the API do the things that always needs to be done. Avoid boilerplate for clients.

// DON'T
val circle = CircleFactory.newInstance().newCircle()
circle.radius(1)
circle.draw()
// DO
val circle = CircleFactory.newCircle(radius = 0.5)
circle.draw()
Enter fullscreen mode Exit fullscreen mode

Apply principle of least astonishment
The API user should not be surprised by behavior. Either you avoid side effects or use descriptive names to describe what the side effects are.

Fail fast
Errors should be reported as soon as possible after they occur. Compile-time is best, so take advantage of generics/static typings.

Provide programmatic access to all data available in string form
If you only use strings, the format and content become part of the API so you can never change it. So provide access to the content of the string via an object. This way you don’t have to make any promises about the format and content of strings.

Overload with care
Only overload methods if their behavior is the same. Taking the Java TreeSet constructor as an example, TreeSet(Collection) ignores order, while TreeSet(SortedSet) respects the order.

Use appropriate parameter and return types
Favor interfaces over classes for input, but use the most specific input parameter type. Don’t use string if a better type exists. You should also not use doubles or floats for monetary values, for example.

Use consistent parameter ordering across methods
Especially when parameter types are identical because you can accidentally swap the parameters around.

fun copy(source: String, destination: String)
fun partialCopy(destination: String, source: String, numberToCopy: Int)
Enter fullscreen mode Exit fullscreen mode

Avoid long parameter lists
Three or fewer parameters is ideal. Long lists of identically typed parameters can be harmful and very error-prone. If necessary, break up the function or use a helper class to hold the parameters.

Avoid return types that demand exceptional processing
Users will forget to write the special-case code, which can lead to bugs. This should be avoided in cases where the non-exceptional flow is also sufficient. For example, return zero-length arrays or collections rather than nulls.


Lastly, you should expect to make mistakes, which is why so many of these points are about being able to change things easily and less about building the perfect API from the get-go.

Please check out the talk as well, he goes into much more detail than I do here.
I hope you found these useful and feel free to share your thoughts or experiences in the comments!
Thank you for reading ❤️

Resources:

How to design a good API and why it matters - Joshua Bloch (Slides)

Discussion

pic
Editor guide
Collapse
linxsoftware profile image
Linx Software

Great post! Having read a gazillion API articles, I compiled this list of API tools

Design
-Postman
-Swagger
-Stoplight

  • Insomnia

Build
-Programming languages

  • Linx

Deploy

  • On-premise: Nginx, Apache, IIS, Linx
  • IaaS: AWS, Azure
  • PaaS: Heroku, Linx

Manage
-Kong
-Azure

  • AWS
Collapse
mnaumanali94 profile image
Nauman Ali

Awesome list. I created a list of free API design tools. Could be a nice addition to this awesome list.

linkedin.com/posts/nauman-ali_apis...

Collapse
johannea profile image
Johanne Andersen Author

Thank you! I need to look into some of these technologies. Are there any articles, in particular, you would recommend?

Collapse
linxsoftware profile image
Linx Software

I wrote this recently, maybe can give you a good idea on API development

dzone.com/articles/the-quickest-wa...

Collapse
tibi profile image
Thiébaut Champenier

Excellent summary, thank you! Nothing new but it's good to read Bloch's advice again and again.

A few errors:

use the most specific input parameter type

It's output, not input

use doubles instead of floats for monetary values

Both are wrong, BigDecimal is a good choice for example.

Collapse
johannea profile image
Johanne Andersen Author

Thank you, I was just repeating what was in the talk and I guess some of it is outdated with regards to monetary values.
For the input values, he does say to "use the most specific input parameter type", but I think it goes for both input and output.

Collapse
tibi profile image
Thiébaut Champenier

Bloch would not say such a thing, no.

I've checked his slides, he says

  • Don't use floating point for monetary values: Binary floating point causes inexact results
  • Use double rather than float

There is no relation between his two assertions. double is as bad as float for monetary values. He says so in the talk.

For the input type, he says in the talk "if you accept a collection but blow up unless you're passed a Set, that's broken." And this I agree, but the fix imho should be to make it work with any Collection. Only if not possible, declare the input as Set. We've all been annoyed by APIs accepting only a List when we have a Set that would perfectly work. But I'm not sure I'm qualified to disagree with Bloch even so slightly ;-)

Thread Thread
johannea profile image
Johanne Andersen Author

You are right, I'm sorry, I misread. I'll edit it in the post as well. Thanks for pointing it out!

For the input type, I'm no expert, but I imagine it also depends on the use case. But I completely agree with your point, if it works for any collection, a collection would also be the most specific input type there is. (I would think so at least)

Collapse
blacklight profile image
Fabio Manganiello

"Long lists of identically typed parameters can be harmful and very error-prone" - I would argue that this can be a quite language-specific remark. Languages like Python or Perl make it easy to use keyword arguments to circumvent the curse of long function signatures:

def start_camera(device='/dev/video0',
                            resolution=(640,480),
                            color_mode='rgb24',
                            rotation=0,
                            scale=(1, 1), 
                            # ...
                           **kwargs):
    cam = Camera(device=device, ...)

# ...

# Pick only some of the parameters through named arguments
start_camera('/dev/video1', rotation=90)
Enter fullscreen mode Exit fullscreen mode

Although this can be achieved in languages like JavaScript with key-value objects:

const startCamera = (props) => {
    // ...
}

startCamera({
    device: '/dev/video1',
    rotation: 90,
})
Enter fullscreen mode Exit fullscreen mode

And in Java with, as you said, helper classes - and even though such a mechanism is also available in recent versions of Python (@dataclass) I still prefer the named arguments approach because it makes a method signature really self-explanatory.

Collapse
johannea profile image
Johanne Andersen Author

I completely agree with the named parameter approach, but I still think a long list of parameters, especially of the same type is still error-prone since you don't have to use the named parameters (I can only speak for Python and JavaScript).
Plus a long parameter list to begin with usually means bad design. But named arguments definitely make it less error-prone should you choose to go down that path.

Collapse
hugekontrast profile image
Ashish Khare😎

Loved it. Well explained and it covered all the points. Thank you for writing this.🤠

Collapse
johannea profile image
Johanne Andersen Author

Thank you, I'm glad you enjoyed it 😁

Collapse
coorasse profile image
Alessandro Rodi

Hi! Nice reading! I also wrote about some useful tips recently. It's targeted to Rails developers but there are some generic learnings as well: dev.to/coorasse/rails-api-be-nice-...

Collapse
mnaumanali94 profile image
Nauman Ali

Hey! Great write up.

One point that i’m not sure about is jumping into coding the API. What’s your take on writing an OpenAPI and getting feedback on that? That way you’re iterating on a spec instead of code.

Collapse
johannea profile image
Johanne Andersen Author

Thank you!
I think that is definitely a good idea, especially in the beginning. In the talk, Bloch also suggests writing to the API as a part of developing it. That way you can get a sense of how it is to use the API while you're designing it.
I hope that makes sense

Collapse
danmarshall909 profile image
DanMarshall909

SOLID advice :)

Collapse
ton_ami profile image
Antoine

I had numerous courses about APIs and this article is what I wished to learn!

Collapse
johannea profile image
Johanne Andersen Author

I am really glad you liked it! I highly recommend watching the talk it's based upon too (Under resources)