Scalar + Next.js Route Handlers:用 Zod 4 生成 OpenAPI 描述并挂载 API Reference 的完整实践
【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar
本文基于 Scalar 仓库中 route-handlers.md 这份官方 Recipe 展开,讲解如何在纯 Next.js App Router 项目里,用 Route Handlers + Zod 4 显式描述路径、方法与状态码,用z.toJSONSchema自动生成响应 Schema,再通过@scalar/nextjs-api-reference把渲染后的 API Reference 挂到/scalar路由上。读完你可以独立完成"Zod Schema → OpenAPI 3.1 文档 → Scalar 可视化参考"这条链路,并理解ApiReference适配器在源码层面的工作方式与验证要点。
方案定位:显式描述路线,不依赖路由自动发现
Scalar 的 Next.js 配方共三种,对应不同的路由层:
| 配方 | 路由层 | OpenAPI 来源 |
|---|---|---|
| 本文:Route Handlers + Zod | 原生 Next.js Route Handler | 手写paths,Zod 生成 Schema |
| Hono 配方 | Hono +@hono/zod-openapi | api.getOpenAPI31Document()从注册路由生成 |
| oRPC 配方 | oRPC Fetch Adapter | OpenAPIGenerator从 router 元数据生成 |
Route Handlers 配方的特点是:没有自动路由发现。/openapi.json端点里写什么路径、方法、状态码,Scalar 就展示什么;Zod 只负责"从 Schema 生成 JSON Schema"这一半工作。如果你已经用 Hono 或 oRPC 这类自带路由元数据的框架,也可以直接走它们的生成通道,但本文覆盖的是零额外路由依赖的裸 Route Handlers 场景。
另外注意边界:Scalar 仓库里还有一个实验性包@scalar/nextjs-openapi,它是一个独立的 Route 扫描器(从源码看,packages/nextjs-openapi/src/openapi.ts 通过 TypeScript Compiler API 扫描app/api目录并读取 JSDoc 注释来产出 OpenAPI 3.1 文档),官方标注为 pre-alpha,与本文的渲染器@scalar/nextjs-api-reference是两个不同的包,不要混用。
第一步:安装依赖
在 App Router 应用中安装渲染器与 Zod 4(Recipe 中明确锁定了zod@4,因为z.toJSONSchema是 Zod 4 的转换 API):
npm install @scalar/nextjs-api-reference zod@4第二步:定义一次可复用的 Zod 响应 Schema
Recipe 的关键实践是"Schema 只定义一次":既作为接口数据的运行时校验(parse),又作为 OpenAPI 文档的 Schema 来源。以 route-handlers.md 中的示例为准:
// app/lib/planets.ts import { z } from 'zod' export const planetsSchema = z.array(z.object({ id: z.string(), name: z.string(), moons: z.number().int().nonnegative(), })) export const planets = planetsSchema.parse([ { id: 'earth', name: 'Earth', moons: 1 }, { id: 'mars', name: 'Mars', moons: 2 }, ])要点:
planetsSchema描述"行星目录数组"这一响应结构,字段约束(string/int/ 非负数)会原样体现在生成的 JSON Schema 中;planets是同一 Schema 解析出的实际数据,接口直接返回它,保证"文档与返回值永远一致"——这是比文档和代码分开维护更省心的写法;- 后续
/openapi.json端点会直接导入planetsSchema,实现单一事实来源。
第三步:编写数据端点(Route Handler)
数据端点就是最普通的 Next.js Route Handler,不需要引入任何 Scalar 代码:
// app/api/planets/route.ts import { planets } from '../../lib/planets' export const GET = (): Response => Response.json(planets)注意导入路径:app/api/planets/route.ts位于app/api/planets/目录下,回app两层再进入lib,所以是../../lib/planets。
第四步:在描述端点中生成 OpenAPI 3.1 文档
新建app/openapi.json/route.ts,手动声明路径、方法与状态码,并用z.toJSONSchema(planetsSchema)注入响应 Schema:
// app/openapi.json/route.ts import { z } from 'zod' import { planetsSchema } from '../lib/planets' export const GET = (): Response => Response.json({ openapi: '3.1.0', info: { title: 'Planets API', version: '1.0.0' }, servers: [{ url: '/' }], paths: { '/api/planets': { get: { operationId: 'listPlanets', summary: 'List planets', responses: { '200': { description: 'The planet catalog.', content: { 'application/json': { schema: z.toJSONSchema(planetsSchema) } }, }, }, }, }, }, })逐段说明:
openapi: '3.1.0':Zod 4 的z.toJSONSchema产出的是 JSON Schema 风格(type: ['array', 'null']这类 3.1 语义),与 OpenAPI 3.1 的 Schema 规则兼容,因此 Recipe 固定使用该版本;servers: [{ url: '/' }]:相对服务器 URL。这是本方案的一个实用特性——相对 URL 会跟随当前站点域名,从本地localhost:3000到预览部署环境都能自动对应,无需按环境硬编码绝对地址;paths['/api/planets']:路径必须与第三步 Route Handler 实际暴露的路径一致(app/api/planets/route.ts对应/api/planets)。这里没有任何自动发现,路径写错文档就能正常渲染但请求会 404;operationId/summary:决定 Scalar 侧边栏中的条目命名(示例中显示为List planets);responses['200'].content['application/json'].schema:这是 Zod 发挥作用的位置,z.toJSONSchema(planetsSchema)把数组结构展开为{ type: 'array', items: { ...id/name/moons... } }的 JSON Schema。对于转换规则覆盖不到的边缘 Schema 类型与可选项,建议以 Zod 官方文档的 JSON Schema 章节为准(原 Recipe 中的外链即指向该文档)。
每新增一个端点,就在这个对象里多写一个paths条目——这是"显式描述"路线的核心成本,换来的是对文档结构的完全控制。
第五步:挂载 Scalar 到 /scalar
// app/scalar/route.ts import { ApiReference } from '@scalar/nextjs-api-reference' export const GET = ApiReference({ url: '/openapi.json' })ApiReference返回的不是直接可挂的组件,而是一个符合 Route Handler 约定的函数。仓库中 integrations/nextjs/src/ApiReference.ts 的源码可以看清它的完整行为:
- 合并默认配置(内置
_integration: 'nextjs'标识)与传入配置; - 调用
@scalar/client-side-rendering的renderApiReference,把配置渲染为一段完整 HTML 文档,并注入 custom-theme.ts 中定义的双主题(light/dark 的--scalar-*CSS 变量); - 返回
Content-Type: text/html的 200Response。
对应的单测 integrations/nextjs/test/ApiReference.test.ts 验证了这三点:ApiReference({})返回函数、响应头为text/html、渲染时默认配置被正确合入(_integration: 'nextjs')。也就是说/scalar是一个完全自包含的 HTML 端点,文档数据由浏览器在页面内通过url: '/openapi.json'拉取——这就是为什么openapi.json与scalar必须是两个独立路由。
url以外的其他配置项(主题、认证、布局等)类型定义为HtmlRenderingConfiguration,可参考仓库的 configuration.md 完整传入。
验证与预期结果
启动并验证(与 Recipe 给出的验收标准一致):
npm run dev- 打开
http://localhost:3000/scalar,侧边栏应出现List planets条目; - 打开Test Request面板发送请求,预期返回
200,响应体为 Earth 与 Mars 两条记录; - 直接访问
/openapi.json,应能看到/api/planets路径及由 Zod 生成的数组响应 Schema。
小结与选型建议
- 本配方的分工很清晰:Zod 管"Schema 从哪来",你手写
paths管"文档长什么样",@scalar/nextjs-api-reference管"怎么渲染"。三者解耦,任何一环都可替换; - 相对
serversURL 让同一份文档在本地与预览部署间零配置迁移; - 若你的路由层本身就是 Hono 或 oRPC,改用对应配方(hono.md、orpc.md)可以把
paths也自动化; - 若想尝试自动扫描 Route Handlers 的方案,可关注仓库中的实验包 packages/nextjs-openapi(pre-alpha,默认扫描目录为
app/api,见 packages/nextjs-openapi/src/openapi.ts 的OpenAPIConfig定义),它独立于本文的渲染器包。
【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考