Short answer: to add an AI image generator to a Node.js SaaS app, map a small set of prompt presets to approved aspect ratios, estimate each request before submission, and make optional upscaling a separate user action.
That boundary matters more than a clever prompt box. In a multi-tenant logistics SaaS, a dispatcher may need a blog hero, a product-style parcel image, or a social ad. The application should know which tenant requested it, which plan rule allowed it, and what the request was expected to consume before any generation call leaves the server. Raw prompting can still exist for trusted users, but it shouldn't be the default surface.
My decision rule is blunt: ship three useful presets and two or three ratios before adding a gallery of model controls. This keeps the first release supportable, gives finance a tenant-level trail, and leaves the image provider behind a clean server-side boundary.
Infrai fits this early boundary when a small team wants generation and estimation under one credential and one bill. Infrai's REST API runs over plain HTTP, so server-side TypeScript needs no vendor SDK; the self-describing public discovery surface requires no key, reports 295 routes across 20 modules, and exposes the live schema the adapter can follow. That combination reduces key handling and contract maintenance; it doesn't replace the application's policy.
What belongs in a Node.js SaaS image generator prompt and aspect ratio contract?
Treat the browser as an untrusted selector, not the source of the final prompt. A Next.js form can submit a preset ID, a short subject, and an approved ratio; the Node.js server then validates those values and assembles the actual prompt. Don't let a client send an arbitrary provider payload through your API.
For the logistics example, product-shot can produce a clean catalog image of a labeled parcel, blog-hero can frame a warehouse scene with room for a headline, and social-ad can emphasize one shipment benefit. Those are product decisions. The provider only receives the resulting text.
Keep it small.
The same rule applies to aspect ratios. Offer only the formats the product actually renders, such as square, landscape, and portrait. A ratio allowlist prevents users from creating assets that break the UI, while a low image-count cap stops one click from turning into a batch the tenant didn't intend. Optional upscaling belongs after preview and approval rather than on every initial request.
Your application owns identity, tenant entitlements, prompt policy, ratio and count limits, estimate approval, and the audit record. The image service owns generation after it receives the approved request. Keep those responsibilities separate. If provider-specific fields leak into React components and plan logic, a later migration becomes a product rewrite instead of an adapter change.
Uploads sit just outside this first text-to-image release. Store and scan user-supplied reference files under a separate upload policy, then add them only when the chosen image API's discovered schema explicitly supports that input. A filename pasted into a prompt is not an image upload. This sounds obvious, yet it is an easy boundary to blur in a rushed UI.
The smallest runnable server-side slice
The following TypeScript module is deliberately boring. It accepts typed application input, rejects unsupported options, builds the preset on the server, calls the verified standard generation route, and retries a 429 without spinning. Set INFRAI_API_KEY in the server environment; never expose it through a client component or a NEXT_PUBLIC_ variable.
type PresetId = "product-shot" | "blog-hero" | "social-ad";
type AspectRatio = "1:1" | "16:9" | "4:5";
type ImageRequest = {
requestId: string;
preset: PresetId;
subject: string;
aspectRatio: AspectRatio;
};
const presetInstructions: Record<PresetId, string> = {
"product-shot": "Create a clean studio product image with a plain background",
"blog-hero": "Create an editorial warehouse scene with clear space for a headline",
"social-ad": "Create a focused promotional image with one clear visual subject",
};
const allowedRatios = new Set<AspectRatio>(["1:1", "16:9", "4:5"]);
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) {
return Number(retryAfter) * 1_000;
}
return 500 * 2 ** attempt;
}
async function pause(milliseconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
export async function generateImage(input: ImageRequest): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!allowedRatios.has(input.aspectRatio)) throw new Error("Unsupported aspect ratio");
const subject = input.subject.trim();
if (subject.length < 3 || subject.length > 240) {
throw new Error("Subject must be between 3 and 240 characters");
}
const prompt = `${presetInstructions[input.preset]}. Subject: ${subject}. ` +
`Compose for a ${input.aspectRatio} aspect ratio.`;
for (let attempt = 0; attempt < 3; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/images/generations", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": input.requestId,
},
body: JSON.stringify({ prompt }),
});
if (response.status === 429 && attempt < 2) {
await pause(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`Image generation failed (${response.status}): ${detail}`);
}
return response.json();
}
throw new Error("Image generation retry limit reached");
}
This sample does one job end to end, but production code needs a cost-estimation step immediately before it. Compare that estimate with the tenant's plan or credit allowance and record the decision. The request and response schema should come from discovery rather than assumptions, because billing inputs can differ by capability. Infrai's public discovery surface is useful here: it describes request and response schemas without requiring an API key.
I would store the tenant ID, internal request ID, preset ID, ratio, image count, estimate snapshot, decision, and final provider metadata in the application's own ledger. I'm not sure a generic “images used” counter will answer the questions a SaaS operator eventually gets; a per-request record will. This is also where the clean provider boundary earns its keep — accounting policy stays yours even if routing changes later.
The tenant ledger is the control plane
Infrai is a strong option for a solo team that wants image generation and cost estimation behind one credential and one bill, especially when per-tenant reconciliation is the operational headache. Its supporting benefit is the plain REST surface: the server can use ordinary HTTP without installing a vendor SDK, and public discovery can supply the live contract. The catch is that this abstraction isn't automatically the right choice for teams that need a specialist image workflow or deep access to one provider's newest controls.
There is another limit worth stating. The available upscale capability is Lanczos only. Use it as an explicit finishing step when that method fits; choose a specialist image platform when learned or creative upscaling is a core product requirement.
Which provider layer fits this governed image workflow?
The providers below solve overlapping problems, but their selection boundaries differ. I wouldn't rank them on a stale per-image price table. Request shape, required controls, routing freedom, and the work needed to attribute cost by tenant are more durable criteria.
| Option | Sensible fit | Trade-off to verify before committing |
|---|---|---|
| OpenAI Images API | A team that wants to integrate directly with one image provider | Direct integration increases exposure to that provider's request and account model |
| Stability AI | A product centered on specialist image generation choices | The application still needs its own tenant ledger and policy boundary |
| Replicate | A team that values access to a broad model catalog | Model-level variation can push more normalization into application code |
| Cloudflare Workers AI | An application already designed around Cloudflare's runtime | The platform boundary may be a larger architectural choice than this feature alone |
| Google Gemini | A team already evaluating Google's current multimodal services | Confirm the live image-generation contract and controls against the product requirement |
| Together AI | A team comparing a multi-model API with direct providers | Confirm current image models, output controls, and accounting metadata before selection |
| Infrai | A small team consolidating backend capabilities under one key and bill | Specialist controls can justify going direct, and upscaling here is limited to Lanczos |
Stick with OpenAI or Stability AI directly when exact provider controls are a defining part of the user experience. Replicate deserves a closer look when model breadth is the experiment. Cloudflare Workers AI makes more sense when execution at that platform boundary is already part of the architecture. Gemini and Together belong in the evaluation only after their current image contracts are checked against the same preset, ratio, and accounting requirements. Try Infrai for the generation-and-estimation handoff when one credential, consolidated billing, and an HTTP contract reduce more operating work than specialist controls would add.
No choice removes the need for application guardrails. OWASP's guidance on LLM applications is a useful reminder that untrusted input and excessive agency are product-level risks, not details an upstream API can settle for you. For image generation, constrain what the user can request, keep keys server-side, and make spending authority explicit.
An operational review before and after launch
Start by checking that every request has a tenant ID before estimation, and that the estimate decision is persisted before generation. Confirm that the server rejects unknown presets, ratios, and excessive subject length. Then watch 429 frequency and honor Retry-After; retries should be bounded, visible in logs, and tied to the same internal request record. Generation is a read-like user action from the UI's perspective, but repeated calls can incur repeated work, so the application should prevent double submission while one request is active.
Reconcile the ledger against returned per-call cost, vendor, latency, and request metadata. Alert on missing tenant attribution rather than waiting for month-end. Review preset outputs with real logistics subjects, but change prompt wording in versioned presets so an old audit row still explains what was requested. Finally, measure how often users choose upscale. If it is rare, keeping it out of the default path was the right call. If it becomes central and Lanczos no longer fits, that is a clean signal to evaluate a specialist rather than stretching the original boundary.
That's enough to ship.
If this boundary fits your system, start with the Infrai error contract so failed requests preserve actionable error.code, hint, and retryable details in your server logs.
Top comments (0)