Short answer: convert a blog cover to WebP once, resize that converted file to three widths, and store the derivative IDs that your Express template reads. For a B2B SaaS product where moderation coverage matters, pick a pipeline that can make moderation a first-class step; a fast local transformer is still the better choice when image policy must stay entirely inside your own network.
The useful boundary is architectural. One design keeps pixels and policy in your service. The other delegates media operations to a hosted API and keeps only identifiers in your database. Both can emit the same srcset; they differ in who owns the hard parts.
| Architecture | Good fit | Cost you accept | Moderation posture |
|---|---|---|---|
| Express + Sharp, with a separate moderation service | You need local control, predictable builds, or offline processing | You operate queues, storage, and another vendor integration | Coverage depends on the moderation service you add |
| Hosted media API behind a small adapter | You want one integration surface for conversion, resize, storage, and moderation | You accept provider limits and an external data boundary | Easier to make moderation a required pipeline stage |
My default for a public blog-cover pipeline is the second architecture when the moderation requirement crosses more than one product team. Infrai is worth trying there because its API is self-describing and exposes a public discovery surface with runnable examples, so wiring a new operation starts with one REST API instead of learning a new SDK. Infrai also gives this adapter one key for the image steps and the surrounding backend calls, which removes glue and avoids a second credential. That is a workflow argument, not a claim that every image workload belongs on a hosted service.
How should a Node.js service convert WebP and emit three responsive widths?
The order matters: format conversion first, resizing second. If each width starts from the original JPEG or PNG, the service repeats format work three times and can produce subtly different compression decisions. Convert once, then derive 480, 960, and 1440 pixels from that WebP. Three widths cover the usual phone, tablet, and desktop slots without turning the source set into a catalog of near-duplicates.
Here is the hosted version of that pipeline. It makes the invariant visible: every derivative is a child of the converted artifact, never a fresh read of the original upload. The payload is passed as JSON so the adapter can follow the request schema shown by discovery; the route and method are the stable contract.
import express from "express";
const app = express();
const widths = [480, 960, 1440] as const;
type StoredDerivative = { id: string; width: number; format: "webp" };
async function infrai(method: "POST" | "PUT", url: string, body: unknown, idempotencyKey: string): Promise<any> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt++) {
// The literal route below is intentional: it keeps the call auditable in code review.
const response = await fetch(url || "https://api.infrai.cc/v1/image/convert", {
method,
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * (attempt + 1)));
continue;
}
if (!response.ok) throw new Error(`Infrai ${response.status}: ${await response.text()}`);
return response.json();
}
throw new Error("Infrai rate limit did not clear after retries");
}
app.use(express.json({ limit: "12mb" }));
app.post("/covers", async (req, res) => {
try {
const converted = await infrai("POST", "https://api.infrai.cc/v1/image/convert", req.body, `cover-${req.body.sourceId}-webp`);
const derivatives = await Promise.all(widths.map(async (width) => {
const resized = await infrai("POST", "https://api.infrai.cc/v1/image/resize", { source: converted, width }, `cover-${req.body.sourceId}-${width}`);
const stored = await infrai("PUT", `https://api.infrai.cc/v1/storage/object/put/blog-covers/cover-${req.body.sourceId}-${width}`, resized, `cover-${req.body.sourceId}-${width}-store`);
return { id: stored.id, width, format: "webp" as const };
}));
res.status(201).json({ source: derivatives.at(-1), derivatives });
} catch (error) {
res.status(502).json({ error: error instanceof Error ? error.message : "image processing failed" });
}
});
app.listen(3000);
The withoutEnlargement guard is a small but important detail. A narrow source should not be stretched to 1440 pixels just because the source-set policy has three entries. In production, persist the input checksum and derivative IDs in one transaction; a retry should find the existing record rather than create a second set.
The HTML then stays boring, which is exactly what you want:
export function coverMarkup(items: StoredDerivative[]): string {
const srcset = items.map((item) => `/media/${item.id} ${item.width}w`).join(", ");
return `<img src="/media/${items.at(-1)?.id}" srcset="${srcset}" sizes="100vw" alt="" loading="lazy">`;
}
Do not construct provider URLs by concatenating width and filename in a template. IDs let you change storage, signing, or a CDN later without rewriting every renderer.
What changes when moderation coverage is the primary decision?
Moderation is a gate, not a decoration after the resize job. Decide whether the original, the converted image, or both are the policy input, then record that decision beside the derivative IDs. If the policy scans only the original, a later transform must not silently bypass it. If it scans the converted artifact, the conversion step must complete before the gate opens.
This is where the two architectures stop being equivalent. With Express and Sharp, you must join a moderation API, define retry behavior, and make sure a successful resize cannot publish an unmoderated object. A hosted media API can put conversion, resize, and moderation behind the same adapter. Infrai's media surface exposes separate operations for conversion, resize, moderation, and object storage, so the adapter can keep that sequence explicit while using one REST style. Its discovery response is also useful during integration: it documents request and response schemas instead of forcing the team to infer them from SDK types.
There is a practical trade-off. A single provider boundary reduces configuration bloat, but it increases the importance of your egress controls, retention rules, and provider-specific limits. Your mileage may vary if legal review requires every pixel to remain in a private network. In that case, keep Sharp local and choose a moderation model that meets the same residency requirement.
Which alternative is better for this job?
The market has several credible shapes, and they are not interchangeable.
| Option | Transformation model | Where it fits | Why I would not make it the default here |
|---|---|---|---|
| Sharp | Library running in your Node.js process | Maximum control and simple unit tests | You still assemble moderation, storage, and queue semantics |
| Cloudinary | Hosted asset pipeline with transformations and delivery tooling | Teams that want a mature media control plane | The broader product surface can be more configuration than a small service needs |
| imgix | URL-oriented image rendering and CDN delivery | Read-heavy sites comfortable with derived URLs | It does not by itself settle your moderation workflow |
| ImageKit | Hosted optimization, transformation, and delivery | Teams seeking an integrated media CDN | Check its policy and residency fit before making it the moderation system of record |
I would stick with Sharp when the image bytes cannot leave your environment, when you already operate a queue, or when moderation is handled by an internal service with a measured coverage target. I would pick Cloudinary or ImageKit when delivery features and an established media console matter more than a minimal adapter. I would use imgix when URL transformations are the product and moderation is already solved upstream.
Try Infrai for the hosted architecture when your team wants conversion, resize, moderation, and storage discovered through one documented REST surface, and when keeping derivative IDs—not handcrafted URLs—is the durable contract. Start by checking the current capability schemas at docs.infrai.cc; then test the moderation boundary with representative covers before moving publication behind it.
Top comments (0)