bknd-custom-endpoint

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Custom Endpoint

自定义API端点

Create custom API endpoints beyond Bknd's auto-generated CRUD routes.
创建超出Bknd自动生成的CRUD路由之外的自定义API端点。

Prerequisites

前置条件

  • Running Bknd instance
  • Basic understanding of HTTP methods and REST APIs
  • Familiarity with TypeScript/JavaScript
  • 运行中的Bknd实例
  • 对HTTP方法和REST API有基本了解
  • 熟悉TypeScript/JavaScript

When to Use UI Mode

何时使用UI模式

Custom endpoints require code configuration. No UI approach available.
自定义端点需要代码配置,暂无UI配置方式。

When to Use Code Mode

何时使用代码模式

  • Creating webhooks for external services
  • Building custom business logic endpoints
  • Adding endpoints that combine multiple operations
  • Integrating with third-party APIs
  • Creating public endpoints without entity CRUD
  • 为外部服务创建webhook
  • 构建包含自定义业务逻辑的端点
  • 添加整合多个操作的端点
  • 与第三方API集成
  • 创建无需实体CRUD的公共端点

Two Approaches

两种实现方式

Bknd offers two ways to create custom endpoints:
ApproachBest ForComplexity
Flows + HTTP TriggersBusiness logic, webhooks, multi-step processesMedium
Plugin RoutesSimple endpoints, middleware, direct Hono accessLow
Bknd提供两种创建自定义端点的方式:
方式适用场景复杂度
Flows + HTTP触发器业务逻辑、webhook、多步骤流程中等
插件路由简单端点、中间件、直接访问Hono

Approach 1: Flows with HTTP Triggers

方式一:结合HTTP触发器的Flows

Step 1: Create a Basic Flow Endpoint

步骤1:创建基础Flow端点

typescript
import { App, Flow, HttpTrigger, LogTask } from "bknd";

// Define a flow with tasks
const helloFlow = new Flow("hello-endpoint", [
  new LogTask("log", { message: "Hello endpoint called!" }),
]);

// Attach HTTP trigger
helloFlow.setTrigger(
  new HttpTrigger({
    path: "/api/custom/hello",
    method: "GET",
  })
);

// Register in app config
const app = new App({
  flows: {
    flows: [helloFlow],
  },
});
Test:
bash
curl http://localhost:7654/api/custom/hello
typescript
import { App, Flow, HttpTrigger, LogTask } from "bknd";

// Define a flow with tasks
const helloFlow = new Flow("hello-endpoint", [
  new LogTask("log", { message: "Hello endpoint called!" }),
]);

// Attach HTTP trigger
helloFlow.setTrigger(
  new HttpTrigger({
    path: "/api/custom/hello",
    method: "GET",
  })
);

// Register in app config
const app = new App({
  flows: {
    flows: [helloFlow],
  },
});
测试:
bash
curl http://localhost:7654/api/custom/hello

Returns: { "success": true }

Returns: { "success": true }

undefined
undefined

Step 2: Create Endpoint with Response

步骤2:创建带响应的端点

Use
setRespondingTask()
to return data from a specific task:
typescript
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]);

// This task's output becomes the response
apiFlow.setRespondingTask(fetchTask);

apiFlow.setTrigger(
  new HttpTrigger({
    path: "/api/custom/external",
    method: "GET",
    response_type: "json",  // "json" | "text" | "html"
  })
);
使用
setRespondingTask()
从指定任务返回数据:
typescript
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]);

// This task's output becomes the response
apiFlow.setRespondingTask(fetchTask);

apiFlow.setTrigger(
  new HttpTrigger({
    path: "/api/custom/external",
    method: "GET",
    response_type: "json",  // "json" | "text" | "html"
  })
);

Step 3: Handle POST with Request Body

步骤3:处理带请求体的POST请求

