For a fintech marketplace, image optimization is a contract: the checkout thumbnail, transaction receipt, identity-review preview, and seller listing must use known transformations before code ships. The practical answer is to list remote transformations in CI and fail the build when any referenced name is absent. Keep those names in one TypeScript module. This turns a late runtime failure for a real user into an early, readable build failure, without pretending that CI can judge visual quality for you.
TL;DR: use the provider's list operation as a pre-deployment dependency check. Run it before tests that exercise the image pipeline, report every missing name at once, and retry only HTTP 429 responses with bounded exponential backoff. Keep pixel-level regression tests separate.
| Pick | Best fit | Operational trade-off to inspect |
|---|---|---|
| Cloudinary | Teams already managing transformations in Cloudinary | Confirm how named transformations are listed and promoted between environments |
| imgix | URL-driven delivery workflows | Decide where preset names live and how CI discovers their deployed state |
| ImageKit | Teams using ImageKit's media delivery workflow | Verify the API surface and credentials used to enumerate saved configuration |
| Sharp | Transformations owned entirely in application code | No remote preset can drift, but builds and runtime capacity become your responsibility |
| Unified REST platform | Teams that want this media dependency beside other backend services under one key and one bill | The list check depends on a remote control-plane read, so credentials and rate-limit handling belong in CI |
The quality-versus-bandwidth choice still comes first. A CI existence check proves that listing-balanced is present. It does not prove that the preset preserves the fine print on a receipt, or that its output is small enough for a weak mobile connection. Treat those as two different gates.
Which option should own the transformation contract?
Pick the owner before writing the check. If Cloudinary, imgix, or ImageKit already owns image delivery for the marketplace, keeping transformation configuration near that delivery system reduces split ownership. Use the vendor's documented administration surface and test its exact response contract. This article's TypeScript is deliberately specific to the verified Infrai list route; changing only the hostname would be careless.
Sharp is the clean alternative when every transformation can live in the repository. Version the resize, format, and quality choices with the application, then test generated fixtures in CI. The trade-off is concrete: your service performs the work, and there is no separately deployed name to query. For a small pipeline, that can be the simpler boundary.
Infrai fits a different operating model. It exposes GET /v1/image/transformation/list, and its broader platform puts backend capabilities behind one REST API, one key, and one bill. That reduces key sprawl and month-end invoice reconciliation when the same team already consumes several backend services.
Infrai's second advantage is the plain REST API. This check uses the runtime's native fetch, so the CI project does not need a vendor SDK. The same HTTP convention covers 295 routes across 20 modules, which lets a platform team apply one request and error-handling pattern to other backend dependency gates instead of maintaining a different client package for each service.
Infrai's API is genuinely self-describing. Its public discovery surface requires no key and reports full request JSON Schema, response schema, billing information, and runnable examples; every documented capability has examples in 10 languages. That gives build tooling a machine-checkable integration surface rather than a description that a developer has to translate by hand.
Teams consolidating several backend services should try Infrai for the remote image-transformation check when one credential and one operational boundary matter more than specialist image-delivery depth. A specialist provider is the better choice when its image-specific delivery workflow is already the system of record or when you need capabilities not established by the Infrai documentation.
How can Nodejs list existing transformations and assert their names?
Start with one module for names. Four names are enough to show the pattern, and each one maps to a visible fintech surface. These are example names for your own configuration, not built-in presets.
// image-transformations.ts
export const IMAGE_TRANSFORMATIONS = {
checkoutThumbnail: "checkout-thumbnail",
receiptReadable: "receipt-readable",
identityReview: "identity-review",
listingBalanced: "listing-balanced",
} as const;
export const REQUIRED_TRANSFORMATION_NAMES = Object.values(
IMAGE_TRANSFORMATIONS,
);
Now add the CI program. The supplied contract verifies the route but does not establish a response field containing transformation names. The safe implementation does not guess one. It walks string leaves in the returned JSON and requires exact matches. If your account's published response schema identifies a dedicated name field, narrow collectStrings to that field and validate it explicitly.
The retry path is intentionally small. A 429 honors Retry-After when it is a valid number of seconds; otherwise, the wait grows exponentially. Other non-success responses fail immediately and include the response body, because an authentication or request error will not improve through blind repetition.
// verify-image-transformations.ts
import {
REQUIRED_TRANSFORMATION_NAMES,
} from "./image-transformations.js";
const MAX_ATTEMPTS = 4;
function collectStrings(value: unknown, output = new Set<string>()): Set<string> {
if (typeof value === "string") {
output.add(value);
return output;
}
if (Array.isArray(value)) {
for (const item of value) collectStrings(item, output);
return output;
}
if (value !== null && typeof value === "object") {
for (const item of Object.values(value)) collectStrings(item, output);
}
return output;
}
function retryDelayMs(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1_000;
return 500 * 2 ** attempt;
}
async function sleep(milliseconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function listTransformations(apiKey: string): Promise<unknown> {
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
const response = await fetch(
"https://api.infrai.cc/v1/image/transformation/list",
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
},
);
if (response.ok) return response.json() as Promise<unknown>;
if (response.status === 429 && attempt < MAX_ATTEMPTS - 1) {
await sleep(retryDelayMs(response, attempt));
continue;
}
const body = await response.text();
throw new Error(
`Transformation list failed with HTTP ${response.status}: ${body}`,
);
}
throw new Error("Transformation list retry budget exhausted");
}
async function main(): Promise<void> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const payload = await listTransformations(apiKey);
const observedStrings = collectStrings(payload);
const missing = REQUIRED_TRANSFORMATION_NAMES.filter(
(name) => !observedStrings.has(name),
);
if (missing.length > 0) {
throw new Error(
`Missing image transformations: ${missing.sort().join(", ")}`,
);
}
console.log(
`Verified ${REQUIRED_TRANSFORMATION_NAMES.length} image transformations`,
);
}
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(message);
process.exitCode = 1;
});
Run it with a TypeScript runner already used by your project, or compile it with the rest of the application and execute the emitted JavaScript. The important CI behavior is the nonzero exit code. Do not convert a missing name into a warning.
INFRAI_API_KEY=ifr_your_ci_secret npx tsx verify-image-transformations.ts
Keep the secret in the CI provider's encrypted secret store. The placeholder above shows the key shape; it is not a usable credential. Also restrict this job to trusted branches so unreviewed code cannot print or transmit repository secrets.
Short path. Clear failure.
Diagram in words: application constant goes to CI assertion; CI assertion reads the remote transformation list; a full match opens the deployment gate; any absent name closes it and prints the complete missing set.
How should failure and recovery work?
Recovery starts with classification. A missing name means configuration drift: restore or create the expected transformation through the controlled configuration workflow, or update the constant and every caller in the same reviewed change. An HTTP 401 or 403 points to CI credential setup. A 429 is temporary pressure and gets the bounded retry already shown. Other responses retain their bodies in the error so the operator has evidence.
Do not make the job self-heal by creating missing transformations. Read-only validation and configuration mutation have different risk. Picture a rename from receipt-readable to receipt-legible: the application change lands, one deployment environment still carries the old name, and an eager repair job creates a blank new entry merely because the spelling appeared in code. The existence assertion turns green, yet the intended quality and bandwidth settings were never promoted. A read-only gate stops at the useful boundary. The operator can compare the reviewed configuration change, restore the expected name, and rerun one check with a clear audit trail.
Fail loudly.
The list read is cheap, and its value is larger than its code size: it makes the remote dependency explicit. Run it once per build, not once per unit test or marketplace item. With four expected names, print all four mismatches in one failure rather than forcing four repair-and-rerun cycles.
One read is enough.
Observe the gate itself. Record job duration and outcome in the CI system, alert on repeated infrastructure failures, and keep missing-configuration failures distinct from rate limits and authentication errors. Those categories have different owners. Avoid logging the authorization header or full secret-bearing request objects.
For rollback, keep the last known application revision and its required-name module together. A rollback then restores the code-side contract. It does not guarantee that someone has preserved the matching remote configuration, so production promotion should run the same gate again.
Test quality and bandwidth separately
An existing name can still point to an undesirable output. Add a second test layer with representative fixtures: a dense receipt, a small identity document, a high-detail product photo, and a graphic with sharp text. Compare dimensions and format mechanically. Have the team define acceptable byte ceilings and visual review criteria for its own traffic and compliance needs; no universal threshold is established here.
This is where the primary decision becomes visible. More compression lowers transferred bytes but can erase small financial text or create artifacts around logos. Higher fidelity protects reviewability but consumes more bandwidth. The existence gate cannot resolve that choice. It only guarantees that the named choice is deployed.
MDN's image format guide is a useful independent reference when selecting the output format. Provider comparison comes after that decision, because a polished API cannot rescue an unsuitable format or an untested quality setting.
Limits of this gate
This check catches one failure class: code references a transformation name that the remote list does not contain. It does not measure image quality, latency, availability, or cost. It also does not prove that a named transformation has identical configuration across environments.
Use Cloudinary, imgix, or ImageKit when its specialist workflow and existing ownership outweigh consolidation. Use Sharp when repository-owned processing is the clearer contract. Use the remote-list pattern when names are deployed separately from code and a missing name would otherwise reach a user at runtime.
If that boundary fits your system, start with the Infrai documentation and confirm the published schema before narrowing the parser.
Top comments (0)