服务端路由
在 routes/ 中新建一个文件即可添加端点。Void 使用 Hono 进行路由与中间件处理。
预计阅读 5 分钟
路由文件
在 routes/**/*.ts 中创建路由处理器。每个文件会根据它在文件系统中的位置映射到一个 URL 路径:
├── api/
│ └── hello.ts → GET|POST /api/hello
├── users/
│ ├── index.ts → GET|POST /api/users
│ └── [id].ts → GET|PUT|DELETE /api/users/:id
└── search/
└── [...query].ts → GET /api/search/* (catch-all)
动态段使用方括号:[id] 会成为路由参数,[...slug] 会成为 catch-all。以 _ 开头的文件或目录会被忽略。像 (admin) 这样用括号包裹的目录是路由组,它们帮助整理文件,而不改变 URL。
仅限某个环境的路由
在扩展名之前添加 .dev 或 .prod 后缀,可以让一条路由只在某个环境中存在:
├── api/
│ ├── health.ts → 始终存在
│ ├── debug.dev.ts → 仅开发环境
│ └── metrics.prod.ts → 仅生产环境
.dev 路由只在开发时运行;.prod 路由会在构建、部署与生产预览中运行。只被 .dev 路由使用的资源不会在生产环境中开通。
导出的方法
每个文件导出具名的 HTTP 方法常量,用来处理特定方法:
import { defineHandler } from 'void';
export const GET = defineHandler((c) => {
return { message: 'Hello!', timestamp: Date.now() };
});
一个文件可以导出多个方法:
import { defineHandler } from 'void';
import { db } from 'void/db';
import { users } from '@schema';
export const GET = defineHandler(async () => {
return await db.select().from(users);
});
export const POST = defineHandler(async (c) => {
const body = await c.req.json();
await db.insert(users).values({ name: body.name });
return { created: true };
});
使用 D1、PostgreSQL 或 MySQL 的查询参见数据库。
defineHandler
处理器接收一个 Hono Context,其中 c.env 上带有类型化的 Cloudflare 绑定。你可以使用完整的 Hono API(c.json()、c.text()、c.header() 等)。
返回值会被自动转换:
| 返回值 | 响应 |
|---|---|
| 对象 / 数组 / 数字 / 布尔值 | JSON 响应 |
| 字符串 | text/html; charset=utf-8 |
null / undefined |
204 No Content |
Response |
原样返回 |
校验
defineHandler.withValidator() 为 body、query 与 params 添加输入校验。推荐的做法是从 Drizzle schema 派生校验器,这样校验规则能始终与数据库保持一致。
import { defineHandler } from 'void';
import { db } from 'void/db';
import { users, insertUserSchema } from '@schema';
export const POST = defineHandler.withValidator({
body: insertUserSchema,
})(async (c, { body }) => {
const [created] = await db.insert(users).values(body).returning();
return created;
});
手写校验器
对于不对应数据库表的端点,你可以用任何兼容 Standard Schema 的库(Valibot、Zod、ArkType 等)手写校验器:
import * as v from 'valibot';
import { defineHandler } from 'void';
export const POST = defineHandler.withValidator({
body: v.object({
query: v.pipe(v.string(), v.minLength(1)),
limit: v.optional(v.pipe(v.number(), v.maxValue(100)), 10),
}),
})(async (c, { body }) => {
// body 的类型是 { query: string; limit: number }
return search(body.query, body.limit);
});
校验失败时会返回 400,并附带结构化的错误详情:
{
"error": "Validation failed",
"issues": [
{
"slot": "body",
"issues": [{ "message": "Invalid email", "path": "email" }]
}
]
}
中间件
Void 使用 Hono 中间件。defineMiddleware() 为常见的 (c, next) 函数补上类型,而 defineHandler() 也接受原生的 Hono 中间件。
import { defineMiddleware } from 'void';
export const addServerTiming = defineMiddleware(async (c, next) => {
const start = performance.now();
await next();
c.header('Server-Timing', `app;dur=${Math.round(performance.now() - start)}`);
});
任何 hono/* 内置中间件、响应助手或第三方 Hono 生态包都可以在 Void 应用中使用。如果你想在应用中从 hono/* 导入,请先自行安装 hono:
npm install hono
import { cors } from 'hono/cors';
const allowDashboard = cors({ origin: 'https://app.example.com' });
中间件可以全局应用,也可以按路由应用。
全局中间件
全局中间件会在每个请求上运行。把文件放在 middleware/ 中,并用数字前缀控制顺序:
├── 01.logger.ts
└── 02.auth.ts
import { defineMiddleware } from 'void';
export default defineMiddleware(async (c, next) => {
console.log(c.req.method, c.req.path);
await next();
});
用 basicAuth() 可以为预览环境加一个临时的站点门禁。Void 保留的 /__void 端点仍然可访问,以支持部署与开发工具。凭据通过回调读取:
import { defineEnv, string } from 'void/env';
export default defineEnv({
BASIC_AUTH_USERNAME: string(),
BASIC_AUTH_PASSWORD: string(),
});
import { basicAuth } from 'void';
import { env } from 'void/env';
export default basicAuth({
username: () => env.BASIC_AUTH_USERNAME,
password: () => env.BASIC_AUTH_PASSWORD,
realm: 'Preview',
});
开发时把 BASIC_AUTH_USERNAME 与 BASIC_AUTH_PASSWORD 作为本地环境变量设置,部署前设置为生产密钥。
对于健康检查或公开 webhook 这类需要绕过的场景,请在自己的中间件中组合好这段逻辑,再调用 basicAuth()。
用 c.set() 与下游处理器共享数据。为 c.get() 补充类型的方式参见上下文变量。
按路由的中间件
在最终处理器之前把中间件传给 defineHandler:
defineHandler(middleware1, middleware2, handler);
中间件按顺序运行。每个都可以短路(不调用 next() 就直接返回响应),也可以做后处理(await next() 之后修改响应)。defineMiddleware 包装过的函数与原生 Hono MiddlewareHandler 都可以传入。
import { cors } from 'hono/cors';
import { defineHandler } from 'void';
export const GET = defineHandler(cors({ origin: 'https://app.example.com' }), (c) => ({
stats: '...',
}));
在处理器之前最多可以传入 5 个中间件,每个位置都有完整的类型推断。