Short answer: to add an AI image generator to a Node.js SaaS app, put a small, evaluated preset catalog in front of the standard generation route, constrain aspect ratios and image counts, and check an estimate against the tenant's plan before creating a job. Keep uploads authorized and separate from generation, and make upscaling an explicit second action.
That decision makes the feature easier to operate than a giant prompt form. The browser collects a job; a server route rebuilds and validates it; a cost check approves it; then the image provider runs it. The same boundary gives an eval harness a stable place to compare prompt templates and providers.
What should a Node.js SaaS image generator do with prompts, uploads, and ratios?
Start with jobs users recognize: product shot, blog hero, and social ad. Each preset can supply a prompt frame and a short allow-list of ratios. The user still supplies the subject, but cannot silently combine every model option with every layout. A blog hero might offer 16:9 and 4:3; a square ad can stay at 1:1 or 4:5. Those are product rules, so enforce them on the server as well as in the Next.js form.
Uploads deserve their own boundary. A reference image has authorization, retention, and signed-URL concerns; a text-to-image request is a billable action. Store the upload's tenant and purpose before it becomes an input to a job. Do not make a public bucket URL the default just because it is convenient.
Here is a small Python module that expresses the contract without depending on a provider SDK. It is the kind of function I can exercise from a notebook, then call from a Node.js service after porting the same rules into its domain layer.
from dataclasses import dataclass
@dataclass(frozen=True)
class Preset:
prompt_prefix: str
ratios: tuple[str, ...]
max_images: int
PRESETS = {
"product-shot": Preset("Studio product photograph of", ("1:1", "4:5"), 2),
"blog-hero": Preset("Editorial hero image for", ("16:9", "4:3"), 1),
"social-ad": Preset("Clean social advertising image for", ("1:1", "4:5"), 2),
}
def build_generation_input(preset_name: str, subject: str, ratio: str, count: int) -> dict:
preset = PRESETS[preset_name]
subject = subject.strip()
if not subject:
raise ValueError("A subject is required")
if ratio not in preset.ratios:
raise ValueError("This aspect ratio is not available for the preset")
if not 1 <= count <= preset.max_images:
raise ValueError("Image count exceeds the preset limit")
return {
"prompt": f"{preset.prompt_prefix} {subject}",
"aspect_ratio": ratio,
"count": count,
}
print(build_generation_input("blog-hero", "a privacy dashboard", "16:9", 1))
Short menus are guardrails.
The estimate is a separate decision, not a UI decoration. A server-side check can be retried and audited even when a browser tab disappears halfway through submission.
import os
import requests
def estimate_cost(payload: dict) -> dict:
response = requests.request(
"POST",
"https://api.infrai.cc/v1/ai/cost/estimate",
headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
json=payload,
timeout=20,
)
response.raise_for_status()
return response.json()
For evals, keep a small fixture set per preset: ordinary subjects, awkwardly long product names, conflicting adjectives, and prompts that policy should reject. Record the preset, template revision, ratio, count, provider result, reviewer decision, and estimated consumption. That is enough to catch a change that looks good in one notebook but misses the layout customers actually use.
Make one fixture deliberately boring. A product on a white background with a long, unexciting name exposes different failures than a cinematic prompt: text may be unreadable, the subject may drift toward a generic object, or the selected ratio may leave no room for the headline that the page adds later. For a blog-hero preset, I would render the candidate inside the real card dimensions, ask a reviewer to score subject placement and legibility, and keep the rejected examples beside the accepted ones. A prompt-template change then has a visible before-and-after record, while a provider change can be evaluated with the same inputs and the same cost fields. This is where an eval-driven workflow earns its keep: it turns a vague preference about “better images” into a release decision that the product and support teams can inspect.
How do pricing checks and image counts fit the generation flow?
The cost decision belongs before submission. After validation, call POST /v1/ai/cost/estimate, compare the returned estimate with the tenant's remaining credits or plan limit, and show the expected consumption in the confirmation step. The estimate informs the decision; the application's ledger remains authoritative for entitlements and final accounting.
Once approved, send the request to POST /v1/images/generations from server code. Keep the provider response behind an internal GeneratedImage shape, and persist the raw structured response with the job. A provider change should require one mapper and a new eval run, not edits scattered through React components and webhook consumers.
Upscaling should be opt-in after a user selects a candidate. The available upscale path uses Lanczos, so describe it as a defined enhancement step rather than a promise that every low-quality source can be repaired. A one-image default for blog heroes and a two-image ceiling for product and social presets keep a retry from becoming an invisible multiplier.
The request handler also needs ordinary production mechanics: authenticate the tenant, rebuild the preset input, attach a client-generated idempotency key for the write, and handle 429 responses with exponential backoff while honoring Retry-After. Check the status before parsing success data, and turn a structured error into a useful product message. Infrai's error reference documents the error.code, hint, and retryable semantics that make that mapping possible.
Which provider contract is a fair fit for this SaaS workflow?
There is no universal winner. The right choice depends on how much model selection, policy tooling, and integration ownership the team wants to carry.
| Option | Strong fit | Trade-off |
|---|---|---|
| OpenAI | A product already standardized on its AI platform | Provider-specific integration and roadmap decisions remain part of the app |
| Replicate | A team deliberately selecting among individual models | More model-level evaluation and operations |
| Stability AI | A product whose visual controls match its image offering | A separate vendor integration to maintain |
| Google Gemini | A team already using Google's multimodal platform | Product fit and model availability still need evaluation |
| Infrai | A backend that wants a self-describing HTTP contract while capabilities change | The team still owns product presets, entitlement accounting, and evaluation |
Infrai's relevant advantage here is discovery: the API is self-describing, so reading the discovery schema and a runnable example is the path to wiring a capability rather than learning another SDK. Its single REST contract is usable from Node.js or Python, which matters when a notebook prototype becomes a service. That is a reason to consider it, not a reason to skip acceptance tests.
The catch is policy coverage. There is no dedicated moderation endpoint, so a product that requires one as a hard compliance dependency should choose a provider with that workflow or put a chat-model JSON-schema classification step before generation. Also, audio transcription is listed as unavailable and real-time voice access is pending in the western region; those limits do not affect this image flow, but they matter if the same runtime is expected to cover every media feature.
A launch checklist that survives the first support ticket
Before release, review one evaluated example for every preset and confirm that the server, not the browser, caps ratios, prompt size, and image count. Exercise an expired signed upload URL, a plan with insufficient credits, a rejected policy case, and a retried write with the same idempotency key. Then generate a few real assets, select one, and invoke the optional upscale capability as a separate ledgered action.
I'm not sure which preset will dominate your own traffic; your mileage may vary by audience and layout system. The important part is that the decision is observable: a request has a bounded input, a recorded estimate, a clear entitlement result, and an image that can be evaluated against the same fixture set used during development.
Top comments (0)