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

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