Access request data in tasks:
typescript
import { App, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";

// Custom task to process request
class ProcessTask extends Task<typeof ProcessTask.schema> {
  override type = "process";

  static override schema = s.strictObject({
    // Define expected params (can use template syntax)
  });

  override async execute(input: Request) {
    // input is the raw Request object
    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",
  })
);
Test:
bash
curl -X POST http://localhost:7654/api/custom/process \
  -H "Content-Type: application/json" \
  -d '{"name": "test", "value": 42}'
在任务中获取请求数据:
typescript
import { App, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";

// Custom task to process request
class ProcessTask extends Task<typeof ProcessTask.schema> {
  override type = "process";

  static override schema = s.strictObject({
    // Define expected params (can use template syntax)
  });

  override async execute(input: Request) {
    // input is the raw Request object
    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",
  })
);
测试:
bash
curl -X POST http://localhost:7654/api/custom/process \
  -H "Content-Type: application/json" \
  -d '{"name": "test", "value": 42}'

Step 4: Sync vs Async Mode

步骤4:同步与异步模式

typescript
// Sync (default): Wait for flow completion, return result
new HttpTrigger({
  path: "/api/custom/sync",
  method: "POST",
  mode: "sync",  // Wait for completion
});

// Async: Return immediately, process in background
new HttpTrigger({
  path: "/api/custom/async",
  method: "POST",
  mode: "async",  // Fire and forget
});
// Returns: { "success": true } immediately
Use async for:
  • Long-running operations
  • Webhook receivers
  • Background jobs
typescript
// Sync (default): Wait for flow completion, return result
new HttpTrigger({
  path: "/api/custom/sync",
  method: "POST",
  mode: "sync",  // Wait for completion
});

// Async: Return immediately, process in background
new HttpTrigger({
  path: "/api/custom/async",
  method: "POST",
  mode: "async",  // Fire and forget
});
// Returns: { "success": true } immediately
异步模式适用场景:
  • 长时间运行的操作
  • Webhook接收器
  • 后台任务

Step 5: Multi-Task Flow with Connections

步骤5:带任务关联的多任务Flow

typescript
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,
]);

// Connect tasks with conditions
flow.task(validateTask)
  .asInputFor(successTask, Condition.success())
  .asInputFor(failTask, Condition.error());

flow.setRespondingTask(successTask);

flow.setTrigger(
  new HttpTrigger({
    path: "/api/custom/validate",
    method: "POST",
  })
);
typescript
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,
]);

// Connect tasks with conditions
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 Trigger Options Reference

HTTP触发器选项参考

typescript
type HttpTriggerOptions = {
  path: string;           // URL path (must start with /)
  method?: string;        // "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
  response_type?: string; // "json" | "text" | "html" (default: "json")
  mode?: string;          // "sync" | "async" (default: "sync")
};
typescript
type HttpTriggerOptions = {
  path: string;           // URL path (must start with /)
  method?: string;        // "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
  response_type?: string; // "json" | "text" | "html" (default: "json")
  mode?: string;          // "sync" | "async" (default: "sync")
};

Approach 2: Plugin Routes (Direct Hono)

方式二:插件路由(直接使用Hono)

For simpler endpoints, use plugins with
onServerInit
:
对于简单端点,可使用带
onServerInit
的插件:

Step 1: Create Plugin with Routes

步骤1:创建带路由的插件

typescript
import { App, createPlugin } from "bknd";
import type { Hono } from "hono";

const customRoutes = createPlugin({
  name: "custom-routes",

  onServerInit: (server: Hono) => {
    // Simple GET endpoint
    server.get("/api/custom/status", (c) => {
      return c.json({ status: "ok", timestamp: Date.now() });
    });

    // POST endpoint with body
    server.post("/api/custom/echo", async (c) => {
      const body = await c.req.json();
      return c.json({ echo: body });
    });

    // With path parameters
    server.get("/api/custom/users/:id", (c) => {
      const id = c.req.param("id");
      return c.json({ userId: id });
    });

    // With query parameters
    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],
});
typescript
import { App, createPlugin } from "bknd";
import type { Hono } from "hono";

