Skip to content
bknd-custom-endpoint logo

Custom Endpoint

bknd-custom-endpoint

Use when creating custom API endpoints in Bknd. Covers HTTP triggers with Flows, plugin routes via onServerInit, request/response handling, sync vs async modes, accessing request data, and returning custom responses.

SKILL.md

Full skill instructions

Custom Endpoint

Create custom API endpoints beyond Bknd's auto-generated CRUD routes.

Prerequisites

  • Running Bknd instance
  • Basic understanding of HTTP methods and REST APIs
  • Familiarity with TypeScript/​JavaScript

When to Use UI Mode

Custom endpoints require code configuration. No UI approach available.

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

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

Approach 1: Flows with HTTP Triggers

Step 1: Create a Basic Flow Endpoint

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:

curl http://localhost:7654/​api/​custom/​hello
# Returns: { "success": true }

Step 2: Create Endpoint with Response

Use setRespondingTask() to return data from a specific task:

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

Access request data in tasks:

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:

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

// 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

Step 5: Multi-Task Flow with Connections

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

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)

For simpler endpoints, use plugins with onServerInit:

Step 1: Create Plugin with Routes

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

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

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

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)

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)

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

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

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

# List all registered routes including custom ones
bknd debug routes

Common Pitfalls

Flow Not Responding

Problem: Endpoint returns { success: true } but no data

Fix: Set responding task:

// 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:

// 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

Problem: Client can't parse response

Fix: Use Hono's response helpers:

// 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:

// 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:

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

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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View