The Developer Utility APIs You Keep Rewriting
Every codebase accumulates the same drawer of small functions. A wrapper around
crypto that produces a SHA-256 hex digest. A UUID helper. A Base64 encode that
handles the URL-safe alphabet. A JSON pretty-printer for logs. A URL parser that
pulls the host out of a string.
None of them is hard. Collectively they are a surprising amount of surface area that nobody owns, nobody tests, and everybody copies into the next project.
This post is about when moving those to an API is genuinely the right call — and, just as importantly, when it very much is not.
Start with the honest answer: usually, don’t
If you are writing server-side code in a language with a standard library,
hashlib.sha256() and crypto.randomUUID() are right there. They are faster
than a network call by four orders of magnitude, they work offline, and they
cannot rate-limit you. Calling an API to generate a UUID inside a request
handler is worse than the helper function in every dimension that matters.
So the question is not “should hashing be an API”. It is “in which environments is the local implementation not actually available or not actually trustworthy”. There are more of those than you would expect.
Where a utility API earns its place
No-code and low-code automations. Zapier, Make, n8n, and Retool steps cannot
import hashlib. If a workflow needs to hash an email before pushing it to an
ad platform’s audience API, or generate a batch of IDs to seed records, an HTTP
call is the only mechanism available. This is the single most common legitimate
use.
Agent and LLM tool calls. A model orchestrating a workflow can call an HTTP endpoint. Giving it a code interpreter so it can Base64-encode something is a much larger security surface than giving it one narrow endpoint that does exactly that and nothing else.
Cross-language consistency. When a Python service, a Go worker, and a TypeScript frontend all need to agree on a normalised URL or a canonical hash, three implementations means three subtly different results. One endpoint means one answer. This is worth real money in systems where the hash is a cache key or a deduplication key.
Environments where you cannot add dependencies. Locked-down CI runners, customer-hosted plugin sandboxes, spreadsheet macros, database functions with HTTP extensions.
Prototypes and internal tools. Sometimes you want the thing working in the next four minutes and the dependency review can wait.
If none of those describe your situation, close this tab and use your standard library. That is the correct outcome.
What the endpoints look like
All of these are part of the Developer Utilities API — one RapidAPI subscription, one host, one response envelope.
Identifiers
UUID and ULID generation takes a kind (uuid4, uuid1,
or ulid) and a count up to 100:
curl --request POST \
--url https://apimask-developer-utilities-api.p.rapidapi.com/v1/dev/data/id/generate \
--header "Content-Type: application/json" \
--header "X-RapidAPI-Key: $RAPIDAPI_KEY" \
--data '{"kind":"uuid4","count":3}'{
"success": true,
"data": {
"kind": "uuid4",
"ids": [
"2b76b310-9a18-4edc-a88a-0f65c8ce9b88",
"86e23583-13df-4022-99b6-4a26fd0d14fb",
"b2d198e4-cc6e-4ad8-8f15-7c3a04cfb274"
]
},
"error": null,
"meta": {}
}The count parameter is the design detail that makes this usable. Seeding a
hundred test records with a hundred round trips would be absurd; one call
returning a hundred IDs is fine.
Worth knowing which kind to ask for. uuid4 is random — use it unless you have
a reason not to. ulid is lexicographically sortable by creation time, which
makes it pleasant as a primary key because inserts stay roughly sequential and
your B-tree does not fragment. uuid1 embeds a timestamp and a MAC address; it
leaks information and you almost never want it.
URL parsing
URL Parser returns the decomposed components plus a normalised form:
{
"success": true,
"data": {
"scheme": "https",
"host": "example.com",
"port": 443,
"path": "/docs",
"query": "q=api",
"fragment": "intro",
"username": null,
"has_password": false,
"query_params": { "q": ["api"] },
"normalized_url": "https://example.com:443/docs?q=api#intro"
},
"error": null,
"meta": {}
}Two fields deserve attention. query_params maps each key to an array,
because ?tag=a&tag=b is legal and a flat object would silently drop one of
them — a real class of bug in analytics pipelines. And has_password is a
boolean rather than the password itself, which is the right call: you want to
know that credentials were embedded in a URL so you can reject or redact it, and
you do not want that secret echoed back in a response body that might get
logged.
Hashing, Base64, JSON
The remaining three are what you would expect:
/v1/dev/data/hash for MD5, SHA-1, SHA-256, SHA-512, and
BLAKE2; /v1/dev/data/base64/encode and /decode
with URL-safe alphabet support; and
/v1/dev/data/json/format to validate, format, minify,
and optionally sort keys.
The JSON one has a non-obvious use: key sorting. If you are hashing a JSON body to produce a cache key or a webhook signature, key order has to be deterministic or the same logical payload produces different hashes. Sort, then hash.
The security line you should not cross
Be clear about what a hashing endpoint is for.
It is for content fingerprints, ETags, cache keys, deduplication, and checksums. It is not for passwords. Passwords need a slow, salted KDF — bcrypt, scrypt, or Argon2 — computed where the plaintext never leaves your control. Sending a plaintext password to any third-party endpoint, including this one, is a straightforward vulnerability. No parameter makes that acceptable.
Similarly, MD5 and SHA-1 are in the list because file checksums and legacy system compatibility still need them, not because they are safe for anything adversarial. Both are broken for collision resistance. Use SHA-256 or BLAKE2 for anything where an attacker benefits from producing a collision.
The same reasoning applies to what you send generally: request bodies are processed to build the response and not retained, but the correct posture for genuinely sensitive material is still to hash it locally.
A consistent envelope is the actual feature
Every endpoint above returns the same shape:
{ "success": true, "data": { }, "error": null, "meta": { } }That sounds like a small thing until you have written the fifth integration
against five APIs with five different error conventions. One envelope means one
error handler, one retry policy, and one type definition that generalises across
every call. When something fails, success is false and error carries a code
you can switch on — documented once in Errors rather than per
endpoint.
Where to go next
If regular expressions are on your list of things you keep rewriting, they are in the same product — generating and explaining regex with an API covers those two endpoints and the cases where regex is the wrong tool entirely.
And if the utility you actually needed was email checking, that is a different kind of problem: it depends on DNS and external state, not just string manipulation, which is exactly why validating emails before signup is worth doing properly rather than with a pattern.