Genace

AI generation API Express: validate requests before forwarding them

·

An Express proxy should have a contract of its own. Accepting an arbitrary body and forwarding it with your shared API key gives callers control over model selection, batch size, and other spending inputs. This walkthrough exposes one image operation and returns its job ID. It uses native fetch in a current Node.js runtime and does not rely on an official Express integration.

1. Create a bounded JSON endpoint

Install Express, save the following as server.mjs, and provide GENACE_API_KEY through the environment. Bind the development server to localhost. Public use requires your existing authentication middleware and an allowance check before the route handler.

npm install express
import express from 'express';

const app = express();
app.use(express.json({ limit: '16kb' }));

app.post('/images', async (request, response, next) => {
  const key = process.env.GENACE_API_KEY;
  if (!key) return response.status(503).json({ error: 'Service unavailable' });
  const prompt = typeof request.body?.prompt === 'string'
    ? request.body.prompt.trim() : '';
  if (!prompt || prompt.length > 2000) {
    return response.status(400).json({ error: 'Invalid prompt' });
  }
  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 }),
      signal: AbortSignal.timeout(45000),
    });
    const data = await upstream.json();
    if (!upstream.ok) {
      const error = data?.error;
      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.status(502).json({ error: {
          code, jobId: error.job_id, submissionOutcome: outcome,
        } });
      }
      return response.status(upstream.status).json({
        error: { code, message: 'Generation request failed; investigate before retry' },
      });
    }
    if (upstream.status !== 202 || typeof data?.job_id !== 'string' ||
        !data.job_id.trim() || !['queued', 'running', 'succeeded', 'failed', 'canceled']
          .includes(data.state)) {
      return response.status(502).json({ error: 'Invalid acceptance response; outcome unknown' });
    }
    return response.status(202).json({ jobId: data.job_id, state: data.state });
  } catch (error) {
    next(error);
  }
});

app.use((error, request, response, next) => {
  if (response.headersSent) return next(error);
  const status = error.type === 'entity.too.large' ? 413
    : error.type === 'entity.parse.failed' ? 400 : 502;
  response.status(status).json({ error: 'Request could not be completed' });
});
app.listen(3000, '127.0.0.1');

The two recognized 502 errors retain error.jobId and the local field error.submissionOutcome. These are this proxy’s names; Genace sends error.job_id and its stable error code. Save a known ID for reconciliation before returning in production. Rejected submission is not proof that a refund has settled. Unknown acceptance must not trigger another POST, automatic refund, or an attempt to bypass cancellation’s reconciliation boundary.

The proxy returns bounded code fields and application-owned messages rather than arbitrary upstream text. Invalid acceptance data does not become a success with an undefined ID. HTML, unreadable bodies, and transport failures go through next(error) to the final error handler; investigate acceptance if no ID survived. This sample does not implement authentication, durable storage, an upstream response-size cap, or complete parser integration tests for your deployment.

2. Decide which fields callers control

The body limit and prompt length cap are example application policies, not Genace API limits. The route intentionally fixes the model and image count. If you later expose aspect ratio, use an allowlist matching your chosen model. Avoid passing an extra object from an untrusted caller because it can change upstream behavior outside your application’s visible controls.

Persist the accepted job ID with your application user and the original request before returning success. The sample keeps storage out of the transport example, but a production service needs that mapping for status authorization and recovery after restarts.

3. Add observation as a separate operation

A subsequent authorized handler can request GET /api/v1/jobs/{id}?wait=0. Return a small response containing state, selected outputs, and a suitable error code. Pending jobs can have null outputs. Failed and canceled are terminal states even if the status HTTP request succeeded.

Do not expose a handler that accepts any remote job ID without checking your local mapping. When all users share your server key, the upstream account ownership check does not know which of your users created each job.

Common mistakes and verification

Avoid a generic retry wrapper around the whole route. A submission whose response is lost may still have created remote work. A timeout should leave an uncertain operation for investigation rather than immediately repeating the POST. Status requests can be retried against the saved identifier under a separate policy.

Verify oversized JSON returns 413, malformed JSON returns 400, and invalid prompts never trigger fetch. Verify upstream 402 and 429 responses retain a useful error code without exposing request headers. Finally, use terminal-state fixtures to check that your UI cannot mistake a failed generation for an empty successful image.

See server-side keys and Node.js video handling for the credential and lifecycle details shared by both media types.

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/providers/nano-banana.ts
  • codebase: src/ai/api/errors.ts
  • https://expressjs.com/en/guide/error-handling/