const customRoutes = createPlugin({
  name: "custom-routes",

  onServerInit: (server: Hono) => {
    // Simple GET endpoint
    server.get("/api/custom/status", (c) => {
      return c.json({ status: "ok", timestamp: Date.now() });
    });

    // POST endpoint with body
    server.post("/api/custom/echo", async (c) => {
      const body = await c.req.json();
      return c.json({ echo: body });
    });

    // With path parameters
    server.get("/api/custom/users/:id", (c) => {
      const id = c.req.param("id");
      return c.json({ userId: id });
    });

    // With query parameters
    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],
});

Step 2: Access App Context in Plugin Routes

步骤2:在插件路由中访问应用上下文

typescript
import { App, createPlugin } from "bknd";

const apiPlugin = createPlugin({
  name: "api-plugin",

  onServerInit: (server, { app }) => {
    server.get("/api/custom/posts-count", async (c) => {
      // Access data 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);
    });
  },
});
typescript
import { App, createPlugin } from "bknd";

const apiPlugin = createPlugin({
  name: "api-plugin",

  onServerInit: (server, { app }) => {
    server.get("/api/custom/posts-count", async (c) => {
      // Access data 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);
    });
  },
});

Step 3: Protected Plugin Routes

步骤3:受保护的插件路由

typescript
import { createPlugin } from "bknd";

const protectedPlugin = createPlugin({
  name: "protected-routes",

  onServerInit: (server, { app }) => {
    // Middleware for auth check
    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();
    };

    // Protected endpoint
    server.get("/api/custom/profile", requireAuth, (c) => {
      const user = c.get("user");
      return c.json({ user });
    });

    // Admin-only endpoint
    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);
      }

      // Clear cache logic...
      return c.json({ cleared: true });
    });
  },
});
typescript
import { createPlugin } from "bknd";

const protectedPlugin = createPlugin({
  name: "protected-routes",

  onServerInit: (server, { app }) => {
    // Middleware for auth check
    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();
    };

    // Protected endpoint
    server.get("/api/custom/profile", requireAuth, (c) => {
      const user = c.get("user");
      return c.json({ user });
    });

    // Admin-only endpoint
    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);
      }

      // Clear cache logic...
      return c.json({ cleared: true });
    });
  },
});

Step 4: Plugin with Sub-Router

步骤4:带子路由的插件

typescript
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");
      // Verify and process 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();
      // Process GitHub webhook...
      return c.json({ received: true });
    });

    // Mount sub-router
    server.route("/api/webhooks", webhooks);
  },
});
typescript
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");
      // Verify and process 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();
      // Process GitHub webhook...
      return c.json({ received: true });
    });

    // Mount sub-router
    server.route("/api/webhooks", webhooks);
  },
});

Accessing Request Data

获取请求数据

In Flow Tasks (via input)

在Flow任务中(通过input)

typescript
class MyTask extends Task {
  async execute(input: Request) {
    // Body
    const json = await input.json();
    const text = await input.text();
    const form = await input.formData();

    // Headers
    const auth = input.headers.get("authorization");
    const contentType = input.headers.get("content-type");

    // URL info
    const url = new URL(input.url);
    const searchParams = url.searchParams;

    return { processed: true };
  }
}
typescript
class MyTask extends Task {
  async execute(input: Request) {
    // Body
    const json = await input.json();
    const text = await input.text();
    const form = await input.formData();

    // Headers
    const auth = input.headers.get("authorization");
    const contentType = input.headers.get("content-type");

    // URL info
    const url = new URL(input.url);
    const searchParams = url.searchParams;

    return { processed: true };
  }
}

In Plugin Routes (via Hono context)

在插件路由中(通过Hono上下文)

