Skip to content

Quick start ​

This guide gets your first OpenAPI call working in 5 minutes. We recommend the official SDK — it takes care of authentication, signing, and the other fiddly parts.

TIP

Use the SDK The official SDK wraps the entire low-level implementation and works out of the box. If you want to see what's underneath, read the "Under the hood" section at the end of this guide.

Prerequisites ​

Before you start, make sure:

  • ✅ You have OpenAPI access (contact your account manager)

  • ✅ You have the following credentials:

    • App Code - your app code
    • Access Key - your access key
    • Dataset Code - your dataset code
  • ✅ Node.js 16+ or Bun is installed

Install the SDK ​

Bash
npm install @lovrabet/sdk
# 或
bun add @lovrabet/sdk

Scenario 1: Server-side (Node.js/SSR) ​

For Node.js servers, Next.js SSR, API routes, and similar environments.

Basic usage ​

TypeScript
import { createClient } from "@lovrabet/sdk";

// 创建客户端(使用 accessKey)
const client = createClient({
  appCode: "your-app-code",
  accessKey: process.env.LOVRABET_ACCESS_KEY, // 从环境变量读取
  models: {
    users: {
      tableName: "users",
      datasetCode: "your-dataset-code",
    },
  },
});

// 查询数据列表
const response = await client.models.users.filter({
  currentPage: 1,
  pageSize: 20,
});

console.log("总条数:", response.paging.totalCount);
console.log("数据:", response.tableData);

// 查询单条数据
const user = await client.models.users.getOne("user-id");
console.log("用户详情:", user);

Next.js App Router example ​

Inside a Next.js server component:

TypeScript
// app/users/page.tsx (Server Component)
import { createClient } from "@lovrabet/sdk";

const client = createClient({
  appCode: process.env.LOVRABET_APP_CODE!,
  accessKey: process.env.LOVRABET_ACCESS_KEY!,
  models: {
    users: {
      tableName: "users",
      datasetCode: process.env.LOVRABET_DATASET_CODE!,
    },
  },
});

export default async function UsersPage() {
  // 直接在服务端组件中调用
  const { tableData: users } = await client.models.users.filter({
    pageSize: 10,
    currentPage: 1,
  });

  return (
    <div>
      <h1>用户列表</h1>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Next.js API route example ​

Expose an API route for your frontend:

TypeScript
// app/api/users/route.ts
import { createClient } from "@lovrabet/sdk";
import { NextResponse } from "next/server";

const client = createClient({
  appCode: process.env.LOVRABET_APP_CODE!,
  accessKey: process.env.LOVRABET_ACCESS_KEY!,
  models: {
    users: {
      tableName: "users",
      datasetCode: process.env.LOVRABET_DATASET_CODE!,
    },
  },
});

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const page = parseInt(searchParams.get("page") || "1");
    const size = parseInt(searchParams.get("size") || "20");

    const data = await client.models.users.filter({
      currentPage: page,
      pageSize: size,
    });

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json({ error: "查询失败" }, { status: 500 });
  }
}

Scenario 2: Browser (pre-generated token) ​

For public data access from the browser, anonymous users, and similar scenarios.

Step 1: Generate the token on the server ​

Create an API route that returns a token:

TypeScript
// app/api/token/route.ts
import { generateOpenApiToken } from "@lovrabet/sdk";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    const result = await generateOpenApiToken({
      appCode: process.env.LOVRABET_APP_CODE!,
      datasetCode: process.env.LOVRABET_DATASET_CODE!,
      accessKey: process.env.LOVRABET_ACCESS_KEY!,
    });

    // 返回 token、timestamp 和 expiresAt
    return NextResponse.json(result);
  } catch (error) {
    return NextResponse.json({ error: "Token 生成失败" }, { status: 500 });
  }
}

Step 2: Use the token in the browser ​

TypeScript
// app/users/client-page.tsx
"use client";

import { createClient } from "@lovrabet/sdk";
import { useEffect, useState } from "react";

