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.mjs2. リクエストの契約を確認
例は 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
// 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; }
}
モデル別パラメーター
公開設定のスナップショットであり、現在の稼働状況ではありません。パラメーター、ルート、参照画像の上限は各モデルページを確認してください。
- Gemini 3.1 Flash Lite Image
google/gemini-3.1-flash-lite-image - GPT Image 2
openai/gpt-image-2