typescript
server.post("/api/custom/upload", async (c) => {
  // Body
  const json = await c.req.json();
  const text = await c.req.text();
  const form = await c.req.formData();

  // Headers
  const auth = c.req.header("authorization");

  // Query params
  const format = c.req.query("format");

  // Path params (if route has :param)
  const id = c.req.param("id");

  // Raw request
  const raw = c.req.raw;

  return c.json({ received: true });
});
typescript
server.post("/api/custom/upload", async (c) => {
  // Body
  const json = await c.req.json();
  const text = await c.req.text();
  const form = await c.req.formData();

  // Headers
  const auth = c.req.header("authorization");

  // Query params
  const format = c.req.query("format");

  // Path params (if route has :param)
  const id = c.req.param("id");

  // Raw request
  const raw = c.req.raw;

  return c.json({ received: true });
});

Response Patterns

响应模式

In Plugin Routes

在插件路由中

typescript
server.get("/api/custom/demo", (c) => {
  // JSON response
  return c.json({ data: "value" });

  // JSON with status
  return c.json({ error: "Not found" }, 404);

  // Text response
  return c.text("Hello, World!");

  // HTML response
  return c.html("<h1>Hello</h1>");

  // Redirect
  return c.redirect("/other-path");

  // Custom response
  return new Response(body, {
    status: 200,
    headers: { "X-Custom": "header" },
  });
});
typescript
server.get("/api/custom/demo", (c) => {
  // JSON response
  return c.json({ data: "value" });

  // JSON with status
  return c.json({ error: "Not found" }, 404);

  // Text response
  return c.text("Hello, World!");

  // HTML response
  return c.html("<h1>Hello</h1>");

  // Redirect
  return c.redirect("/other-path");

  // Custom response
  return new Response(body, {
    status: 200,
    headers: { "X-Custom": "header" },
  });
});

Complete Example: Webhook Receiver

完整示例:Webhook接收器

typescript
import { App, createPlugin, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";

// Option 1: Using 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();

    // Process webhook based on event type
    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",  // Return immediately
  })
);

// Option 2: Using Plugin (simpler)
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();

      // Queue for background processing
      queueMicrotask(async () => {
        // Process webhook...
      });

      return c.json({ received: true });
    });
  },
});

const app = new App({
  flows: { flows: [webhookFlow] },
  plugins: [webhookPlugin],
});
typescript
import { App, createPlugin, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";

// Option 1: Using 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();

    // Process webhook based on event type
    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",  // Return immediately
  })
);

// Option 2: Using Plugin (simpler)
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();

      // Queue for background processing
      queueMicrotask(async () => {
        // Process webhook...
      });

      return c.json({ received: true });
    });
  },
});

const app = new App({
  flows: { flows: [webhookFlow] },
  plugins: [webhookPlugin],
});

Listing Custom Endpoints

列出自定义端点

bash
undefined
bash
undefined

List all registered routes including custom ones

List all registered routes including custom ones

bknd debug routes
undefined
bknd debug routes
undefined

Common Pitfalls

常见陷阱

Flow Not Responding

Flow无响应返回

Problem: Endpoint returns
{ success: true }
but no data
Fix: Set responding task:
typescript
// WRONG - no response data
const flow = new Flow("my-flow", [task]);
flow.setTrigger(new HttpTrigger({ path: "/api/test" }));

// CORRECT - task output becomes response
const flow = new Flow("my-flow", [task]);
flow.setRespondingTask(task);  // Add this!
flow.setTrigger(new HttpTrigger({ path: "/api/test" }));
问题: 端点返回
{ success: true }
但无数据
解决方法: 设置响应任务:
typescript
// WRONG - no response data
const flow = new Flow("my-flow", [task]);
flow.setTrigger(new HttpTrigger({ path: "/api/test" }));

// CORRECT - task output becomes response
const flow = new Flow("my-flow", [task]);
flow.setRespondingTask(task);  // Add this!
flow.setTrigger(new HttpTrigger({ path: "/api/test" }));

Path Conflicts

路径冲突

Problem: Custom endpoint conflicts with built-in routes
Fix: Use unique path prefixes:
typescript
// WRONG - conflicts with data API
new HttpTrigger({ path: "/api/data/custom" });

