bknd-client-setup

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Client Setup

客户端设置

Set up the Bknd TypeScript SDK in your frontend application.
在你的前端应用中设置Bknd TypeScript SDK。

Prerequisites

前提条件

  • Bknd backend running (local or deployed)
  • Frontend project initialized (React, Vue, vanilla JS, etc.)
  • bknd
    package installed:
    npm install bknd
  • 运行中的Bknd后端(本地或已部署)
  • 已初始化的前端项目(React、Vue、原生JS等)
  • 已安装
    bknd
    包:
    npm install bknd

When to Use UI Mode

何时使用UI模式

Not applicable - client setup is code-only.
不适用 - 客户端设置仅通过代码完成。

When to Use Code Mode

何时使用代码模式

  • Always - SDK setup requires code configuration
  • Choose approach based on architecture:
    • Standalone API client - Connecting to separate Bknd backend
    • Embedded (BkndBrowserApp) - Bknd runs in browser (Vite/React)
    • Framework adapter - Next.js, Astro, etc.
  • 始终适用 - SDK设置需要代码配置
  • 根据架构选择方案:
    • 独立API客户端 - 连接到独立的Bknd后端
    • 嵌入式(BkndBrowserApp) - Bknd在浏览器中运行(Vite/React)
    • 框架适配器 - Next.js、Astro等

Approach 1: Standalone API Client

方案1:独立API客户端

Use when connecting to a separate Bknd backend server.
当连接到独立的Bknd后端服务器时使用。

Step 1: Basic Setup

步骤1:基础设置

typescript
import { Api } from "bknd";

const api = new Api({
  host: "https://api.example.com",  // Your Bknd backend URL
});

// Make requests
const { ok, data } = await api.data.readMany("posts");
typescript
import { Api } from "bknd";

const api = new Api({
  host: "https://api.example.com",  // 你的Bknd后端URL
});

// 发起请求
const { ok, data } = await api.data.readMany("posts");

Step 2: Add Token Persistence

步骤2:添加令牌持久化

Store auth tokens across page refreshes:
typescript
import { Api } from "bknd";

const api = new Api({
  host: "https://api.example.com",
  storage: localStorage,  // Persists token as "auth" key
});
Custom storage key:
typescript
const api = new Api({
  host: "https://api.example.com",
  storage: localStorage,
  key: "myapp_auth",  // Custom key instead of "auth"
});
跨页面刷新存储认证令牌:
typescript
import { Api } from "bknd";

const api = new Api({
  host: "https://api.example.com",
  storage: localStorage,  // 将令牌持久化为"auth"键
});
自定义存储键:
typescript
const api = new Api({
  host: "https://api.example.com",
  storage: localStorage,
  key: "myapp_auth",  // 自定义键,替代"auth"
});

Step 3: Handle Auth State Changes

步骤3:处理认证状态变更

React to login/logout events:
typescript
const api = new Api({
  host: "https://api.example.com",
  storage: localStorage,
  onAuthStateChange: (state) => {
    console.log("Auth state:", state);
    // state.user - current user or undefined
    // state.token - JWT or undefined
    // state.verified - whether token was verified with server
  },
});
响应登录/登出事件:
typescript
const api = new Api({
  host: "https://api.example.com",
  storage: localStorage,
  onAuthStateChange: (state) => {
    console.log("认证状态:", state);
    // state.user - 当前用户或undefined
    // state.token - JWT或undefined
    // state.verified - 令牌是否已通过服务器验证
  },
});

Step 4: Cookie-Based Auth (SSR)

步骤4:基于Cookie的认证(SSR)

For server-side rendering with cookies:
typescript
const api = new Api({
  host: "https://api.example.com",
  credentials: "include",  // Send cookies cross-origin
});
适用于使用Cookie的服务器端渲染:
typescript
const api = new Api({
  host: "https://api.example.com",
  credentials: "include",  // 跨域发送Cookie
});

