教程
使用 hiapi API 将 AI 图像生成功能添加到 Next.js 应用
路由处理器代理、异步任务轮询、持久化存储,以及零 CLS——约 150 行 TypeScript 即可实现接近生产环境的集成。
将图像生成模型接入 Next.js 14 App Router 应用只是一个小项目,但几乎所有人都会在同样的四个关键难点上犯错:
- API 需要 60–120 秒——对单次客户端请求来说太长,对大多数边缘运行时来说也太长。
- API 密钥不能发送到浏览器,因此需要路由处理器作为代理。
- 结果 URL 有效期很短,因此需要在用户刷新之前完成持久化存储。
- 图片返回时尺寸未知,如果直接渲染,会严重破坏累积布局偏移(Cumulative Layout Shift)。
本教程将带你完成一次接近生产环境的 hiapi 图像 API 集成——任务创建、轮询、持久化上传,以及 CLS 安全的渲染——大约 150 行 TypeScript。所有代码都可以直接放入真实的 app/ 目录。
异步任务约定
hiapi 为所有图像和视频模型提供一个统一端点:POST https://api.hiapi.ai/v1/tasks。它完全异步——提交任务,轮询直到进入终态,然后下载输出。没有长连接 HTTP,没有 100 秒代理超时,也没有 Edge 运行时的坑。

任务有四种可能状态:
pending——已接受但尚未被取走running——正在生成success——output[0].url已就绪(签名、限时有效)fail——error.code和error.message已填充
单张 1K 图像通常需要 30–120 秒完成。其中大部分时间用于模型运行;API 本身不增加可测量的开销。
第 1 步:项目设置
你需要一个 Next.js 14+ App Router 项目和一个环境变量:
# .env.local
HIAPI_API_KEY=sk-... # 切勿暴露给浏览器
将此密钥作为服务端机密添加到你的部署平台(Vercel、Cloudflare Pages、Railway)。不要给它加上 NEXT_PUBLIC_ 前缀。
如果你想要持久化存储(推荐——第 5 步会涵盖),还需添加:
R2_ACCOUNT_ID=...
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET=...
R2_PUBLIC_BASE=https://cdn.your-domain.com # 存储桶上的自定义域名
第 2 步:路由处理器——为什么需要它

