Node.js で画像を生成する

標準 fetch、回数制限付きポーリング、明示的なエラー処理を使い、POST を自動再送しない依存関係不要の例です。

技術確認日:

1. 生成処理をサーバー側に置く

Node.js 22 以降で、配布ファイルを maxapi-image.mjs として保存します。組み込み fetch と ES モジュールを使うため SDK は不要です。MAXAPI_KEY はプロセスの環境変数に設定します。Web アプリではブラウザーから自社の認証済みバックエンドを呼び、そのバックエンドから MaxAPI を呼び出してください。キーを NEXT_PUBLIC_ 変数、HTML、クライアント JavaScript、URL クエリに入れないでください。

node maxapi-image.mjs

2. リクエストの契約を確認

例は google/gemini-3.1-flash-lite-image で 1K の画像タスクを 1 件送信します。runImage を export しているので、ワーカーから独自の payload も渡せます。モデル変更時にはサイズと品質のルールを確認してください。Gemini 用のパラメーターが GPT Image にも使えるとは限りません。route_mode の省略でもキーに固定されたルートは解除されません。

import { runImage, examplePayload } from "./maxapi-image.mjs";
const result = await runImage({
  key: process.env.MAXAPI_KEY,
  payload: { ...examplePayload, prompt: "A red ceramic bowl on a cream background." },
  onTask: task => console.error("Save task:", JSON.stringify(task)),
});

3. 重複生成を避けてタイムアウトを処理

AbortSignal.timeout により、本文の読み取りを含む各 fetch を標準で 30 秒に制限します。確認は別に 5 秒間隔で最大 120 回までで、全体の厳密な 10 分制限ではありません。HTTP エラーや失敗タスクは例外になります。POST の応答喪失は未作成の証拠ではないため、runImage 全体を自動再試行で囲まないでください。onTask で ID と poll_url を保存し、pollURL または MAXAPI_POLL_URL で確認を再開します。

4. API の送信先を固定

確認 URL は設定済み API と同じオリジンの画像タスクパスだけを許可します。認証情報や想定外のパラメーターを含む URL、リダイレクトは拒否します。応答から別ホストへキーを転送しないためです。出力は画像のバイト列ではなく URL です。画像は Bearer キーを付けず別途取得し、ダウンロード方針を検証し、公開ログに画像 URL を残さないでください。

完全なサンプルをダウンロード

maxapi-image.mjs

maxapi-image.mjs
// Node.js 22+. No dependencies. Server-side only; never expose MAXAPI_KEY in a browser.
// Tested against a local fixture, not a paid upstream model.
import { pathToFileURL } from 'node:url';
const ACTIVE = new Set(['submitted', 'queued', 'pending', 'running', 'processing']);
export const examplePayload = {
  model: 'google/gemini-3.1-flash-lite-image',
  prompt: 'A single blue ceramic cup on a plain cream background, soft studio light.',
  resolution: '1K', aspect_ratio: '1:1', n: 1, async: true,
};
export function safeURL(base, path) {
  const origin = new URL(base), url = new URL(path, origin);
  if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password ||
      !(origin.protocol === 'https:' || (origin.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(origin.hostname))) ||
      url.origin !== origin.origin || url.username || url.password || url.hash ||
      !/^\/v1\/images\/(generations|edits)(\/[^/?]+)?$/.test(url.pathname) || url.search) {
    throw new Error('Unsafe API or polling URL; no credentials sent.');
  }
  return url.href;
}
export async function runImage({ key, payload = examplePayload, base = 'https://api.maxapi.dev', pollURL,
  intervalMs = 5000, maxPolls = 120, requestTimeoutMs = 30000, onTask = () => {} }) {
  if (!key) throw new Error('Set MAXAPI_KEY in your server environment.');
  if (!Number.isInteger(maxPolls) || maxPolls < 1 || maxPolls > 720 || !Number.isFinite(intervalMs) || intervalMs < 0 ||
      !Number.isInteger(requestTimeoutMs) || requestTimeoutMs < 1) throw new Error('Invalid polling limits.');
  async function request(path, body) {
    let response;
    try {
      response = await fetch(safeURL(base, path), { method: body ? 'POST' : 'GET', redirect: 'error',
        headers: { Authorization: `Bearer ${key}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
        body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(requestTimeoutMs) });
      let result;
      try { result = await response.json(); } catch { throw new Error(`HTTP ${response.status}: non-JSON response; inspect request history before resubmitting.`); }
      if (!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Invalid JSON response object.');
      if (!response.ok || result.error) {
        const code = /^[a-zA-Z0-9_-]{1,80}$/.test(result.error?.code ?? '') ? result.error.code : 'api_error';
        throw new Error(`HTTP ${response.status}: ${code}; no automatic retry.`);
      }
      return result;
    } catch (error) {
      if (!response) throw new Error('Network, URL, redirect or timeout error; outcome may be unknown. No automatic resubmission.', { cause: error });
      throw error;
    }
  }
  // A saved polling URL resumes the original task without submitting another generation.
  let current = pollURL ? await request(pollURL) : await request('/v1/images/generations', payload);
  let path = pollURL ?? current.poll_url;
  if (path) { path = safeURL(base, path); onTask({ id: current.id, poll_url: path }); }
  for (let count = 0; ; count++) {
    if (current.status === 'succeeded' || (!current.status && Array.isArray(current.data) && current.data.length)) return current;
    if (!ACTIVE.has(current.status)) throw new Error('Task failed, cancelled or returned an unknown state. Check task history; no resubmission.');
    if (!path) throw new Error('Pending task has no polling URL. Check history; do not resubmit automatically.');
    if (count >= maxPolls) throw new Error('Polling limit reached; task was NOT cancelled. Resume using the saved polling URL.');
    await new Promise(resolve => setTimeout(resolve, intervalMs));
    current = await request(path);
  }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    const result = await runImage({ key: process.env.MAXAPI_KEY, pollURL: process.env.MAXAPI_POLL_URL,
      onTask: task => console.error('Save to resume:', JSON.stringify(task)) });
    // Do not print base64, prompts, the API key, or the entire response into shared logs.
    console.log(JSON.stringify({ id: result.id, status: result.status ?? 'succeeded', urls: (result.data ?? []).flatMap(item => item.url ? [item.url] : []) }, null, 2));
  } catch (error) { console.error(error.message); process.exitCode = 1; }
}

モデル別パラメーター

公開設定のスナップショットであり、現在の稼働状況ではありません。パラメーター、ルート、参照画像の上限は各モデルページを確認してください。

参照資料

次のステップ

API キーを管理API リファレンス現在の料金を確認