Step 5: Full Configuration

步骤5:完整配置

typescript
import { Api } from "bknd";

const api = new Api({
  // Required
  host: "https://api.example.com",

  // Auth persistence
  storage: localStorage,
  key: "auth",

  // Auth events
  onAuthStateChange: (state) => {
    if (state.user) {
      console.log("Logged in:", state.user.email);
    } else {
      console.log("Logged out");
    }
  },

  // Request options
  credentials: "include",  // For cookies
  verbose: true,           // Log requests (dev only)

  // Data API defaults
  data: {
    defaultQuery: { limit: 20 },
  },
});

export { api };
typescript
import { Api } from "bknd";

const api = new Api({
  // 必填项
  host: "https://api.example.com",

  // 认证持久化
  storage: localStorage,
  key: "auth",

  // 认证事件
  onAuthStateChange: (state) => {
    if (state.user) {
      console.log("已登录:", state.user.email);
    } else {
      console.log("已登出");
    }
  },

  // 请求选项
  credentials: "include",  // 用于Cookie
  verbose: true,           // 记录请求(仅开发环境)

  // Data API默认值
  data: {
    defaultQuery: { limit: 20 },
  },
});

export { api };

Approach 2: React with BkndBrowserApp (Embedded)

方案2:通过BkndBrowserApp集成React(嵌入式)

Use when Bknd runs entirely in the browser (Vite + React).
当Bknd完全在浏览器中运行时使用(Vite + React)。

Step 1: Define Schema

步骤1:定义Schema

typescript
// bknd.config.ts
import { boolean, em, entity, text } from "bknd";

export const schema = em({
  todos: entity("todos", {
    title: text(),
    done: boolean(),
  }),
});

// Type registration for autocomplete
type Database = (typeof schema)["DB"];
declare module "bknd" {
  interface DB extends Database {}
}
typescript
// bknd.config.ts
import { boolean, em, entity, text } from "bknd";

export const schema = em({
  todos: entity("todos", {
    title: text(),
    done: boolean(),
  }),
});

// 注册类型以获得自动补全
type Database = (typeof schema)["DB"];
declare module "bknd" {
  interface DB extends Database {}
}

Step 2: Configure BkndBrowserApp

步骤2:配置BkndBrowserApp

tsx
// App.tsx
import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser";
import { schema } from "./bknd.config";

const config = {
  config: {
    data: schema.toJSON(),
    auth: {
      enabled: true,
      jwt: {
        secret: "your-secret-key",  // Use env var in production
      },
    },
  },
  options: {
    seed: async (ctx) => {
      // Initial data (runs once on empty DB)
      await ctx.em.mutator("todos").insertMany([
        { title: "Learn bknd", done: false },
      ]);
    },
  },
} satisfies BrowserBkndConfig;

export default function App() {
  return (
    <BkndBrowserApp {...config}>
      <YourRoutes />
    </BkndBrowserApp>
  );
}
tsx
// App.tsx
import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser";
import { schema } from "./bknd.config";

const config = {
  config: {
    data: schema.toJSON(),
    auth: {
      enabled: true,
      jwt: {
        secret: "your-secret-key",  // 生产环境使用环境变量
      },
    },
  },
  options: {
    seed: async (ctx) => {
      // 初始数据(空数据库时仅运行一次)
      await ctx.em.mutator("todos").insertMany([
        { title: "学习Bknd", done: false },
      ]);
    },
  },
} satisfies BrowserBkndConfig;

export default function App() {
  return (
    <BkndBrowserApp {...config}>
      <YourRoutes />
    </BkndBrowserApp>
  );
}

Step 3: Use the useApp Hook

步骤3:使用useApp Hook

tsx
import { useApp } from "bknd/adapter/browser";

