21 February 2025

Solution of the month: Endpoint rename

Sometimes I think we forget that the "I" in API stands for interface. Those endpoints are often a third-party developer's only insight into your database's model. What words you use shape their understanding and affect how they build their integration.

For the API that my team maintain, I actually got an outsider's perspective a year before it became my responsibility. I was building the integration between our product and theirs before we all learned that this was to be more than just an integration, they were actually acquiring us.

The frustrating endpoint

There was a POST /client endpoint for creating client profiles and also a POST /client/check which was documented as an "Existence Checker". You supplied the authentication credential headers and an email in the request body and it would return a match, or not.

At one particularly infuriating point during development the POST /client endpoint was returning a 422 Unprocessable response with the error message "Email must be unique. Email has already been taken." but in contrast, when I fired a request at the /client/check using the same email it returned 404 Not Found. This drove me crazy! How could it simultaneously exist and not exist. I was totally blocked, unable to debug, let alone fix the problematic data. Completely dependant on painfully slow support requests.

Since the acquisition my team has inherited responsibility for that very same API, so I did some code archeology. What I found is that the endpoint documented as an "Existence Checker" is actually called "access checker" in the code but somebody had chosen to deviate from that name when writing the documentation. I thought hard about the meaning and studied the database queries in front of me, and then it struck me!

Email uniqueness is enforced at a company-level. But API access is controlled at a more granular company department level. The 404 Not Found response did not mean a match didn't exist, it meant the credentials you are using don't have access to a match!

Retrospective

I confirmed my understanding with my colleagues. Some knowledge had been lost but essentially the decision 4 years previous came from an honest – but in hindsight misguided – attempt to ring-fence each set of API credentials as if they were operating on their own distinct database. The theory was that a set of API credentials should only be able to read from and write to entities within the department it has access to.

However that theory breaks down when you consider the company-wide unique constraints. A uniqueness check during an update request essentially turns it into a read operation because the validation outcome will reveal the existence of an entity outside your permitted zone if the values match.

The tidy-up

The solution for preventing future developers from experiencing the same pain was actually all documentation changes. I corrected the name to "Access Checker" and added nuance to the usage description, explaining that it is used to check if there is a matching profile in any of the company departments that your credentials have access to. I doubled down; expanding the 404 response description to explain that a match may exist but in a department your creds don't have access to.

This gave me enormous satisfaction, especially when my colleagues reacted to the revelation with laughter and delight at the demystification of these previously obscure internal workings.

There was also a tidy-up to be done in the code of the integration that consumed it. The confusion caused by that poor documentation had led myself and colleagues to build the integration assuming clients could have multiple distinct profiles in each company department. That of course was more effort to fix, and all because of overzealous documentation.

Moral of the story

Do not attempt to illude consumers of your API into thinking you have a different model than you actually do. The illusion will break down as multiple endpoints work across various data integrity constraints. If the user doesn't have access to something, say that or say "Not Found", don't pretend "It doesn't exist". Finally, all unique constraints effectively turn writers into readers, consider if this exposes a vulnerability.