@raisfast/sdk
The official TypeScript SDK for RaisFast.
Framework-agnostic · Auto token refresh · Full type safety · Zero dependencies
Website ·
Docs ·
npm ·
Quick Start
Install
pnpm add @raisfast/sdk
# or
npm install @raisfast/sdk
Quick Start
import { RaisFast } from "@raisfast/sdk";
const client = new RaisFast("http://localhost:9898/api/v1");
// Auth
await client.auth.login("user@example.com", "password");
console.log(client.auth.user);
// Content Type CRUD (custom content type, e.g. "books")
const books = client.ct<Book>("books");
const list = await books.getList(1, 25);
const book = await books.getOne("slug-or-id");
const created = await books.create({ title: "Hello", author: "World" });
await books.update(created.id, { title: "Updated" });
await books.delete(created.id);
Auth
// Login (auto-saves to localStorage)
await client.auth.login("user@example.com", "password");
// Register
await client.auth.register({
email: "user@example.com",
password: "secret",
nickname: "Alice",
});
// Get current user
const me = await client.auth.getMe();
// Update profile
await client.auth.updateMe({ nickname: "New Name" });
// Change password
await client.auth.changePassword({ old_password: "old", new_password: "new" });
// Logout
await client.auth.logout();
The default LocalAuthStore persists auth state to localStorage. Access token refresh is handled automatically on 401 responses.
Token Auto-Refresh
When a request returns 401, the SDK automatically:
- Sends a
POST /auth/refreshwith the stored refresh token (concurrent 401s are deduplicated to a single refresh call). - On success — calls
authStore.save(auth)with the new tokens, then retries the original request once. - On failure — calls
authStore.clear(), ending the session.
This works out of the box with the default LocalAuthStore. API tokens (Bearer rf_...) skip the refresh path since they have no refresh token.
To integrate with your own state management (e.g. Zustand, Pinia, Redux) and handle session expiry, extend BaseAuthStore and override save() / clear(). Below uses Zustand as an example (myStateStore is your own store — replace it with whatever you use):
import { create } from "zustand";
import { BaseAuthStore, RaisFast } from "@raisfast/sdk";
// 1. Your app's state store (Zustand, Pinia, Redux, etc.)
interface MyAuthState {
accessToken: string | null;
refreshToken: string | null;
setTokens: (access: string, refresh: string) => void;
logout: () => void;
}
const myStateStore = create<MyAuthState>((set) => ({
accessToken: null,
refreshToken: null,
setTokens: (access, refresh) =>
set({ accessToken: access, refreshToken: refresh }),
logout: () => set({ accessToken: null, refreshToken: null }),
}));
// 2. Bridge: SDK AuthStore <-> your state store
class MyAuthStore extends BaseAuthStore {
constructor() {
super();
// hydrate from your store on boot
const saved = myStateStore.getState();
if (saved.accessToken) {
this._token = saved.accessToken;
this._refreshToken = saved.refreshToken;
}
}
// Called on login AND after every successful token refresh
save(auth) {
this._token = auth.access_token;
this._refreshToken = auth.refresh_token;
this._user = auth.user;
this._notify();
myStateStore.getState().setTokens(auth.access_token, auth.refresh_token);
}
// Called when refresh fails — session is dead
clear() {
super.clear();
myStateStore.getState().logout();
if (typeof window !== "undefined") {
window.location.href = "/login?expired=1";
}
}
}
// 3. Pass it to the SDK client
const client = new RaisFast("http://localhost:9000/api/v1", {
authStore: new MyAuthStore(),
});
Listen for Auth Changes
const unsub = client.authStore.onChange((token, user) => {
console.log("auth changed:", { token, user });
}, true); // fire immediately with current state
unsub(); // unsubscribe
Content Types (ct)
Content Types (CT) are the core data model in RaisFast. Built-in CTs (like posts, pages, products) are ready to use. Custom CTs are defined via the Admin UI (Content Type Builder) or TOML config — not from application code. The SDK's job is to read and write the data stored in those CTs.
const books = client.ct<Book>("books");
// Paginated list
const page = await books.getList(1, 25, {
sort: "created_at:desc",
// filter expression (PocketBase style, supports parens & && ||)
filter: 'status = "published" && price >= 10',
// or bracket-style field conditions (Strapi style)
filterCond: {
status: "published",
"price[$gte]": 10,
"author[$in]": ["Alice", "Bob"],
},
search: "hello",
fields: "id,title",
});
// Full list (auto-paginates)
const all = await books.getFullList({ sort: "title:asc" });
// First item matching filter
const first = await books.getFirstListItem('slug = "hello-world"');
// Single item
const book = await books.getOne("id-or-slug");
// Create / Update / Delete
const created = await books.create({ title: "New Book", author: "Alice" });
await books.update(created.id, { title: "Updated" });
await books.delete(created.id);
Admin Content Type
Admin content types use the /admin/cms/ prefix:
const adminBooks = client.adminCt<Book>("books");
Admin
// Dashboard stats
const stats = await client.admin.stats.overview();
// Content stats
const contentStats = await client.admin.stats.content("posts");
// Trends
const trends = await client.admin.stats.trends("posts", 30);
// Plugins
const plugins = await client.admin.plugins.list();
await client.admin.plugins.enable("my-plugin");
// Content Types (defined via Admin UI / TOML; SDK only reads the schema)
const types = await client.admin.contentTypes.list();
const booksSchema = await client.admin.contentTypes.get("books");
Request Hooks
beforeSend
Intercept requests before they are sent (e.g. add custom headers):
client.beforeSend = (url, options) => {
console.log("Requesting:", url);
return { url, options };
};
afterSend
Transform responses after they are received:
client.afterSend = (response, data) => {
console.log("Response status:", response.status);
return data;
};
Request Options
All request methods accept an optional RequestOptions:
await books.getList(1, 25, {
headers: { "X-Custom": "value" },
query: { foo: "bar" },
signal: abortController.signal,
fetch: customFetch, // override global fetch
});
Multi-tenant
client.setTenantId("tenant-123");
client.setTenantId(null); // reset to default
Error Handling
import { SDKError } from "@raisfast/sdk";
try {
await books.getOne("nonexistent");
} catch (e) {
if (e instanceof SDKError) {
console.log(e.code); // backend error code
console.log(e.status); // HTTP status
console.log(e.message); // error message
console.log(e.url); // request URL
console.log(e.response); // full response body
console.log(e.isAbort); // was request aborted
console.log(e.originalError); // original Error if any
}
}
TypeScript
The SDK is written in TypeScript and ships type definitions. Generic parameters are available for content types:
interface Book {
id: string;
title: string;
author: string;
price: number;
created_at: string;
}
const books = client.ct<Book>("books");
const book = await books.getOne("slug"); // typed as Book
Build
pnpm build
Outputs CJS + ESM + type declarations via tsup.
License
MIT