function TodoList() {
  const { api, app, user, isLoading } = useApp();

  if (isLoading) return <div>Loading...</div>;

  // api - Api instance for data/auth/media
  // app - Full App instance
  // user - Current user or null
  // isLoading - True while initializing

  const [todos, setTodos] = useState([]);

  useEffect(() => {
    api.data.readMany("todos").then(({ data }) => setTodos(data));
  }, [api]);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
tsx
import { useApp } from "bknd/adapter/browser";

function TodoList() {
  const { api, app, user, isLoading } = useApp();

  if (isLoading) return <div>加载中...</div>;

  // api - 用于数据/认证/媒体的Api实例
  // app - 完整的App实例
  // user - 当前用户或null
  // isLoading - 初始化时为True

  const [todos, setTodos] = useState([]);

  useEffect(() => {
    api.data.readMany("todos").then(({ data }) => setTodos(data));
  }, [api]);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

Approach 3: React Standalone (External Backend)

方案3:独立React集成(连接外部后端)

Use when React connects to a separate Bknd server.
当React连接到独立的Bknd服务器时使用。

Step 1: Create API Instance

步骤1:创建API实例

typescript
// lib/api.ts
import { Api } from "bknd";

export const api = new Api({
  host: import.meta.env.VITE_BKND_URL || "http://localhost:7654",
  storage: localStorage,
});
typescript
// lib/api.ts
import { Api } from "bknd";

export const api = new Api({
  host: import.meta.env.VITE_BKND_URL || "http://localhost:7654",
  storage: localStorage,
});

Step 2: Create React Context

步骤2:创建React Context

tsx
// context/BkndContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api, type TApiUser } from "bknd";
import { api } from "../lib/api";

type BkndContextType = {
  api: Api;
  user: TApiUser | null;
  isLoading: boolean;
};

const BkndContext = createContext<BkndContextType | null>(null);

export function BkndProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<TApiUser | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Listen for auth changes
    api.options.onAuthStateChange = (state) => {
      setUser(state.user ?? null);
    };

    // Verify existing token on mount
    api.verifyAuth().finally(() => {
      setUser(api.getUser());
      setIsLoading(false);
    });

    return () => {
      api.options.onAuthStateChange = undefined;
    };
  }, []);

  return (
    <BkndContext.Provider value={{ api, user, isLoading }}>
      {children}
    </BkndContext.Provider>
  );
}

export function useBknd() {
  const ctx = useContext(BkndContext);
  if (!ctx) throw new Error("useBknd must be used within BkndProvider");
  return ctx;
}
tsx
// context/BkndContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api, type TApiUser } from "bknd";
import { api } from "../lib/api";

type BkndContextType = {
  api: Api;
  user: TApiUser | null;
  isLoading: boolean;
};

const BkndContext = createContext<BkndContextType | null>(null);

export function BkndProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<TApiUser | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // 监听认证变更
    api.options.onAuthStateChange = (state) => {
      setUser(state.user ?? null);
    };

    // 挂载时验证现有令牌
    api.verifyAuth().finally(() => {
      setUser(api.getUser());
      setIsLoading(false);
    });

    return () => {
      api.options.onAuthStateChange = undefined;
    };
  }, []);

  return (
    <BkndContext.Provider value={{ api, user, isLoading }}>
      {children}
    </BkndContext.Provider>
  );
}

export function useBknd() {
  const ctx = useContext(BkndContext);
  if (!ctx) throw new Error("useBknd必须在BkndProvider内部使用");
  return ctx;
}

Step 3: Wrap App

步骤3:包裹应用

tsx
// main.tsx
import { BkndProvider } from "./context/BkndContext";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <BkndProvider>
    <App />
  </BkndProvider>
);
tsx
// main.tsx
import { BkndProvider } from "./context/BkndContext";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <BkndProvider>
    <App />
  </BkndProvider>
);

Step 4: Use in Components

步骤4:在组件中使用