export default function ClientUsersPage() {
  const [users, setUsers] = useState([]);
  const [client, setClient] = useState(null);

  useEffect(() => {
    async function init() {
      // 1. 获取 token
      const { token, timestamp } = await fetch("/api/token").then((r) =>
        r.json()
      );

      // 2. 创建客户端
      const newClient = createClient({
        appCode: "your-app-code",
        token: token,
        timestamp: timestamp,
        models: {
          users: {
            tableName: "users",
            datasetCode: "your-dataset-code",
          },
        },
      });

      setClient(newClient);

      // 3. 查询数据
      const { tableData } = await newClient.models.users.filter({
        pageSize: 10,
      });
      setUsers(tableData);
    }

    init();
  }, []);

  return (
    <div>
      <h1>用户列表</h1>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Auto-refresh the token ​

Tokens last 10 minutes, so refresh them periodically:

TypeScript
"use client";

import { createClient, isTokenExpiring } from "@lovrabet/sdk";
import { useEffect, useState } from "react";

export default function UsersWithRefresh() {
  const [client, setClient] = useState(null);
  const [timestamp, setTimestamp] = useState(null);

  // 初始化客户端
  useEffect(() => {
    fetchTokenAndCreateClient();
  }, []);

  // Token 过期检查
  useEffect(() => {
    if (!timestamp) return;

    const interval = setInterval(() => {
      // 检查 Token 是否即将过期(提前 1 分钟刷新)
      if (isTokenExpiring(timestamp, 60000)) {
        console.log("Token 即将过期,刷新中...");
        fetchTokenAndCreateClient();
      }
    }, 30000); // 每 30 秒检查一次

    return () => clearInterval(interval);
  }, [timestamp]);

  async function fetchTokenAndCreateClient() {
    const { token, timestamp: newTimestamp } = await fetch("/api/token").then(
      (r) => r.json()
    );

    const newClient = createClient({
      appCode: "your-app-code",
      token: token,
      timestamp: newTimestamp,
      models: {
        users: {
          tableName: "users",
          datasetCode: "your-dataset-code",
        },
        users: {
          tableName: "users",
          datasetCode: "your-dataset-code",
        },
      },
    });

    setClient(newClient);
    setTimestamp(newTimestamp);
  }

  // ... 其他代码
}

For signed-in users reading their own private data.

TypeScript
"use client";

import { createClient } from "@lovrabet/sdk";
import { useEffect, useState } from "react";

export default function PrivateDataPage() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      // 不提供任何认证信息,自动使用浏览器 Cookie
      const client = createClient({
        appCode: "your-app-code",
        models: {
          orders: {
            tableName: "orders",
            datasetCode: "your-dataset-code",
          },
        },
      });

      // 请求会自动携带用户的登录 Cookie
      const { tableData } = await client.models.orders.filter();
      setData(tableData);
    }

    fetchData();
  }, []);

  return (
    <div>
      <h1>我的订单</h1>
      {data.map((order) => (
        <div key={order.id}>{order.orderNo}</div>
      ))}
    </div>
  );
}

Multiple models ​

Configure several data models at once:

TypeScript
const client = createClient({
  appCode: "your-app-code",
  accessKey: process.env.LOVRABET_ACCESS_KEY,
  models: {
    users: {
      tableName: "users",
      datasetCode: "dataset-001",
    },
    orders: {
      tableName: "orders",
      datasetCode: "dataset-002",
    },
    products: {
      tableName: "products",
      datasetCode: "dataset-003",
    },
  },
});

// 使用不同的模型
const users = await client.models.users.filter();
const orders = await client.models.orders.filter();
const products = await client.models.products.filter();

Advanced queries ​

Pagination ​

TypeScript
const response = await client.models.users.filter({
  currentPage: 2,
  pageSize: 50,
});

console.log("当前页:", response.paging.currentPage);
console.log("每页条数:", response.paging.pageSize);
console.log("总条数:", response.paging.totalCount);
console.log("数据:", response.tableData);

Filtering ​

TypeScript
const response = await client.models.users.filter({
  currentPage: 1,
  pageSize: 20,
  // 其他查询条件会传递到 paramMap
  status: "active",
  role: "admin",
});

Fetch everything (walk the pages) ​

