Skip The API When There's No Logic To Share

The question worth asking before you build an integration isn't "should this go through an API," it's "what logic would that API actually be protecting me from duplicating." If the honest answer is none, you're not designing an architecture, you're adding a detour.
I had this conversation again this week, in a slightly different costume than usual. A new service needed a small, specific piece of data: given an organisational unit, find its child units and a handful of delivery locations. The existing pattern in the codebase was to reach for a client package and call an internal API to get it. The API would run a query, wrap the result in JSON, and hand it back over the wire. My colleague asked the obvious question: why not just query the table directly? We know the org unit ID. We know the shape of the data. What is the API actually doing for us here, other than adding a network hop and a dependency to keep in sync?
The honest answer was nothing. The API in that case wasn't encapsulating anything. It wasn't hiding a set of business rules that might change, or joining across tables in a way that took real thought to get right, or applying access logic on behalf of the caller. It was a thin pass-through: query a table, serialise it, ship the JSON. Calling it through an API meant carrying a package dependency, handling its failure modes, and trusting its versioning, all to get something we could read directly with one query and considerably less code.
That's the test I keep coming back to when this comes up, and it comes up constantly: would going direct mean duplicating logic that genuinely lives somewhere else, or would it just mean skipping a formality? Those are very different situations, and treating them the same is where a lot of unnecessary API surface comes from.
The case for the API shows up when there's something worth protecting. We'd made this same call on another project a few months earlier, where a service needed order data that had originally come from an older external system. Building a report against that data meant re-deriving joins, status logic and edge cases that had already been solved, tested and quietly patched over a couple of years in the system that already served it. Querying the underlying tables directly there would have meant rebuilding that logic badly, and then maintaining two versions of it that would inevitably drift apart. That's a real API: not a courier for rows, but a contract around logic that's expensive to get right and dangerous to get wrong twice.
The distinction sounds obvious stated plainly, and yet it's easy to lose sight of once you're inside a codebase that has a house style. Once a team gets used to "we always go through the API," that habit starts making decisions for people instead of the actual situation making them. Consistency is a fine default until it becomes a substitute for judgement. The reflexive version of consistency says every access to another system's data goes through its API, no exceptions, because that's the pattern. The useful version asks what the API is buying you in this specific case, and is willing to say "nothing" out loud.
There's a cost on both sides worth being honest about. Going direct to a database means coupling yourself to its schema, and schemas change under you in ways that a well-designed API would have absorbed. That's a real risk, not a hypothetical one, and it's the strongest argument for the API-first habit. But it's a risk you take on deliberately, weighed against the cost you're avoiding: an extra package to version, an extra service to keep available, an extra layer of translation that adds nothing but the appearance of decoupling. For a single table, read-only, no business rules attached, that trade tips toward going direct almost every time. For anything with real logic behind it, it tips the other way just as clearly.
What made this particular decision easy wasn't a rule, it was precedent we'd already tested. We'd made the equivalent call before, on a service with a similar shape, and it had worked out fine: no drift, no surprises, considerably less code to carry. That gave us more confidence in the direct-query call this time than we'd have had reasoning about it cold. That's worth naming, because it cuts against a common instinct in engineering to treat every decision as needing its own first-principles argument. Sometimes the right answer is "we've done this exact trade-off before and it held up," and that's a perfectly good reason, provided you're honest about whether the new situation actually matches the old one.
None of this is an argument against APIs. It's an argument against letting the existence of an API become the default answer to "how do two systems talk to each other," when the real question is what that API is protecting anyone from. An API that hides genuine complexity earns the overhead of building and running it. An API that just relays a table earns nothing but upkeep. The judgement call is knowing which one you're looking at before you commit to carrying it.


Share your thoughts