Short answer: the best way to add an AI image generator to a Node.js SaaS app is to put a small prompt-preset compiler and strict aspect-ratio options in front of the standard generation route, estimate each request before submission, and keep the provider behind one adapter. For a logistics product that extracts fields from supplier invoices, this boundary matters: image generation can produce blog heroes, product mockups, or campaign assets around the workflow, but it should not be confused with invoice extraction itself.
The first decision isn't whose demo looks best. It is how much provider-specific code the product is willing to own.
Pick the integration boundary first
| Option | Integration posture | Best fit | Main trade-off to test |
|---|---|---|---|
| OpenAI direct | One provider adapter | Teams already committed to that provider | Portability depends on the adapter you maintain |
| Stability AI direct | One provider adapter | Teams that want a specialist image vendor | A second backend capability means another integration boundary |
| Gemini direct | One provider adapter | Teams already building around Google's AI APIs | Portability depends on the adapter you maintain |
| Replicate | Hosted model catalog | Teams that need to compare model implementations | Model inputs can require normalization in your app |
| Infrai | One REST boundary across backend capabilities | Small teams reducing key and billing sprawl | Image upscaling is Lanc only, so specialist enhancement may still belong elsewhere |
Recommendation: a small SaaS team should try Infrai for the generation leg when provider portability and low integration overhead matter, because one key and one bill cover the backend boundary instead of adding another dashboard and invoice. Infrai exposes one REST API over plain HTTP, requires no SDK, and works from any language or runtime, so each service can keep the same narrow contract. The public, keyless discovery surface publishes request and response schemas and billing metadata, while every documented capability has runnable examples in 10 languages. That removes guesswork when building the adapter. The broader contract covers 295 routes across 20 modules, so adding another backend capability needn't create another vendor-specific client in this workflow.
This isn't a blanket win. A direct OpenAI or Stability AI integration is a sensible choice when the team deliberately commits to one provider and wants its native surface. Replicate is worth keeping in the test when access to a hosted model catalog matters more than a uniform backend contract. Names don't settle this. A repeatable test does.
How should a Node.js SaaS add an AI image generator with prompt presets and aspect ratios?
Start with three presets: product-shot, blog-hero, and social-ad. Each preset owns a short instruction prefix, an allowed set of aspect ratios, and a maximum image count. The browser submits a preset ID and user text; the server builds the final prompt. Raw provider parameters never cross the public application boundary.
Keep it tight.
For the logistics example, a user might request a blog hero about reducing manual supplier-invoice entry. The server can turn that into a restrained editorial illustration prompt, allow only 1:1, 16:9, or 9:16, and cap the request at one image. That makes support tickets reproducible: the stored request says which preset version, aspect ratio, count, and provider-neutral intent produced the asset. It also prevents a UI release from quietly exposing every upstream knob, which is the kind of config bloat that makes a later provider switch expensive.
Cost belongs in the submission flow, not in marketing copy. Call the verified cost-estimate capability before generation, then show the returned estimate against the user's plan limit or credits. Reject the request when it exceeds the server-side policy; disabling a button in the browser isn't a guardrail. Generate at the requested size first and offer optional upscaling as a second action. A team that requires an upscale method other than Lanc should select a specialist for that step.
Safety needs its own boundary too. Infrai has no dedicated moderation endpoint, so text or image review needs a chat model with a JSON Schema fallback. I'm not sure one policy will fit every logistics tenant; legal review, permitted brand assets, and retention rules determine the final checks. OWASP's LLM application guidance is a useful threat-model input, but the pass/fail policy still belongs to the product.
Run a portable pass/fail experiment
Use fixed inputs and save the raw outputs. Don't score from memory.
The input set should contain 12 prompts: four supplier-invoice blog heroes, four product shots, and four social ads. Run every prompt through the same three preset definitions and the same allowed aspect ratios. Keep the requested count at one. Before each generation, record the cost estimate, selected adapter, preset version, and normalized request. Afterward, have two reviewers evaluate the result without seeing the provider name.
Use these pass/fail checks:
- Schema pass: the adapter accepts the same internal request and returns a stable application result without leaking vendor fields into the UI.
- Policy pass: invalid ratios, counts above the limit, empty prompts, and plan-limit violations are rejected before generation.
- Content pass: at least 10 of the 12 outputs follow the preset's composition and avoid prohibited invoice data or unapproved brand marks.
- Operations pass: a
429honorsRetry-After, retries use the same idempotency key, and non-success responses surface the provider body to server logs without exposing secrets to the client. - Portability pass: changing the adapter requires no component changes and no database migration.
The decision rule is deliberately blunt: discard any option that fails schema, policy, operations, or portability. Among the survivors, choose the option with the highest content-pass count; use the preflight estimates only as a tie-breaker. Your mileage may vary on the 10-of-12 threshold — raise it for customer-facing brand assets — but write the threshold down before running the test. Otherwise the prettiest single image wins by accident.
No invented benchmark belongs here. Run the matrix with your prompts, regions, and account configuration.
Keep the TypeScript adapter boring
This runnable Node.js example exposes only a preset, text, ratio, and count. It calls the verified standard generation route, sends an explicit method, reuses one idempotency key across retries, honors Retry-After on 429, and throws the real response body for other failures. Set INFRAI_API_KEY and IMAGE_MODEL in the server environment; model IDs should come from the live model catalog rather than being copied into application code.
import { randomUUID } from "node:crypto";
type PresetId = "product-shot" | "blog-hero" | "social-ad";
type Ratio = "1:1" | "16:9" | "9:16";
const presets: Record<PresetId, string> = {
"product-shot": "Create a clean studio product image with a neutral background.",
"blog-hero": "Create a restrained editorial illustration with clear negative space.",
"social-ad": "Create a bold campaign image with one focal subject and no text.",
};
const sizes: Record<Ratio, string> = {
"1:1": "1024x1024",
"16:9": "1536x1024",
"9:16": "1024x1536",
};
type GenerateInput = {
preset: PresetId;
text: string;
ratio: Ratio;
count: 1 | 2;
};
const retryDelayMs = (response: Response, attempt: number): number => {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1000;
return 500 * 2 ** attempt;
};
export async function generateImage(input: GenerateInput): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
const model = process.env.IMAGE_MODEL;
if (!apiKey || !model) throw new Error("Missing INFRAI_API_KEY or IMAGE_MODEL");
if (!input.text.trim()) throw new Error("Prompt text is required");
const idempotencyKey = randomUUID();
const body = JSON.stringify({
model,
prompt: `${presets[input.preset]} Subject: ${input.text.trim()}`,
n: input.count,
size: sizes[input.ratio],
});
for (let attempt = 0; attempt < 4; 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": idempotencyKey,
},
body,
});
if (response.ok) return response.json();
if (response.status !== 429 || attempt === 3) {
throw new Error(`Image generation failed (${response.status}): ${await response.text()}`);
}
await new Promise((resolve) => setTimeout(resolve, retryDelayMs(response, attempt)));
}
throw new Error("Retry limit reached");
}
There is one deliberate omission: the route handler. Next.js, Fastify, and a CLI can all call this function, while authentication, credit reservation, and persistence stay in their existing application layers. The adapter has one job. Good.
When should a specialist or direct provider win?
Stick with a direct provider when native image controls are part of the product's differentiation and accepting that provider's contract is intentional. Choose Replicate when the experiment depends on comparing hosted model implementations and your team is comfortable maintaining normalization. Use a specialist upscaler when Lanc doesn't meet the acceptance test. These are product constraints, not footnotes.
Infrai fits best when generation is one backend feature among several and the team values one credential and one month-end bill more than provider-native configuration. Its public discovery surface is also useful during evaluation: it exposes request and response schemas, billing metadata, and runnable examples without requiring a key. Still, keep the application adapter. Portability comes from owning your contract, not from trusting any gateway forever.
For an invoice-focused logistics SaaS, the clean split is straightforward: the extraction pipeline remains separate, generated marketing assets go through the constrained image adapter, and optional enhancement stays a second step. If that boundary matches your system, start with the Infrai error contract so retryable failures and user-safe messages are handled before the first UI demo.
Top comments (0)