|
|
|
@@ -0,0 +1,277 @@
|
|
|
|
|
---
|
|
|
|
|
title: "Web Adapter"
|
|
|
|
|
description: "Bring bknd to any web framework or runtime"
|
|
|
|
|
tags: ["documentation"]
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## What is the Web Adapter?
|
|
|
|
|
|
|
|
|
|
The web adapter (`bknd/adapter/web`) is a framework/runtime agnostic adapter which can integrate `bknd` into **any** web framework or runtime — even ones without a dedicated adapter.
|
|
|
|
|
|
|
|
|
|
**Use the web adapter when:**
|
|
|
|
|
- Your framework doesn't have a dedicated bknd adapter
|
|
|
|
|
- You want full control over your server setup
|
|
|
|
|
- You're building a custom server or edge function
|
|
|
|
|
|
|
|
|
|
**Use a platform-specific adapter when:**
|
|
|
|
|
- You're using a supported framework (Next.js, SvelteKit, Nuxt, Astro, etc.)
|
|
|
|
|
- You prefer opinionated, zero-config setup
|
|
|
|
|
|
|
|
|
|
## Installation
|
|
|
|
|
|
|
|
|
|
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
|
|
|
|
|
|
|
|
|
|
```bash tab="npm"
|
|
|
|
|
npm install bknd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash tab="pnpm"
|
|
|
|
|
pnpm install bknd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash tab="yarn"
|
|
|
|
|
yarn add bknd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash tab="bun"
|
|
|
|
|
bun add bknd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
## Basic Setup
|
|
|
|
|
|
|
|
|
|
Start by creating a config file:
|
|
|
|
|
|
|
|
|
|
```typescript title="bknd.config.ts"
|
|
|
|
|
import type { Config } from "bknd/adapter/web";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
connection: {
|
|
|
|
|
url: "file:data.db",
|
|
|
|
|
},
|
|
|
|
|
} satisfies Config<"api">;
|
|
|
|
|
```
|
|
|
|
|
<Callout type="info" title="The Config type helper">
|
|
|
|
|
The `Config` type is a helper type which maps the correct config type based on mode.
|
|
|
|
|
|
|
|
|
|
Use `Config<"api">` when you are using React-based framework otherwise use `Config<"standalone">` to serve both the Admin UI and bknd api using the same handler
|
|
|
|
|
</Callout>
|
|
|
|
|
|
|
|
|
|
### Helper Singleton
|
|
|
|
|
```typescript title="lib/bknd.ts"
|
|
|
|
|
import { createBknd } from "bknd/adapter/web";
|
|
|
|
|
import config from "../bknd.config";
|
|
|
|
|
|
|
|
|
|
export const bknd = createBknd({ mode: "standalone", options: config }, process.env);
|
|
|
|
|
// ^ use "api" if not using React based framework
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`createBknd` returns three methods:
|
|
|
|
|
|
|
|
|
|
| Method | Returns | Description |
|
|
|
|
|
|--------|---------|-------------|
|
|
|
|
|
| `getApp(env: Env)` | `Promise<App>` | The built bknd app instance |
|
|
|
|
|
| `getApi({ headers, verify })` | `Promise<Api>` | Convenience wrapper around `app.getApi()` |
|
|
|
|
|
| `serve()` | `(req: Request) => Promise<Response>` | A fetch handler for your server |
|
|
|
|
|
|
|
|
|
|
## Choosing an Admin UI Path
|
|
|
|
|
|
|
|
|
|
The web adapter supports two approaches to serving the admin UI:
|
|
|
|
|
|
|
|
|
|
| Path | Best For | How |
|
|
|
|
|
|------|----------|-----|
|
|
|
|
|
| **Client-side** (default) | React frameworks: Next.js, Astro, React Router, Tanstack Start, Waku | Import and render `<Admin />` in your own React route |
|
|
|
|
|
| **Server-side** | Non-React frameworks: SvelteKit, Nuxt, , Bun, Node, Deno | The server serves the Admin UI assets and API |
|
|
|
|
|
|
|
|
|
|
## Path 1: Client-Side Admin (Default)
|
|
|
|
|
|
|
|
|
|
For React-capable frameworks, render the admin UI directly in a route:
|
|
|
|
|
|
|
|
|
|
```tsx title="app/admin/[[...admin]]/page.tsx"
|
|
|
|
|
import { Admin } from "bknd/ui";
|
|
|
|
|
import "bknd/dist/styles.css";
|
|
|
|
|
import { bknd } from "@/bknd";
|
|
|
|
|
|
|
|
|
|
export default async function AdminPage() {
|
|
|
|
|
const api = await bknd.getApi({ verify: true });
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Admin
|
|
|
|
|
withProvider={{ user: api.getUser() }}
|
|
|
|
|
config={{ basepath: "/admin" }}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
No server-side admin configuration needed. The admin UI runs entirely in the browser.
|
|
|
|
|
|
|
|
|
|
## Path 2: Server-Side Admin
|
|
|
|
|
|
|
|
|
|
For non-React frameworks or standalone servers, enable the admin controller:
|
|
|
|
|
|
|
|
|
|
```typescript title="src/bknd.ts"
|
|
|
|
|
import { createBknd } from "bknd/adapter/web";
|
|
|
|
|
import config from "../bknd.config";
|
|
|
|
|
|
|
|
|
|
export const bknd = createBknd({ mode: "standalone", options: config }, env);
|
|
|
|
|
// ^ use "api" if not using React based framework
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You'll also need to serve the admin's static assets (JS, CSS). Choose one of three strategies:
|
|
|
|
|
|
|
|
|
|
### Strategy 1: serveStatic Middleware
|
|
|
|
|
|
|
|
|
|
**Best for:** Bun, Node, standalone servers with filesystem access
|
|
|
|
|
|
|
|
|
|
Use Hono's platform-specific `serveStatic` in `bknd.config.ts` to serve assets from `node_modules/bknd/dist/static/`:
|
|
|
|
|
|
|
|
|
|
```typescript title="bknd.config.ts"
|
|
|
|
|
import type { Config } from "bknd/adapter/web";
|
|
|
|
|
import { serveStatic } from "hono/bun"; // or "@hono/node-server/serve-static"
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
connection: {
|
|
|
|
|
url: "file:data.db",
|
|
|
|
|
serveStatic: serveStatic({ root: "./node_modules/bknd/dist/static" }),
|
|
|
|
|
adminOptions: {
|
|
|
|
|
adminBasepath: "/admin",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} satisfies Config<"standalone">;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Strategy 2: copy-assets Postinstall
|
|
|
|
|
|
|
|
|
|
**Best for:** SvelteKit, Nuxt, any framework with a static directory
|
|
|
|
|
|
|
|
|
|
Copy assets at install time and let your framework serve them:
|
|
|
|
|
|
|
|
|
|
```json title="package.json"
|
|
|
|
|
{
|
|
|
|
|
"scripts": {
|
|
|
|
|
"postinstall": "bknd copy-assets --out static"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Per-framework output paths:
|
|
|
|
|
|
|
|
|
|
| Framework | `--out` flag |
|
|
|
|
|
|-----------|-------------|
|
|
|
|
|
| SvelteKit | `--out static` |
|
|
|
|
|
| Nuxt | `--out public` |
|
|
|
|
|
| Next.js | `--out public` |
|
|
|
|
|
| Astro | `--out public` |
|
|
|
|
|
| Qwik | `--out public` |
|
|
|
|
|
|
|
|
|
|
### Strategy 3: serveStaticViaImport
|
|
|
|
|
|
|
|
|
|
**Best for:** Edge/serverless runtimes (Deno Deploy, Cloudflare Workers)
|
|
|
|
|
|
|
|
|
|
For environments without filesystem access:
|
|
|
|
|
|
|
|
|
|
```typescript title="bknd.config.ts"
|
|
|
|
|
import type { Config } from "bknd/adapter/web";
|
|
|
|
|
import { serveStaticViaImport } from "bknd/adapter";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
connection: {
|
|
|
|
|
url: "file:data.db",
|
|
|
|
|
serveStatic: serveStaticViaImport(),
|
|
|
|
|
adminOptions: {
|
|
|
|
|
adminBasepath: "/admin",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} satisfies Config<"standalone">;
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Serving Requests
|
|
|
|
|
|
|
|
|
|
Exmaple use `bknd.serve()` as a fetch handler in your server:
|
|
|
|
|
|
|
|
|
|
```typescript title="server.ts"
|
|
|
|
|
import { bknd } from "./bknd";
|
|
|
|
|
|
|
|
|
|
// Bun
|
|
|
|
|
Bun.serve({ fetch: bknd.serve(), port: 3000 });
|
|
|
|
|
|
|
|
|
|
// Node (with @hono/node-server)
|
|
|
|
|
import { serve } from "@hono/node-server";
|
|
|
|
|
serve({ fetch: bknd.serve(), port: 3000 });
|
|
|
|
|
|
|
|
|
|
// Next.js
|
|
|
|
|
const handler = bknd.serve(); // here you'll use "api" for mode when setting up `bknd` instance
|
|
|
|
|
|
|
|
|
|
export const GET = handler;
|
|
|
|
|
export const POST = handler;
|
|
|
|
|
export const PUT = handler;
|
|
|
|
|
export const PATCH = handler;
|
|
|
|
|
export const DELETE = handler;
|
|
|
|
|
|
|
|
|
|
// Qwik City (Middleware)
|
|
|
|
|
export const onRequest: RequestHandler = async ({
|
|
|
|
|
url,
|
|
|
|
|
next,
|
|
|
|
|
status,
|
|
|
|
|
headers,
|
|
|
|
|
request,
|
|
|
|
|
redirect,
|
|
|
|
|
getWritableStream,
|
|
|
|
|
}) => {
|
|
|
|
|
const pathname = url.pathname;
|
|
|
|
|
|
|
|
|
|
if (pathname.startsWith("/api") || pathname !== "/") {
|
|
|
|
|
const response = await handler(request);
|
|
|
|
|
|
|
|
|
|
// skips unknown paths
|
|
|
|
|
if (response.status === 404) {
|
|
|
|
|
await next();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// adds the set-cookie header
|
|
|
|
|
response.headers.forEach((value, key) => {
|
|
|
|
|
headers.set(key, value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// for redirect
|
|
|
|
|
if (response.status >= 300 && response.status < 400) {
|
|
|
|
|
const location = response.headers.get("location");
|
|
|
|
|
if (location) {
|
|
|
|
|
throw redirect(response.status as any, location);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stream back the body
|
|
|
|
|
status(response.status);
|
|
|
|
|
if (response.body) {
|
|
|
|
|
await response.body?.pipeTo(getWritableStream());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Or use `bknd.getApp()` to integrate with an existing Hono or other router setup.
|
|
|
|
|
|
|
|
|
|
## Reference: Config\<T\>
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
export type AdapterModeWithOptions<Env = Record<string, string | undefined>> =
|
|
|
|
|
| {
|
|
|
|
|
mode: "standalone";
|
|
|
|
|
options: RuntimeBkndConfig<Env>;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
mode: "api";
|
|
|
|
|
options: FrameworkBkndConfig<Env>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type Config<T extends AdapterModeWithOptions["mode"]> = Extract<
|
|
|
|
|
Parameters<typeof createBknd>[0],
|
|
|
|
|
{ mode: T }
|
|
|
|
|
>['options'];
|
|
|
|
|
```
|