tsx
function Profile() {
  const { api, user, isLoading } = useBknd();

  if (isLoading) return <div>Loading...</div>;
  if (!user) return <div>Not logged in</div>;

  return <div>Hello, {user.email}</div>;
}
tsx
function Profile() {
  const { api, user, isLoading } = useBknd();

  if (isLoading) return <div>加载中...</div>;
  if (!user) return <div>未登录</div>;

  return <div>你好,{user.email}</div>;
}

Approach 4: Next.js Integration

方案4:Next.js集成

Step 1: Create API Utility

步骤1:创建API工具

typescript
// lib/bknd.ts
import { Api } from "bknd";

// Client-side singleton
let clientApi: Api | null = null;

export function getClientApi() {
  if (typeof window === "undefined") {
    throw new Error("getClientApi can only be used client-side");
  }

  if (!clientApi) {
    clientApi = new Api({
      host: process.env.NEXT_PUBLIC_BKND_URL!,
      storage: localStorage,
    });
  }

  return clientApi;
}

// Server-side (per-request)
export function getServerApi(request?: Request) {
  return new Api({
    host: process.env.BKND_URL!,
    request,  // Extracts token from cookies/headers
  });
}
typescript
// lib/bknd.ts
import { Api } from "bknd";

// 客户端单例
let clientApi: Api | null = null;

export function getClientApi() {
  if (typeof window === "undefined") {
    throw new Error("getClientApi只能在客户端使用");
  }

  if (!clientApi) {
    clientApi = new Api({
      host: process.env.NEXT_PUBLIC_BKND_URL!,
      storage: localStorage,
    });
  }

  return clientApi;
}

// 服务端(每个请求实例)
export function getServerApi(request?: Request) {
  return new Api({
    host: process.env.BKND_URL!,
    request,  // 从Cookie/Header中提取令牌
  });
}

Step 2: Client Component

步骤2:客户端组件

tsx
"use client";

import { useEffect, useState } from "react";
import { getClientApi } from "@/lib/bknd";

