name: bknd-custom-endpoint description: 在 Bknd 中创建自定义 API 端点时使用。涵盖使用 Flows 的 HTTP 触发器、通过 onServerInit 的插件路由、请求/响应处理、同步与异步模式、访问请求数据以及返回自定义响应。
自定义端点
创建超出 Bknd 自动生成的 CRUD 路由的自定义 API 端点。
先决条件
- 运行中的 Bknd 实例
- 对 HTTP 方法和 REST API 的基本理解
- 熟悉 TypeScript/JavaScript
何时使用 UI 模式
自定义端点需要代码配置。没有 UI 方法可用。
何时使用代码模式
- 为外部服务创建 webhooks
- 构建自定义业务逻辑端点
- 添加结合多个操作的端点
- 与第三方 API 集成
- 创建无需实体 CRUD 的公共端点
两种方法
Bknd 提供两种创建自定义端点的方法:
| 方法 | 最佳适用场景 | 复杂性 |
|---|---|---|
| Flows + HTTP Triggers | 业务逻辑、webhooks、多步骤流程 | 中等 |
| 插件路由 | 简单端点、中间件、直接 Hono 访问 | 低 |
方法 1:使用 Flows 和 HTTP Triggers
步骤 1:创建基本 Flow 端点
import { App, Flow, HttpTrigger, LogTask } from "bknd";
// 定义带有任务的 flow
const helloFlow = new Flow("hello-endpoint", [
new LogTask("log", { message: "Hello endpoint called!" }),
]);
// 附加 HTTP 触发器
helloFlow.setTrigger(
new HttpTrigger({
path: "/api/custom/hello",
method: "GET",
})
);
// 在应用配置中注册
const app = new App({
flows: {
flows: [helloFlow],
},
});
测试:
curl http://localhost:7654/api/custom/hello
# 返回:{ "success": true }
步骤 2:创建带响应的端点
使用 setRespondingTask() 从特定任务返回数据:
import { App, Flow, HttpTrigger, FetchTask } from "bknd";
const fetchTask = new FetchTask("fetch-data", {
url: "https://api.example.com/data",
method: "GET",
});
const apiFlow = new Flow("external-api", [fetchTask]);
// 此任务的输出成为响应
apiFlow.setRespondingTask(fetchTask);
apiFlow.setTrigger(
new HttpTrigger({
path: "/api/custom/external",
method: "GET",
response_type: "json", // "json" | "text" | "html"
})
);
步骤 3:处理 POST 请求体
在任务中访问请求数据:
import { App, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";
// 自定义任务处理请求
class ProcessTask extends Task<typeof ProcessTask.schema> {
override type = "process";
static override schema = s.strictObject({
// 定义预期参数(可使用模板语法)
});
override async execute(input: Request) {
// input 是原始的 Request 对象
const body = await input.json();
return {
received: body,
processed: true,
timestamp: new Date().toISOString(),
};
}
}
const processTask = new ProcessTask("process-input", {});
const postFlow = new Flow("process-data", [processTask]);
postFlow.setRespondingTask(processTask);
postFlow.setTrigger(
new HttpTrigger({
path: "/api/custom/process",
method: "POST",
response_type: "json",
})
);
测试:
curl -X POST http://localhost:7654/api/custom/process \
-H "Content-Type: application/json" \
-d '{"name": "test", "value": 42}'
步骤 4:同步与异步模式
// 同步(默认):等待 flow 完成,返回结果
new HttpTrigger({
path: "/api/custom/sync",
method: "POST",
mode: "sync", // 等待完成
});
// 异步:立即返回,在后台处理
new HttpTrigger({
path: "/api/custom/async",
method: "POST",
mode: "async", // 发射后不管
});
// 返回:{ "success": true } 立即
异步模式适用于:
- 长时间运行的操作
- Webhook 接收器
- 后台任务
步骤 5:多任务 Flow 与连接
import { Flow, HttpTrigger, FetchTask, LogTask, Condition } from "bknd";
const validateTask = new FetchTask("validate", {
url: "https://api.example.com/validate",
method: "POST",
});
const successTask = new LogTask("success", {
message: "Validation passed!",
});
const failTask = new LogTask("fail", {
message: "Validation failed!",
});
const flow = new Flow("validation-flow", [
validateTask,
successTask,
failTask,
]);
// 使用条件连接任务
flow.task(validateTask)
.asInputFor(successTask, Condition.success())
.asInputFor(failTask, Condition.error());
flow.setRespondingTask(successTask);
flow.setTrigger(
new HttpTrigger({
path: "/api/custom/validate",
method: "POST",
})
);
HTTP 触发器选项参考
type HttpTriggerOptions = {
path: string; // URL 路径(必须以 / 开头)
method?: string; // "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
response_type?: string; // "json" | "text" | "html"(默认:"json")
mode?: string; // "sync" | "async"(默认:"sync")
};
方法 2:插件路由(直接 Hono)
对于更简单的端点,使用带 onServerInit 的插件:
步骤 1:创建带路由的插件
import { App, createPlugin } from "bknd";
import type { Hono } from "hono";
const customRoutes = createPlugin({
name: "custom-routes",
onServerInit: (server: Hono) => {
// 简单 GET 端点
server.get("/api/custom/status", (c) => {
return c.json({ status: "ok", timestamp: Date.now() });
});
// POST 端点带请求体
server.post("/api/custom/echo", async (c) => {
const body = await c.req.json();
return c.json({ echo: body });
});
// 带路径参数
server.get("/api/custom/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ userId: id });
});
// 带查询参数
server.get("/api/custom/search", (c) => {
const query = c.req.query("q");
const limit = c.req.query("limit") || "10";
return c.json({ query, limit: parseInt(limit) });
});
},
});
const app = new App({
plugins: [customRoutes],
});
步骤 2:在插件路由中访问应用上下文
import { App, createPlugin } from "bknd";
const apiPlugin = createPlugin({
name: "api-plugin",
onServerInit: (server, { app }) => {
server.get("/api/custom/posts-count", async (c) => {
// 访问数据 API
const em = app.modules.data?.em;
if (!em) {
return c.json({ error: "Data module not available" }, 500);
}
const count = await em.repo("posts").count();
return c.json({ count });
});
server.post("/api/custom/create-post", async (c) => {
const body = await c.req.json();
const em = app.modules.data?.em;
const post = await em.repo("posts").insertOne({
title: body.title,
content: body.content,
});
return c.json({ created: post }, 201);
});
},
});
步骤 3:保护插件路由
import { createPlugin } from "bknd";
const protectedPlugin = createPlugin({
name: "protected-routes",
onServerInit: (server, { app }) => {
// 认证检查中间件
const requireAuth = async (c, next) => {
const auth = app.modules.auth;
const user = await auth?.authenticator?.verify(c.req.raw);
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
c.set("user", user);
return next();
};
// 保护端点
server.get("/api/custom/profile", requireAuth, (c) => {
const user = c.get("user");
return c.json({ user });
});
// 仅管理员端点
server.delete("/api/custom/admin/clear-cache", requireAuth, async (c) => {
const user = c.get("user");
if (user.role !== "admin") {
return c.json({ error: "Forbidden" }, 403);
}
// 清除缓存逻辑...
return c.json({ cleared: true });
});
},
});
步骤 4:插件带子路由器
import { createPlugin } from "bknd";
import { Hono } from "hono";
const webhooksPlugin = createPlugin({
name: "webhooks",
onServerInit: (server) => {
const webhooks = new Hono();
webhooks.post("/stripe", async (c) => {
const payload = await c.req.text();
const sig = c.req.header("stripe-signature");
// 验证和处理 Stripe webhook...
return c.json({ received: true });
});
webhooks.post("/github", async (c) => {
const event = c.req.header("x-github-event");
const body = await c.req.json();
// 处理 GitHub webhook...
return c.json({ received: true });
});
// 挂载子路由器
server.route("/api/webhooks", webhooks);
},
});
访问请求数据
在 Flow 任务中(通过输入)
class MyTask extends Task {
async execute(input: Request) {
// 请求体
const json = await input.json();
const text = await input.text();
const form = await input.formData();
// 请求头
const auth = input.headers.get("authorization");
const contentType = input.headers.get("content-type");
// URL 信息
const url = new URL(input.url);
const searchParams = url.searchParams;
return { processed: true };
}
}
在插件路由中(通过 Hono 上下文)
server.post("/api/custom/upload", async (c) => {
// 请求体
const json = await c.req.json();
const text = await c.req.text();
const form = await c.req.formData();
// 请求头
const auth = c.req.header("authorization");
// 查询参数
const format = c.req.query("format");
// 路径参数(如果路由有 :param)
const id = c.req.param("id");
// 原始请求
const raw = c.req.raw;
return c.json({ received: true });
});
响应模式
在插件路由中
server.get("/api/custom/demo", (c) => {
// JSON 响应
return c.json({ data: "value" });
// 带状态的 JSON
return c.json({ error: "Not found" }, 404);
// 文本响应
return c.text("Hello, World!");
// HTML 响应
return c.html("<h1>Hello</h1>");
// 重定向
return c.redirect("/other-path");
// 自定义响应
return new Response(body, {
status: 200,
headers: { "X-Custom": "header" },
});
});
完整示例:Webhook 接收器
import { App, createPlugin, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";
// 选项 1:使用 Flows
class WebhookTask extends Task<typeof WebhookTask.schema> {
override type = "webhook-processor";
static override schema = s.strictObject({});
override async execute(input: Request) {
const event = input.headers.get("x-webhook-event");
const body = await input.json();
// 根据事件类型处理 webhook
switch (event) {
case "user.created":
console.log("New user:", body.user);
break;
case "order.completed":
console.log("Order completed:", body.order);
break;
}
return { processed: true, event };
}
}
const webhookFlow = new Flow("webhook-handler", [
new WebhookTask("process", {}),
]);
webhookFlow.setRespondingTask(webhookFlow.tasks[0]);
webhookFlow.setTrigger(
new HttpTrigger({
path: "/api/webhooks/external",
method: "POST",
mode: "async", // 立即返回
})
);
// 选项 2:使用插件(更简单)
const webhookPlugin = createPlugin({
name: "webhook-handler",
onServerInit: (server) => {
server.post("/api/webhooks/simple", async (c) => {
const event = c.req.header("x-webhook-event");
const body = await c.req.json();
// 排队进行后台处理
queueMicrotask(async () => {
// 处理 webhook...
});
return c.json({ received: true });
});
},
});
const app = new App({
flows: { flows: [webhookFlow] },
plugins: [webhookPlugin],
});
列出自定义端点
# 列出所有注册路由,包括自定义路由
bknd debug routes
常见陷阱
Flow 无响应
问题: 端点返回 { success: true } 但无数据
修复: 设置响应任务:
// 错误 - 无响应数据
const flow = new Flow("my-flow", [task]);
flow.setTrigger(new HttpTrigger({ path: "/api/test" }));
// 正确 - 任务输出成为响应
const flow = new Flow("my-flow", [task]);
flow.setRespondingTask(task); // 添加这个!
flow.setTrigger(new HttpTrigger({ path: "/api/test" }));
路径冲突
问题: 自定义端点与内置路由冲突
修复: 使用唯一路径前缀:
// 错误 - 与数据 API 冲突
new HttpTrigger({ path: "/api/data/custom" });
// 正确 - 唯一命名空间
new HttpTrigger({ path: "/api/custom/data" });
new HttpTrigger({ path: "/api/v1/custom" });
new HttpTrigger({ path: "/webhooks/stripe" });
响应中缺少 Content-Type
问题: 客户端无法解析响应
修复: 使用 Hono 的响应助手:
// 错误
return new Response(JSON.stringify(data));
// 正确
return c.json(data); // 自动设置 Content-Type
异步模式混淆
问题: 期望从异步端点获取数据
修复: 理解异步立即返回:
// 异步模式 - 立即返回 { success: true }
new HttpTrigger({ path: "/api/job", mode: "async" });
// 对于数据响应,使用同步(默认)
new HttpTrigger({ path: "/api/query", mode: "sync" });
插件未加载
问题: 自定义路由返回 404
修复: 确保插件已注册:
const app = new App({
plugins: [myPlugin], // 必须在这里包含插件
});
应做和不应做
应做:
- 使用 Flows 处理复杂多步操作
- 使用插件处理简单 CRUD 风格端点
- 为 webhooks 和长时间操作设置
mode: "async" - 使用唯一路径前缀(如
/api/custom/、/webhooks/) - 当需要响应数据时调用
setRespondingTask() - 在处理前验证请求体
不应做:
- 与内置路径冲突(如
/api/data/、/api/auth/) - 忘记在应用配置中注册 flows/plugins
- 为长时间运行操作使用同步模式
- 返回无 Content-Type 的原始 Response
- 在没有认证检查的情况下暴露敏感操作
相关技能
- bknd-api-discovery - 探索自动生成的端点
- bknd-webhooks - 配置 webhook 集成
- bknd-protect-endpoint - 保护自定义端点
- bknd-client-setup - 从前端调用自定义端点