Claude
Skills
Sign in
Back

bknd-custom-endpoint

Included with Lifetime
$97 forever

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.

Backend & APIs

What this skill does


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

| Approach | Best For | Complexity |
|----------|----------|------------|
| **Flows + HTTP Triggers** | Business logic, webhooks, multi-step processes | Medium |
| **Plugin Routes** | Simple endpoints, middleware, direct Hono access | Low |

## Approach 1: Flows with HTTP Triggers

### Step 1: Create a Basic Flow Endpoint

```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
# Returns: { "success": true }
```

### Step 2: Create Endpoint with Response

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"
  })
);
```

### Step 3: Handle POST with Request Body

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}'
```

### Step 4: Sync vs Async Mode

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

### Step 5: Multi-Task Flow with Connections

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

```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)

For simpler endpoints, use plugins with `onServerInit`:

### Step 1: Create Plugin with Routes

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

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

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

```typescript
import { createPlugin } from "bknd";
import { Hono } from "hono";

const webhooksPlugin = createPlugin({
  name: "webhooks",

  onServerInit: (server) => {
    const webhooks = new Hono();

Related in Backend & APIs