export function PostList() {
  const [posts, setPosts] = useState([]);
  const api = getClientApi();

  useEffect(() => {
    api.data.readMany("posts").then(({ data }) => setPosts(data));
  }, []);

  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
tsx
"use client";

import { useEffect, useState } from "react";
import { getClientApi } from "@/lib/bknd";

export function PostList() {
  const [posts, setPosts] = useState([]);
  const api = getClientApi();

  useEffect(() => {
    api.data.readMany("posts").then(({ data }) => setPosts(data));
  }, []);

  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

Step 3: Server Component

步骤3:服务端组件

tsx
// app/posts/page.tsx
import { getServerApi } from "@/lib/bknd";

export default async function PostsPage() {
  const api = getServerApi();
  const { data: posts } = await api.data.readMany("posts");

  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
tsx
// app/posts/page.tsx
import { getServerApi } from "@/lib/bknd";

export default async function PostsPage() {
  const api = getServerApi();
  const { data: posts } = await api.data.readMany("posts");

  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

TypeScript Type Registration

TypeScript类型注册

Get autocomplete for entity names and fields:
typescript
// types/bknd.d.ts
import { em, entity, text, number } from "bknd";

// Define your schema
const schema = em({
  posts: entity("posts", {
    title: text(),
    views: number(),
  }),
  users: entity("users", {
    email: text(),
    name: text(),
  }),
});

// Register types globally
type Database = (typeof schema)["DB"];
declare module "bknd" {
  interface DB extends Database {}
}
Now
api.data.readMany("posts")
returns typed
Post[]
.
为实体名称和字段获得自动补全:
typescript
// types/bknd.d.ts
import { em, entity, text, number } from "bknd";

// 定义你的Schema
const schema = em({
  posts: entity("posts", {
    title: text(),
    views: number(),
  }),
  users: entity("users", {
    email: text(),
    name: text(),
  }),
});

// 全局注册类型
type Database = (typeof schema)["DB"];
declare module "bknd" {
  interface DB extends Database {}
}
现在
api.data.readMany("posts")
会返回类型化的
Post[]

Api Class Methods Reference

Api类方法参考

typescript
// Auth state
api.getUser()           // Current user or null
api.isAuthenticated()   // Has valid token
api.isAuthVerified()    // Token verified with server
api.verifyAuth()        // Verify token (async)
api.getAuthState()      // { token, user, verified }

// Module APIs
api.data.readMany(...)  // CRUD operations
api.auth.login(...)     // Authentication
api.media.upload(...)   // File uploads
api.system.health()     // System checks

// Token management
api.updateToken(token)  // Manually set token
api.token_transport     // "header" | "cookie" | "none"
typescript
// 认证状态
api.getUser()           // 当前用户或null
api.isAuthenticated()   // 是否拥有有效令牌
api.isAuthVerified()    // 令牌是否已通过服务器验证
api.verifyAuth()        // 验证令牌(异步)
api.getAuthState()      // { token, user, verified }

// 模块API
api.data.readMany(...)  // CRUD操作
api.auth.login(...)     // 认证
api.media.upload(...)   // 文件上传
api.system.health()     // 系统检查

// 令牌管理
api.updateToken(token)  // 手动设置令牌
api.token_transport     // "header" | "cookie" | "none"

Environment Variables

环境变量

bash
undefined
bash
undefined

.env.local (Next.js)

.env.local (Next.js)

NEXT_PUBLIC_BKND_URL=http://localhost:7654 BKND_URL=http://localhost:7654
NEXT_PUBLIC_BKND_URL=http://localhost:7654 BKND_URL=http://localhost:7654

.env (Vite)

.env (Vite)

VITE_BKND_URL=http://localhost:7654
undefined
VITE_BKND_URL=http://localhost:7654
undefined

Common Pitfalls

常见陷阱

Token Not Persisting

令牌未持久化

Problem: User logged out after refresh
Fix: Provide storage option:
typescript
// WRONG - no persistence
const api = new Api({ host: "..." });

// CORRECT
const api = new Api({
  host: "...",
  storage: localStorage,
});
问题: 页面刷新后用户登出
解决方法: 提供storage选项:
typescript
// 错误 - 无持久化
const api = new Api({ host: "..." });

// 正确
const api = new Api({
  host: "...",
  storage: localStorage,
});

CORS Errors

CORS错误

Problem: Browser blocks requests to backend
Fix: Configure CORS on backend:
typescript
// bknd.config.ts (server)
const app = new App({
  server: {
    cors: {
      origin: ["http://localhost:3000"],
      credentials: true,
    },
  },
});
问题: 浏览器阻止向后端发起的请求
解决方法: 在后端配置CORS:
typescript
// bknd.config.ts (服务端)
const app = new App({
  server: {
    cors: {
      origin: ["http://localhost:3000"],
      credentials: true,
    },
  },
});

Auth State Not Updating

认证状态未更新

Problem: UI doesn't reflect login/logout
Fix: Use
onAuthStateChange
:
typescript
const api = new Api({
  host: "...",
  onAuthStateChange: (state) => {
    // Update your UI state here
    setUser(state.user ?? null);
  },
});
问题: UI未反映登录/登出状态
解决方法: 使用
onAuthStateChange
typescript
const api = new Api({
  host: "...",
  onAuthStateChange: (state) => {
    // 在此处更新UI状态
    setUser(state.user ?? null);
  },
});

SSR Hydration Mismatch

SSR hydration不匹配

Problem: Server/client render different content
Fix: Check auth on client only:
tsx
function AuthStatus() {
  const [user, setUser] = useState(null);
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
    setUser(api.getUser());
  }, []);

  if (!mounted) return null;  // Avoid hydration mismatch

  return user ? <span>{user.email}</span> : <span>Guest</span>;
}
问题: 服务端和客户端渲染内容不同
解决方法: 仅在客户端检查认证:
tsx
function AuthStatus() {
  const [user, setUser] = useState(null);
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
    setUser(api.getUser());
  }, []);

  if (!mounted) return null;  // 避免hydration不匹配

  return user ? <span>{user.email}</span> : <span>访客</span>;
}

