What makes a good API?
There’s a ton of thoughts and written material about writing good public API’s for things like REST or RPC but I feel like the amount of times I am writing a public API is dwarfed by the number of internal contracts and API’s I build on the daily.
I consider every layer of an application as a distinct set of API’s that layers talk to - database repositories, business logic classes, externally accessible API’s, unit tests, etc. Each layer here has its own boundaries and those boundaries I care a lot about.
Creating strong, consistent, contracts, makes these different lego pieces composable. This is really how you build flexible systems!
Things I consider when crafting an API are
Do the naming conventions match other layers? If we call something an
annotation
in on place, are we calling it anattachment
in another? That inconsistency makes fitting pieces together annoying, and potentially error prone.What data we taking into our API? Are we exposing data objects that only take (and return) what we need? Even if what we need at this layer is a subset of what a another layer needs, we shouldn’t conflate the data object layers. For example, if you have a config class for a mapping layer like below, if the
v2 engine
takes a lot of the same properties we probably still want to create a new unique interface for the v2 engine instead of re-using this config. Why? Well, what doesenableV2
even mean once you are already in thisv2 engine
? It’s kind of a nonsense thing, since it was presumably used for logic to determine if you even enter this engine or not.interface MappingConfig { // enables the v2 engine enableV2: bool // flags related to mapping useDistinctNames: bool ... } interface EngineConfig { // flags related to the engine useDistinctNames: bool ... }
This model can probably be simplified by composing the configs, but the point stands which is to isolate and control the data at each layer and not leak context
Consistent access patterns. This goes hand in hand with naming, but we want contracts and API’s within the system to play well together. If one thing works on batches and another thing works on individual units, are they easily composed with mapping? Is it simple to transform and consume data between layers? If not, then the API is clunky and needs to be adjusted. Internally to a codebase you have a lot of flexibility and I often argue that we should ruthlessly refactor our internals to make this easy for us. After all, when things are smooth we can deliver value (product, bugfixes, features) faster! I talk about this at length in my upcoming book “Building A Startup - A primer for the individual contributor” (which should come out sometime in April).
Internal re-use? Are you able to re-use sections of your code often and plug in different strategies, implementations, and components to create new functionality? If not, why not? Most non trivial codebases have a ton of supporting tooling and classes - rate limiters, loggers, feature flag providers, reflection wrappers, etc. When systems are properly componentized they allow you to plug and play building blocks. It should be obvious when this happens! A build block takes no more no less than it needs. If you find its missing something, really evaluate if that’s a generic thing that needs to be exposed or if the interactions between other objects is what needs tweaking.
Conclusion
Building internal tiered API’s is a little bit of a hand-wavy art. It’s impossible to give concrete examples because every system is different. What I can suggest is that you listen to your gut and constantly evaluate if the lego blocks you are using feel like they are fitting right, minimizing repetition, and maximizing re-usablity. It is ok to duplicate things if they act as linear separations between layers. Contracts and distinct lines drawn are important and they let you iterate safely at different locations in your codebase.