直入正题,Next.js 自带的 API Routes (现已改名为 Route Handlers) 异常难用,例如当你需要编写一个 RESTful API 时,尤为痛苦
这还没完,当你需要数据验证、错误处理、中间件等等功能,又得花费不小的功夫,所以 Next.js 的 API Route 更多是为你的全栈项目编写一些简易的 API 供外部服务,这也可能是为什么 Next.js 宁可设计 Server Action 也不愿为 API Route 提供传统后端的能力。
但不乏有人会想直接使用 Next.js 来编写这些复杂服务,恰好 Hono.js 便提供相关能力。
这篇文章就带你在 Next.js 项目中要如何接入 Hono,以及开发可能遇到的一些坑点并如何优化。
Next.js 中使用 Hono
可以按照 官方的 cli 搭建或者照 next.js 模版 https://github.com/vercel/hono-nextjs 搭建,核心代码 app/api/[[...route]]/route.ts
的写法如下所示。
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
const app = new Hono().basePath('/api')
app.get('/hello', (c) => {
return c.json({
message: 'Hello Next.js!',
})
})
export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const DELETE = handle(app)
从 hono/vercel
导入的 handle
函数会将 app 实例下的所有请求方法导出,例如 GET、POST、PUT、DELETE 等。
一开始的 User CRUD 例子,则可以将其归属到一个文件内下,这里我不建议将后端业务代码放在 app/api 下,因为 Next.js 会自动扫描 app 下的文件夹,这可能会导致不必要的热更新,并且也不易于服务相关代码的拆分。而是在根目录下创建名为 server 的目录,并将有关后端服务的工具库(如 db、redis、zod)放置该目录下以便调用。
至此 next.js 的 api 接口都将由 hono.js 来接管,接下来只需要按照 Hono 的开发形态便可。
数据效验
zod 可以说是 TS 生态下最优的数据验证器,hono 的 @hono/zod-validator
很好用,用起来也十分简单。
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
const paramSchema = z.object({
id: z.string().cuid(),
})
const jsonSchema = z.object({
status: z.boolean(),
})
const app = new Hono().put(
'/users/:id',
zValidator('param', paramSchema),
zValidator('json', jsonSchema),
(c) => {
const { id } = c.req.valid('param')
const { status } = c.req.valid('json')
// 逻辑代码...
return c.json({})
},
)
export default app
支持多种验证目标(param,query,json,header 等),以及 TS 类型完备,这都不用多说。
但此时触发数据验证失败,响应的结果令人不是很满意。下图为访问 /api/todo/xxx
的响应结果(其中 xxx 不为 cuid 格式,因此抛出数据验证异常)
所返回的响应体是完整的 zodError 内容,并且状态码为 400
数据验证失败的状态码通常为 422
因为 zod-validator 默认以 json 格式返回整个 result,代码详见 zod-validator/src/index.ts#L68-L70
这就是坑点之一,返回给客户端的错误信息肯定不会是以这种格式。这里我将其更改为全局错误捕获,做法如下
- 复制 zod-validator 文件并粘贴至
server/api/validator.ts
,并将 return 语句更改为 throw 语句。
if (!result.success) {
- return c.json(result, 400)
}
if (!result.success) {
+ throw result.error
}
- 在
server/api/error.ts
中,编写 handleError 函数用于统一处理异常。(后文前端请求也需要统一处理异常)
import { z } from 'zod'
import type { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export function handleError(err: Error, c: Context): Response {
if (err instanceof z.ZodError) {
const firstError = err.errors[0]
return c.json(
{ code: 422, message: `\`${firstError.path}\`: ${firstError.message}` },
422,
)
}
// handle other error, e.g. ApiError
return c.json(
{
code: 500,
message: '出了点问题, 请稍后再试。',
},
{ status: 500 },
)
}
- 在
server/api/index.ts
,也就是 hono app 对象中绑定错误捕获。
const app = new Hono().basePath('/api')
app.onError(handleError)