TypeScript
async function getAllUsers() {
  const allUsers = [];
  let currentPage = 1;
  const pageSize = 50;
  let hasMore = true;

  while (hasMore) {
    const response = await client.models.users.filter({
      currentPage,
      pageSize,
    });

    allUsers.push(...response.tableData);

    // 判断是否还有更多数据
    hasMore = currentPage * pageSize < response.paging.totalCount;
    currentPage++;

    // 避免请求过快
    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  return allUsers;
}

const allUsers = await getAllUsers();
console.log(`共获取 ${allUsers.length} 条数据`);

Error handling ​

TypeScript
import { createClient, LovrabetError } from "@lovrabet/sdk";

try {
  const response = await client.models.users.filter();
  console.log(response.tableData);
} catch (error) {
  if (error instanceof LovrabetError) {
    console.error("API 错误:", error.message);
    console.error("状态码:", error.statusCode);
    console.error("错误详情:", error.details);
  } else {
    console.error("未知错误:", error);
  }
}

Common errors and fixes ​

1. Signature verification failed ​

Error: errorCode: 1002, errorMsg: "Signature verification failed"

Fix:

  • Check that the Access Key is correct
  • Load it from an environment variable instead of hard-coding it
  • Verify that appCode and datasetCode match

2. Token expired ​

Error: errorCode: 1003, errorMsg: "Timestamp expired"

Fix:

  • Make sure the client clock is accurate
  • Implement automatic token refresh in the browser
  • Check token lifetime with isTokenExpiring()

3. Access denied ​

Error: errorCode: 1006, errorMsg: "Access denied"

Fix:

  • Confirm the Dataset Code is correct
  • Check that the app has access to the dataset
  • Ask your account manager to confirm the permission setup

Environment configuration ​

Using environment variables ​

Create a .env.local file:

Bash
LOVRABET_APP_CODE=your-app-code
LOVRABET_ACCESS_KEY=your-access-key
LOVRABET_DATASET_CODE=your-dataset-code

Then use it in code:

TypeScript
const client = createClient({
  appCode: process.env.LOVRABET_APP_CODE!,
  accessKey: process.env.LOVRABET_ACCESS_KEY!,
  models: {
    users: {
      tableName: "users",
      datasetCode: process.env.LOVRABET_DATASET_CODE!,
    },
  },
});

Performance tips ​

1. Use sensible page sizes ​

TypeScript
// ✅ 推荐:10-50 条
const response = await client.models.users.filter({
  pageSize: 20,
});

// ❌ 避免:单次查询过多数据
const response = await client.models.users.filter({
  pageSize: 1000, // 不推荐
});

2. Don't call getOne in a loop ​

TypeScript
// ❌ 不推荐:循环调用 getOne
for (const id of userIds) {
  const user = await client.models.users.getOne(id);
}

// ✅ 推荐:使用 getList 批量查询
const users = await client.models.users.filter({
  id_in: userIds, // 假设支持 IN 查询
});

3. Add caching ​

TypeScript
const cache = new Map();

async function getCachedUsers() {
  const cacheKey = "users-list";

  if (cache.has(cacheKey)) {
    const { data, timestamp } = cache.get(cacheKey);
    // 缓存 1 分钟
    if (Date.now() - timestamp < 60000) {
      return data;
    }
  }

  const data = await client.models.users.filter();
  cache.set(cacheKey, { data, timestamp: Date.now() });

  return data;
}

Under the hood (advanced) ​

INFO

For reference only The following shows how to work without the SDK. We strongly recommend the SDK unless you have special requirements.

Signature algorithm ​

TypeScript
import crypto from "crypto";

function generateToken(
  timestamp: number,
  appCode: string,
  datasetCode: string,
  accessKey: string,
  secretKey: string = "lovrabet"
): string {
  const params: Record<string, string> = {
    accessKey: accessKey,
    timeStamp: timestamp.toString(),
    appCode: appCode,
  };

  if (datasetCode) {
    params.datasetCode = datasetCode;
  }

  // 按字典序排序参数
  const sortedParams = Object.keys(params)
    .sort()
    .map((key) => `${key}=${params[key]}`)
    .join("&");

  // 计算 HMAC-SHA256
  return crypto
    .createHmac("sha256", secretKey)
    .update(sortedParams, "utf8")
    .digest("base64");
}

Raw HTTP request ​

TypeScript
import axios from "axios";

async function getListRaw() {
  const timestamp = Date.now();
  const appCode = "your-app-code";
  const datasetCode = "your-dataset-id";
  const accessKey = "your-access-key";

  const token = generateToken(timestamp, appCode, datasetCode, accessKey);

  const response = await axios.post(
    "https://runtime.lovrabet.com/openapi/data/get-list",
    {
      appCode: appCode,
      datasetCode: datasetCode,
      paramMap: {
        pageSize: 10,
        currentPage: 1,
      },
    },
    {
      headers: {
        "Content-Type": "application/json",
        "X-Time-Stamp": timestamp.toString(),
        "X-App-Code": appCode,
        "X-Dataset-Code": datasetCode,
        "X-Token": token,
      },
    }
  );

  return response.data;
}

Next steps ​

Congratulations — you've covered the basics of Lovrabet OpenAPI. Where to go next:

Need help? ​

If something goes wrong:

  1. Check the common errors section in this document
  2. Browse the Issues on the GitHub repository
  3. Contact your account manager for technical support

基于飞书知识库同步生成,内容以飞书源文档为准