Bknd自定义端点Skill bknd-custom-endpoint

这个技能用于在 Bknd 平台中创建自定义 API 端点,支持通过 Flows 和 HTTP Triggers 或 Plugin Routes 实现复杂的业务逻辑和外部服务集成。关键词包括 Bknd、自定义端点、API 开发、HTTP 触发器、插件路由、后端开发、webhook 处理、请求响应处理。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

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 - 从前端调用自定义端点