What is the cost of a bad API? Try multiplying all of your tech debt by your number of users.

In a team environment, the pain of a bad internal API presents itself readily: frustrated developers and delayed delivery. In a public API, the costs are spread across many teams and thereby vastly overpaid. And despite being more costly in total, a bad public API can resist needed improvements. The maintainers may not feel sharp feedback immediately, and downstream developers may have no choice (an org-level mandate) or alternatives (a monopoly).

If you write code, you write APIs. You should care about the costs they inflict. In this article, I’m going to show examples of how a good API can guide you deterministically towards correct usage, and how a bad API can waste your time and erode your sanity.

I’ll show a real-world example in a bit. But first, what does a good API look like?

Good API by (tiny) Example

Suppose you are implementing login using a third party library that handles acquiring authentication tokens. You glance at the docs, but decide to read a few method signatures before going further.

One small piece of the public API is as follows:

A hypothetical “Login” API.

You can see some requirements and capabilities.

login(credentials: Credentials) looks useful. It’s not immediately clear how to acquire an instance of Credentials , but since it’s a concrete type (instead of, say, a plain String ) it shows you where to look for more information.

looks useful. It’s not immediately clear how to acquire an instance of , but since it’s a concrete type (instead of, say, a plain ) it shows you where to look for more information. Authorization contains a String “authToken”, but the constructor is effectively private. You must use an instance of AuthApi to get one. That makes sense — the library is supposed to handle fetching tokens.

contains a “authToken”, but the constructor is effectively private. You must use an instance of to get one. That makes sense — the library is supposed to handle fetching tokens. You need an instance of AuthApi , and there is a builder.

, and there is a builder. Causes are co-located with effects. login() has a return type.

Like a directed acyclic graph, each piece of this small API surface area points you towards the next relevant requirement. There are more details to explore ( Credentials , for instance) but everything is signposted.

This principle becomes more important as the API surface area grows. A good library doesn’t necessarily hide complexity — it can model it, and even teach you how a complex underlying protocol works while you implement it. It encodes complexity into a chain of steps for the consuming developer to follow, and provides guardrails along the way.

You could always argue about how to improve this small example, but you could never claim that it’s leaving you clueless.

But maybe Authentication just lends itself to a clean API?

Nope.

How bad could it be?

Suppose the AuthApi looked like this instead. Remember, this is a small piece of a potentially larger API:

Take that in for a second.

Surely login() is the crucial method call, but what are the requirements? Does that encapsulate both anonymous and user login?

is the crucial method call, but what are the requirements? Does that encapsulate both anonymous and user login? setCredentials looks important. Is that Map for combinations of username/passwords? If not, what are the inputs and where do I get them? String isn’t type-specific to authentication. This API must be expecting a specific type of string, so where are the guardrails?

looks important. Is that for combinations of username/passwords? If not, what are the inputs and where do I get them? isn’t type-specific to authentication. This API must be expecting a specific type of string, so where are the guardrails? What is the expected effect of calling login ? The void return doesn’t point towards the expected outcome.

This API surface area does nothing to nudge towards correct usage. The types don’t indicate or enforce any order of operations. Effects are not proximate to their causes. You need to manually hunt for other relevant classes, methods, and types. Nothing is signposted. This API is happy to let you run headlong into every possible error condition without guidance. The documentation has the incredible burden of explaining all of this — and where that falls short, you are on your own.

But no one would put up with that. How about a real example?

I just showed it to you.

That’s a small piece of a public API maintained by a large company of high technical proficiency, and in use by thousands of developers. I have modified the type names because the intention is not to shame anyone. The API solves a difficult problem, and I appreciate that. However, it also wastes the time of downstream developers by neglecting to model itself. That cost is being paid over and over again with every new consumer.

A good API does the opposite: users can’t help but fall into correctness as they explore a potentially complex interaction. It offers affordances, feedback, and will signpost requirements. The design principles often applied to products and UI are just as applicable to programming interfaces (check out applying design concepts to code for more on that topic!).

Collin writes APIs with Livefront and he thanks you for reading.