AI generation API Next.js: keep submissions behind a server route
·
A Next.js application should send image generation requests from server code. A client component can collect the prompt, but a shared Genace key belongs in the server environment. Use an application route as the boundary where your own authentication, budget rules, and input policy apply. The route below illustrates that boundary and returns promptly with the remote job identifier.
1. Configure the server environment
Put GENACE_API_KEY in your local server environment and deployment secret settings. Do not use a NEXT_PUBLIC_ prefix for this key. Create app/api/create-image/route.ts in your application. Before exposing it to users, add your existing session guard and enforce your own account allowance; the sample route intentionally does not invent an authentication library for your project.
export async function POST(request: Request) {
const key = process.env.GENACE_API_KEY;
if (!key)
return Response.json({ error: 'Configuration missing' }, { status: 503 });
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json({ error: 'Invalid JSON' }, { status: 400 });
}
const prompt =
typeof body === 'object' &&
body !== null &&
'prompt' in body &&
typeof body.prompt === 'string'
? body.prompt.trim()
: '';
if (!prompt || prompt.length > 2000) {
return Response.json(
{ error: 'Prompt must contain 1-2000 characters' },
{ status: 400 }
);
}
try {
const upstream = await fetch(
'https://genace.ai/api/v1/images/generations',
{
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'nano-banana', prompt, n: 1 }),
cache: 'no-store',
signal: AbortSignal.timeout(45000),
}
);
const parsed: unknown = await upstream.json();
const data =
parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {};
if (!upstream.ok) {
const error =
data.error && typeof data.error === 'object'
? (data.error as Record<string, unknown>)
: {};
const code =
typeof error.code === 'string' && /^[A-Z0-9_]{1,80}$/.test(error.code)
? error.code
: 'UPSTREAM_ERROR';
const outcome =
code === 'UPSTREAM_SUBMISSION_FAILED'
? 'rejected'
: code === 'UPSTREAM_SUBMISSION_UNCERTAIN'
? 'uncertain'
: null;
if (
upstream.status === 502 &&
outcome &&
typeof error.job_id === 'string' &&
error.job_id.trim()
) {
return Response.json(
{
error: {
code,
jobId: error.job_id,
submissionOutcome: outcome,
},
},
{ status: 502 }
);
}
return Response.json(
{
error: {
code,
message: 'Generation request failed; investigate before retry',
},
},
{ status: upstream.status }
);
}
if (
upstream.status !== 202 ||
typeof data.job_id !== 'string' ||
!data.job_id.trim() ||
typeof data.state !== 'string' ||
!['queued', 'running', 'succeeded', 'failed', 'canceled'].includes(
data.state
)
) {
return Response.json(
{ error: 'Invalid acceptance response; outcome unknown' },
{ status: 502 }
);
}
return Response.json(
{ jobId: data.job_id, state: data.state },
{ status: 202 }
);
} catch {
return Response.json(
{ error: 'Submission outcome unknown; investigate before retry' },
{ status: 502 }
);
}
}
Route Handlers use Web Request and Response, as documented in the
Next.js reference.
The two recognized 502 envelopes preserve error.jobId and the application-owned
error.submissionOutcome. Genace itself sends error.job_id with a stable error
code; this proxy deliberately renames fields and omits arbitrary upstream messages.
Persist a known ID for reconciliation rather than treating every non-2xx as
unsubmitted. A rejected submission may still have pending credit compensation.
Missing identifiers, unexpected success bodies, unreadable JSON, and transport failures do not establish remote rejection. The route returns a controlled 502; it neither reconstructs a lost ID nor repeats the POST. Unknown acceptance must not cause automatic refunds or attempts to bypass cancellation’s reconciliation boundary. This transport example still needs your authentication, durable storage, request/response byte limits, and actual route integration tests before public use.
The two thousand character cap is an application policy in this example. It is not a documented maximum imposed by Genace. The fixed model and batch size make the first version easier to budget than a proxy accepting arbitrary upstream JSON.
2. Record ownership before returning to the UI
In production, insert a row linking your signed-in application user, the remote job_id, the model, and the accepted credit reservation. Do that before returning the identifier to the browser. Your status route should look up this row and confirm ownership before calling Genace’s job endpoint.
Genace checks ownership against the account associated with its API key. If your application shares one key across all of its users, that upstream check does not separate your own tenants. Your database mapping supplies that missing application boundary. An opaque job ID alone is not a substitute for authorization.
3. Design the status path separately
Have the UI call your status route, which uses GET /api/v1/jobs/{id}?wait=0 with the server key and returns only the fields your UI needs. Keep the route uncached. Show queued and running as pending, succeeded as complete, and failed or canceled as terminal outcomes. Read outputs only after checking the state and allow it to be null.
A serverless request deadline should not hold the entire generation open. The initial route submits once, while later status requests observe the saved job. If the submission connection fails after acceptance, automatic retries can duplicate work because the POST route does not expose request idempotency support.
Common mistakes and verification
Do not return the Authorization header to the client, place the key in serialized component props, or log complete request headers. Test your route with malformed JSON, an empty prompt, and a prompt exceeding your own cap before enabling live submissions. Then verify that one user cannot observe another user’s mapped job.
Confirm that a pending fixture with null outputs renders a waiting state and that a failed fixture shows a useful error instead of an empty image. Keep POST and GET telemetry distinct so repeated status checks do not resemble repeated submissions.
See server-side key boundaries and input validation for the production policies around this route.
Continue with code and integration
Models covered on this page
Where these facts come from
- Editorial keyword hypothesis; search volume not measured
- codebase: src/app/api/v1/images/generations/route.ts
- codebase: src/ai/api/auth.ts
- codebase: src/ai/jobs/http.ts
- https://nextjs.org/docs/app/api-reference/file-conventions/route