Back to Blog

Don't Hand Them Your Table

The moment something depends on your API, you've made a promise, and you usually can't see who you made it to. Which is why the worst thing you can do is expose your raw table. Here's the design lesson, and the argument that taught it to me.

Years ago I worked on a product that started out bring-your-own-server. It ran in each customer's own environment, on their infrastructure, so their integrations could talk straight to the database. It was their box and their data, and when they wanted something, they reached in and took it.

That model was slow to run. Every customer environment meant VPNs to reach it and customers stranded on their own legacy versions, a pile of operational hoops between us and moving at any speed. So we started a lift and shift onto our own AWS: same architecture, same one-deployment-per-customer shape, just hosted in our cloud instead of theirs. And that quietly changed everything about data access, because the database now lived in our environment, not the customer's. Reaching straight into it was over. Customers needed a supported way to get at their own data, which meant an API.

A product owner came to me and said a customer needed an API for one of our tables. Fine. I asked the obvious question: which columns do they actually need? He didn't know. "Just give them all of them," he said.

I pushed back, and the argument that followed is most of the reason I have opinions about this at all. Exposing every column, I said, means we've just promised every column, forever. What happens when we change that table? "Well," he said, "if we drop a column, we drop it from the API." And there it was, the whole misunderstanding in one sentence. Dropping a column from a live API isn't a cleanup. It's a breaking change, and somewhere out there a customer's integration falls over the moment we ship it.

He was writing a cheque I would be paying off for years.

An API is a promise, not a window

That's the thing the "just give them everything" instinct misses. The moment a customer wires their system to your API, they've signed an agreement with its exact shape: these fields, these types, this behaviour. You can't see them, you often can't name them, and you can't reach into their code to fix it when you change yours. Inside your own codebase you can rename whatever you like, because you can see every caller and fix them in the same commit. An API is the opposite. Its whole job is to be depended on by people and systems you don't control, which means every part of it you expose is a promise you're now on the hook to keep.

Don't hand them your table

Which is exactly why "give them all the columns" was the wrong answer, and not in a harmless way. It quietly welds your public contract to your internal database schema, so that every future change to that table, a rename, a type change, a column you simply don't need anymore, becomes a breaking change to a customer you can't warn.

An API should be a deliberate facade over your data, not a mirror of it. You choose what to expose. You name it for what it means to the consumer, not for whatever your column happens to be called. You keep the surface as small as the customer actually needs, not as wide as your table happens to be.

Concretely, "give them the whole table" means shipping the raw row, every column, internal names and all:

{
  "cust_id": 4021,
  "f_name": "Dana",
  "l_name": "Okafor",
  "addr_line_1": "12 King St",
  "created_ts": "2026-03-01T09:22:11Z",
  "internal_status_cd": 2,
  "legacy_import_flag": true,
  "pricing_tier_id": 7
}

You just promised all of it: the cryptic abbreviations, the internal status code, the legacy flag you'll want gone next quarter, the pricing tier you'd rather not expose at all. A deliberate facade promises only what the consumer actually needs, named for what it means:

{
  "id": 4021,
  "firstName": "Dana",
  "lastName": "Okafor",
  "city": "Toronto"
}

Do that, and the table underneath stays free to change, because what you promised was the facade, not the schema behind it. The columns you didn't hand over are the columns you're still free to touch. "Give them all of them" throws that freedom away before you've even shipped, in exchange for saving one short conversation about what the customer actually needed.

Design so you rarely have to break

Once you accept that the API is a promise, the most useful habit is designing so you almost never have to break it. And most of the time you don't have to, if you know the difference between the two kinds of change.

Adding is safe. A new field in a response, a new optional parameter, a whole new endpoint: a well-behaved consumer ignores what it didn't ask for, so nothing breaks. Changing what's already there is the dangerous kind. Renaming a field, removing one, altering its type or its meaning: those break the contract, because something out there was counting on the old shape.

Design a deliberate, minimal surface and prefer adding over changing, and you can evolve an API for years without ever betraying the people who depend on it. That's the whole game. Not clever tricks, but a contract shaped so it can grow without breaking.

Sometimes, though, you genuinely do have to break something. The old shape has to go, and there's no additive way around it. That day comes for every long-lived API, and how you handle it, how you break a promise to people you can't even see without leaving them face-down, is its own discipline. That one's a separate post.

I've written before about legacy systems whose worst trait was an implicit contract nobody had written down. Designing an API well is that lesson turned right side up: you decide what you're promising, on purpose, out loud, and you shape it so you can actually keep the promise. Which starts, always, with not handing someone the whole table because asking what they needed felt like too much trouble.

Share this article

Want to Work Together?

Let's discuss how I can help with your project.

Get in Touch