// CORRECT - unique namespace
new HttpTrigger({ path: "/api/custom/data" });
new HttpTrigger({ path: "/api/v1/custom" });
new HttpTrigger({ path: "/webhooks/stripe" });
问题: 自定义端点与内置路由冲突
解决方法: 使用唯一的路径前缀:
typescript
// WRONG - conflicts with data API
new HttpTrigger({ path: "/api/data/custom" });

// CORRECT - unique namespace
new HttpTrigger({ path: "/api/custom/data" });
new HttpTrigger({ path: "/api/v1/custom" });
new HttpTrigger({ path: "/webhooks/stripe" });

Missing Content-Type in Response

响应中缺少Content-Type

Problem: Client can't parse response
Fix: Use Hono's response helpers:
typescript
// WRONG
return new Response(JSON.stringify(data));

// CORRECT
return c.json(data);  // Sets Content-Type automatically
问题: 客户端无法解析响应
解决方法: 使用Hono的响应辅助方法:
typescript
// WRONG
return new Response(JSON.stringify(data));

// CORRECT
return c.json(data);  // Sets Content-Type automatically

Async Mode Confusion

异步模式误解

Problem: Expecting data from async endpoint
Fix: Understand async returns immediately:
typescript
// Async mode - returns { success: true } immediately
new HttpTrigger({ path: "/api/job", mode: "async" });

// For data responses, use sync (default)
new HttpTrigger({ path: "/api/query", mode: "sync" });
问题: 期望从异步端点获取数据
解决方法: 理解异步模式会立即返回:
typescript
// Async mode - returns { success: true } immediately
new HttpTrigger({ path: "/api/job", mode: "async" });

// For data responses, use sync (default)
new HttpTrigger({ path: "/api/query", mode: "sync" });

Plugin Not Loading

插件未加载

Problem: Custom routes return 404
Fix: Ensure plugin is registered:
typescript
const app = new App({
  plugins: [myPlugin],  // Must include plugin here
});
问题: 自定义路由返回404
解决方法: 确保已注册插件:
typescript
const app = new App({
  plugins: [myPlugin],  // Must include plugin here
});

DOs and DON'Ts

注意事项

DO:
  • Use Flows for complex multi-step operations
  • Use plugins for simple CRUD-style endpoints
  • Set
    mode: "async"
    for webhooks and long operations
  • Use unique path prefixes (
    /api/custom/
    ,
    /webhooks/
    )
  • Call
    setRespondingTask()
    when you need response data
  • Validate request bodies before processing
DON'T:
  • Conflict with built-in paths (
    /api/data/
    ,
    /api/auth/
    )
  • Forget to register flows/plugins in App config
  • Use sync mode for long-running operations
  • Return raw Response without Content-Type
  • Expose sensitive operations without auth checks
建议:
  • 复杂的多步骤操作使用Flows
  • 简单CRUD风格端点使用插件
  • Webhook和长时间操作设置
    mode: "async"
  • 使用唯一路径前缀(如
    /api/custom/
    /webhooks/
  • 需要返回响应数据时调用
    setRespondingTask()
  • 处理前验证请求体
禁止:
  • 与内置路径(如
    /api/data/
    /api/auth/
    )冲突
  • 忘记在App配置中注册Flow/插件
  • 长时间运行的操作使用同步模式
  • 返回未设置Content-Type的原始Response
  • 未做权限校验就暴露敏感操作

Related Skills

相关技能

  • bknd-api-discovery - Explore auto-generated endpoints
  • bknd-webhooks - Configure webhook integrations
  • bknd-protect-endpoint - Secure custom endpoints
  • bknd-client-setup - Call custom endpoints from frontend
  • bknd-api-discovery - 探索自动生成的端点
  • bknd-webhooks - 配置webhook集成
  • bknd-protect-endpoint - 保护自定义端点
  • bknd-client-setup - 从前端调用自定义端点