Using Wrong Import Path

使用错误的导入路径

Problem: Import errors
Fix: Use correct subpath:
typescript
// Standalone API
import { Api } from "bknd";

// React browser adapter
import { BkndBrowserApp, useApp } from "bknd/adapter/browser";

// Next.js adapter
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
问题: 导入错误
解决方法: 使用正确的子路径:
typescript
// 独立API
import { Api } from "bknd";

// React浏览器适配器
import { BkndBrowserApp, useApp } from "bknd/adapter/browser";

// Next.js适配器
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";

Multiple Api Instances

多个Api实例

Problem: Auth state inconsistent across app
Fix: Use singleton pattern:
typescript
// lib/api.ts
let api: Api | null = null;

export function getApi() {
  if (!api) {
    api = new Api({ host: "...", storage: localStorage });
  }
  return api;
}
问题: 应用中认证状态不一致
解决方法: 使用单例模式:
typescript
// lib/api.ts
let api: Api | null = null;

export function getApi() {
  if (!api) {
    api = new Api({ host: "...", storage: localStorage });
  }
  return api;
}

Verification

验证

Test your setup:
typescript
import { api } from "./lib/api";

async function test() {
  // 1. Check connection
  const { ok } = await api.data.readMany("posts", { limit: 1 });
  console.log("API connected:", ok);

  // 2. Check auth
  console.log("Authenticated:", api.isAuthenticated());
  console.log("User:", api.getUser());

  // 3. Test login
  const { ok: loginOk } = await api.auth.login("password", {
    email: "test@example.com",
    password: "password123",
  });
  console.log("Login:", loginOk);
}
测试你的设置:
typescript
import { api } from "./lib/api";

async function test() {
  // 1. 检查连接
  const { ok } = await api.data.readMany("posts", { limit: 1 });
  console.log("API已连接:", ok);

  // 2. 检查认证
  console.log("已认证:", api.isAuthenticated());
  console.log("用户:", api.getUser());

  // 3. 测试登录
  const { ok: loginOk } = await api.auth.login("password", {
    email: "test@example.com",
    password: "password123",
  });
  console.log("登录结果:", loginOk);
}

DOs and DON'Ts

注意事项

DO:
  • Use
    storage: localStorage
    for token persistence
  • Handle
    onAuthStateChange
    for reactive UI
  • Use singleton pattern for Api instance
  • Call
    verifyAuth()
    on app startup
  • Use environment variables for host URL
DON'T:
  • Create multiple Api instances
  • Forget to configure CORS on backend
  • Use Api directly in SSR without request context
  • Hardcode backend URLs
  • Ignore auth state changes in UI
建议:
  • 使用
    storage: localStorage
    实现令牌持久化
  • 处理
    onAuthStateChange
    以实现响应式UI
  • 为Api实例使用单例模式
  • 应用启动时调用
    verifyAuth()
  • 使用环境变量存储后端URL
禁止:
  • 创建多个Api实例
  • 忘记在后端配置CORS
  • 在SSR中直接使用Api而不传入请求上下文
  • 硬编码后端URL
  • 忽略UI中的认证状态变更

Related Skills

相关技能

  • bknd-api-discovery - Explore available endpoints
  • bknd-login-flow - Implement authentication
  • bknd-crud-read - Query data with SDK
  • bknd-session-handling - Manage user sessions
  • bknd-local-setup - Set up local backend
  • bknd-api-discovery - 探索可用的端点
  • bknd-login-flow - 实现认证流程
  • bknd-crud-read - 使用SDK查询数据
  • bknd-session-handling - 管理用户会话
  • bknd-local-setup - 设置本地后端