路由处理器存在的唯一原因:将 API 密钥保留在服务器端。从 React 组件发起的任何请求都在用户的浏览器中运行,而编译进客户端代码的任何字符串都离被窃取只差一个 DevTools 标签页。
路由处理器还为你提供了一个单一、可审计的位置,用于在请求到达付费端点之前实施速率限制、记录使用情况并验证提示词。
创建 app/api/images/route.ts:
// app/api/images/route.ts
import { NextResponse } from "next/server";
export const runtime = "nodejs"; // 长时间 fetch 需要 Node,而非 Edge
export const maxDuration = 180; // Vercel:延长无服务器超时
const HIAPI = "https://api.hiapi.ai/v1/tasks";
type CreateBody = {
prompt: string;
model?: string;
aspectRatio?: string; // 例如 "16:9"、"3:2"、"1:1"
resolution?: "1K" | "2K" | "4K";
};
export async function POST(req: Request) {
const body = (await req.json()) as CreateBody;
if (!body?.prompt || body.prompt.length > 2000) {
return NextResponse.json({ error: "invalid prompt" }, { status: 400 });
}
const res = await fetch(HIAPI, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.HIAPI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: body.model ?? "Nano-Banana-2",
input: {
prompt: body.prompt,
aspect_ratio: body.aspectRatio ?? "16:9",
resolution: body.resolution ?? "1K",
},
}),
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text }, { status: res.status });
}
const { data } = await res.json();
// data.taskId 是客户端用于轮询的句柄
return NextResponse.json({ taskId: data.taskId });
}
有两个设置很重要,但很容易忘记:
runtime = "nodejs"——在许多环境中,Edge 运行时将单次 fetch 限制在约 25 秒,不适合轮询循环。maxDuration = 180——Vercel 无服务器函数在 Hobby 计划下默认 10 秒,Pro 计划下 60 秒;你必须手动提高此值,否则如果你选择在服务端等待,路由会在轮询中途被终止。
第 3 步:轮询端点(或者:为什么不要在服务端等待)
向客户端展示进度有两种选择:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 服务端等待后一次性返回 | 只需一次客户端请求 | 占用无服务器调用 60–120 秒,容易超时,没有进度反馈 |
服务端返回 taskId,客户端轮询 |
服务端成本低,实时进度 | 需要多构建一个端点 |
在生产环境中,第二种方案明显更好。添加 app/api/images/[id]/route.ts:
// app/api/images/[id]/route.ts
import { NextResponse } from "next/server";
export const runtime = "nodejs";
const HIAPI = "https://api.hiapi.ai/v1/tasks";
export async function GET(
_req: Request,
{ params }: { params: { id: string } },
) {
const res = await fetch(`${HIAPI}/${params.id}`, {
headers: { "Authorization": `Bearer ${process.env.HIAPI_API_KEY}` },
cache: "no-store",
});
if (!res.ok) {
return NextResponse.json({ error: await res.text() }, { status: res.status });
}
const { data } = await res.json();
// status ∈ { pending, running, success, fail }
return NextResponse.json({
status: data.status,
url: data.output?.[0]?.url ?? null,
error: data.error ?? null,
});
}
这个端点就是客户端每 3 秒轮询的对象。每次调用都很快(不到 200ms)且无状态。
第 4 步:客户端组件
一个简单的表单、带加载指示器的加载状态,以及一个结果画布。轮询通过 setInterval 设置,并在进入终态或组件卸载时清除。
// app/generate/page.tsx
"use client";
import { useEffect, useRef, useState } from "react";
type Status = "idle" | "queued" | "running" | "done" | "error";
export default function GeneratePage() {
const [prompt, setPrompt] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [url, setUrl] = useState<string | null>(null);
const [taskId, setTaskId] = useState<string | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => () => {
if (pollRef.current) clearInterval(pollRef.current);
}, []);
async function start() {
setStatus("queued");
setUrl(null);
const r = await fetch("/api/images", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt, aspectRatio: "16:9" }),
});
if (!r.ok) return setStatus("error");
const { taskId } = await r.json();
setTaskId(taskId);
setStatus("running");
pollRef.current = setInterval(async () => {
const p = await fetch(`/api/images/${taskId}`).then(x => x.json());
if (p.status === "success") {
setUrl(p.url);
setStatus("done");
clearInterval(pollRef.current!);
} else if (p.status === "fail") {
setStatus("error");
clearInterval(pollRef.current!);
}
}, 3000);
}
return (
<main className="mx-auto max-w-2xl p-8 space-y-6">
<textarea
value={prompt}
onChange={e => setPrompt(e.target.value)}
rows={3}
className="w-full border rounded p-2"
placeholder="A sunset over a misty pine forest, cinematic, 16:9"
/>
<button
onClick={start}
disabled={!prompt || status === "queued" || status === "running"}
className="px-4 py-2 rounded bg-black text-white disabled:opacity-50"
>
{status === "running" ? "Generating…" : "Generate"}
</button>
{/* 预留画布——为什么这个精确宽高比的 div 很重要,见第 6 步 */}
<div className="relative w-full" style={{ aspectRatio: "16/9" }}>
{status === "running" && (
<div className="absolute inset-0 grid place-items-center bg-neutral-100 rounded">
<span className="text-neutral-500">Waiting on task {taskId?.slice(0, 8)}…</span>
</div>
)}
{url && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
alt={prompt}
className="absolute inset-0 w-full h-full object-cover rounded"
/>
)}
</div>
</main>
);
}
一些生产环境的改进值得现在注意,之后可以逐步加入:
- 指数退避(3 秒 → 5 秒 → 8 秒)可减少慢速生成时的请求数量。
- 空闲超时(例如 240 秒后放弃)可防止对已死亡任务的僵尸轮询。
- 显示部分进度文本——
"running"与"pending"——让用户确信有事情在发生。
第 5 步:持久化结果——输出 URL 会过期
使用任何现代图像 API 时,最常见的错误就是把 output[0].url 存入数据库然后以为万事大吉。那些 URL 是签名的、短命的(通常几分钟到几小时),用户一刷新就会 404。
解决办法是将字节通过你自己的服务器管道传入持久化存储。同一个 app/api/images/[id]/route.ts 处理器就是正确的位置——只在第一次收到 success 响应时执行,在将 URL 返回给客户端之前。
// 在 `data.status === "success"` 检查之后添加到 app/api/images/[id]/route.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({
region: "auto",
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
async function persist(taskId: string, sourceUrl: string): Promise<string> {
const bytes = new Uint8Array(await (await fetch(sourceUrl)).arrayBuffer());
const key = `generations/${taskId}.jpg`;
await s3.send(new PutObjectCommand({
Bucket: process.env.R2_BUCKET!,
Key: key,
Body: bytes,
ContentType: "image/jpeg",
CacheControl: "public, max-age=31536000, immutable",
}));
return `${process.env.R2_PUBLIC_BASE}/${key}`;
}
在状态路由中,一旦状态变为 success,就使用 persist(),然后向客户端返回持久化的 URL,而不是上游的签名 URL。客户端永远不会看到会过期的 URL,明天刷新仍然有效。
如果你持久化到你自己的数据库(Postgres、Supabase 等),请存储持久化 URL——而不是签名 URL。
第 6 步:消除 CLS
累积布局偏移是 AI 图像应用上核心网页指标的无声杀手。图片在页面加载 60 多秒后才到达。如果你在一个未设定尺寸的容器中用 <img> 渲染它,每次结果都会移动其下方的所有内容。
两种可靠的技术:
用 CSS 预留宽高比。这就是上面组件中 aspectRatio: "16/9" 样式所做的。无论图片是否已加载,容器都会保持其空间。
在请求中匹配宽高比。向 hiapi 发送 aspectRatio: "16:9",然后在容器上声明 16/9。两者必须一致,否则会出现黑边,或者更糟——形状与预留的不一致。
对于 Next.js 的 <Image> 组件,同样的原则也适用:传入与请求比例匹配的 width 和 height 属性。除非父元素已经是固定尺寸的盒子,否则不要使用 fill。
import Image from "next/image";
<Image
src={persistedUrl}
alt={prompt}
width={1280}
height={720}
className="rounded"
priority={false}
/>
第 7 步:选择模型并估算成本
hiapi 上的图像模型共享相同的端点结构——只有 model 字段不同。常见选择,默认分辨率下的每张图片价格:
| 模型 | 价格 / 1K 图像 | 优势 |
|---|---|---|
qwen-image-2.0 |
$0.025 | 最便宜;默认 2K;强大的中日韩文字渲染能力 |
gpt-image-2 |
$0.03 | 宽高比可靠;强大的排版能力 |
Nano-Banana |
$0.05 | 非常快(1–2 秒);角色一致性好 |
flux-1.1-pro |
$0.05 | 偏艺术风格的照片级真实感 |
Nano-Banana-2 |
$0.085(1K)/ $0.076(2K)/ $0.114(4K) | 高端质量,原生 4K |
诚实的默认选择取决于任务。对于 UI 模型稿和标题,gpt-image-2 最稳妥。对于原生 4K 的照片级真实感,选 Nano-Banana-2。对于每张图扔掉也不心疼的批量工作,选 qwen-image-2.0 或 Nano-Banana。
对于一个 10 万用户的应用,每个会话每天生成一张图片:
qwen-image-2.0:$2,500/天Nano-Banana-21K:$8,500/天
上述架构不变。只有第 2 步中的 model 字段会变。
第 8 步:生产环境检查清单
上线前:
- 对路由处理器实施速率限制——否则一个 IP 就能在一个下午耗尽你的月度预算。Upstash、Cloudflare Rate Limiting,或简单的 Redis 计数器都可以。
- 在服务端转发前验证提示词长度和内容。hiapi 平台会拒绝某些内容;你不会想为 400 错误支付往返延迟。
- 设置每用户每日配额——按
userId和YYYY-MM-DD存储生成次数,并在当天第 11 次时用友好的提示拒绝。 - 在响应前持久化——永远不要让结果 URL 在未经过持久化存储之前到达客户端。
- 预设置结果容器尺寸以匹配请求的宽高比。跳过这一步就是 CLS 为 0 和为 0.4 的区别。
- 将持久化步骤放到后台以让客户端亚秒级响应:立即返回上游 URL,启动队列任务镜像到 R2 并修补数据库行。
最后一点值得再多说一句:大多数应用不需要它。内联持久化让代码保持简单,额外 1–2 秒发生在 60 秒等待之后,此时用户已经在看加载指示器了。
接下来
你现在拥有一个完整、安全、CLS 安全的 hiapi 图像 API 的 Next.js 集成,大约 150 行 TypeScript。从这里开始,同样的 POST /v1/tasks 模式可扩展到视频模型(Seedance 2.0 用于电影感素材)、图生图流程(只需在 input 内添加 image_url)以及多轮编辑——所有这些都无需改变轮询架构。
如果你想要一个可工作的起点,hiapi 模型页面列出了所有响应此端点的模型,并给出了每个模型的示例输入结构。将其中的任何一个放入上面的 model 字段,同一个组件就能渲染结果。