API Records - JavaScript SDK Documentation
Overview
The Records API provides comprehensive CRUD (Create, Read, Update, Delete) operations for collection records, along with powerful search, filtering, and authentication capabilities.
Key Features:
- Paginated list and search with filtering and sorting
- Single record retrieval with expand support
- Create, update, and delete operations
- Batch operations for multiple records
- Authentication methods (password, OAuth2, OTP)
- Email verification and password reset
- Relation expansion up to 6 levels deep
- Field selection and excerpt modifiers
Backend Endpoints:
GET /api/collections/{collection}/records- List recordsGET /api/collections/{collection}/records/{id}- View recordPOST /api/collections/{collection}/records- Create recordPATCH /api/collections/{collection}/records/{id}- Update recordDELETE /api/collections/{collection}/records/{id}- Delete recordPOST /api/batch- Batch operations
CRUD Operations
List/Search Records
Returns a paginated records list with support for sorting, filtering, and expansion.
import BosBase from 'bosbase';
const pb = new BosBase('http://127.0.0.1:8090');
// Basic list with pagination
const result = await pb.collection('posts').getList(1, 50);
console.log(result.page); // 1
console.log(result.perPage); // 50
console.log(result.totalItems); // 150
console.log(result.totalPages); // 3
console.log(result.items); // Array of records
Advanced List with Filtering and Sorting
// Filter and sort
const result = await pb.collection('posts').getList(1, 50, {
filter: 'created >= "2022-01-01 00:00:00" && status = "published"',
sort: '-created,title', // DESC by created, ASC by title
expand: 'author,categories',
});
// Filter with operators
const result2 = await pb.collection('posts').getList(1, 50, {
filter: 'title ~ "javascript" && views > 100',
sort: '-views',
});
Get Full List
Fetch all records at once (useful for small collections):
// Get all records
const allPosts = await pb.collection('posts').getFullList({
sort: '-created',
filter: 'status = "published"',
});
// With batch size for large collections
const allPosts = await pb.collection('posts').getFullList(200, {
sort: '-created',
});
Get First Matching Record
Get only the first record that matches a filter:
const post = await pb.collection('posts').getFirstListItem(
'slug = "my-post-slug"',
{
expand: 'author,categories.tags',
}
);
View Record
Retrieve a single record by ID:
// Basic retrieval
const record = await pb.collection('posts').getOne('RECORD_ID');
// With expanded relations
const record = await pb.collection('posts').getOne('RECORD_ID', {
expand: 'author,categories,tags',
});
// Nested expand
const record = await pb.collection('comments').getOne('COMMENT_ID', {
expand: 'post.author,user',
});
// Field selection
const record = await pb.collection('posts').getOne('RECORD_ID', {
fields: 'id,title,content,author.name',
});
Create Record
Create a new record:
// Simple create
const record = await pb.collection('posts').create({
title: 'My First Post',
content: 'Lorem ipsum...',
status: 'draft',
});
// Create with relations
const record = await pb.collection('posts').create({
title: 'My Post',
author: 'AUTHOR_ID', // Single relation
categories: ['cat1', 'cat2'], // Multiple relation
});
// Create with file upload (multipart/form-data)
const formData = new FormData();
formData.append('title', 'My Post');
formData.append('image', fileInput.files[0]);
const record = await pb.collection('posts').create(formData);
// Create with expand to get related data immediately
const record = await pb.collection('posts').create({
title: 'My Post',
author: 'AUTHOR_ID',
}, {
expand: 'author',
});
Update Record
Update an existing record:
// Simple update
const record = await pb.collection('posts').update('RECORD_ID', {
title: 'Updated Title',
status: 'published',
});
// Update with relations
await pb.collection('posts').update('RECORD_ID', {
'categories+': 'NEW_CATEGORY_ID', // Append
'tags-': 'OLD_TAG_ID', // Remove
});
// Update with file upload
const formData = new FormData();
formData.append('title', 'Updated Title');
formData.append('image', newFile);
const record = await pb.collection('posts').update('RECORD_ID', formData);
// Update with expand
const record = await pb.collection('posts').update('RECORD_ID', {
title: 'Updated',
}, {
expand: 'author,categories',
});
Delete Record
Delete a record:
// Simple delete
await pb.collection('posts').delete('RECORD_ID');
// Note: Returns 204 No Content on success
// Throws error if record doesn't exist or permission denied
Filter Syntax
The filter parameter supports a powerful query syntax:
Comparison Operators
// Equal
filter: 'status = "published"'
// Not equal
filter: 'status != "draft"'
// Greater than / Less than
filter: 'views > 100'
filter: 'created < "2023-01-01"'
// Greater/Less than or equal
filter: 'age >= 18'
filter: 'price <= 99.99'
String Operators
// Contains (like)
filter: 'title ~ "javascript"'
// Equivalent to: title LIKE "%javascript%"
// Not contains
filter: 'title !~ "deprecated"'
// Exact match (case-sensitive)
filter: 'email = "user@example.com"'
Array Operators (for multiple relations/files)
// Any of / At least one
filter: 'tags.id ?= "TAG_ID"' // Any tag matches
filter: 'tags.name ?~ "important"' // Any tag name contains "important"
// All must match
filter: 'tags.id = "TAG_ID" && tags.id = "TAG_ID2"'
Logical Operators
// AND
filter: 'status = "published" && views > 100'
// OR
filter: 'status = "published" || status = "featured"'
// Parentheses for grouping
filter: '(status = "published" || featured = true) && views > 50'
Special Identifiers
// Request context (only in API rules, not client filters)
// @request.auth.id, @request.query.*, etc.
// Collection joins
filter: '@collection.users.email = "test@example.com"'
// Record fields
filter: 'author.id = @request.auth.id'
Comments
// Single-line comments are supported
filter: 'status = "published" // Only published posts'
Sorting
Sort records using the sort parameter:
// Single field (ASC)
sort: 'created'
// Single field (DESC)
sort: '-created'
// Multiple fields
sort: '-created,title' // DESC by created, then ASC by title
// Supported fields
sort: '@random' // Random order
sort: '@rowid' // Internal row ID
sort: 'id' // Record ID
sort: 'fieldName' // Any collection field
// Relation field sorting
sort: 'author.name' // Sort by related author's name
Field Selection
Control which fields are returned:
// Specific fields
fields: 'id,title,content'
// All fields at level
fields: '*'
// Nested field selection
fields: '*,author.name,author.email'
// Excerpt modifier for text fields
fields: '*,content:excerpt(200,true)'
// Returns first 200 characters with ellipsis if truncated
// Combined
fields: '*,content:excerpt(200),author.name,author.email'
Expanding Relations
Expand related records without additional API calls:
// Single relation
expand: 'author'
// Multiple relations
expand: 'author,categories,tags'
// Nested relations (up to 6 levels)
expand: 'author.profile,categories.tags'
// Back-relations
expand: 'comments_via_post.user'
See Relations Documentation for detailed information.
Pagination Options
// Skip total count (faster queries)
const result = await pb.collection('posts').getList(1, 50, {
skipTotal: true, // totalItems and totalPages will be -1
filter: 'status = "published"',
});
// Get Full List with batch processing
const allPosts = await pb.collection('posts').getFullList(200, {
sort: '-created',
});
// Processes in batches of 200 to avoid memory issues
Batch Operations
Execute multiple operations in a single transaction:
// Create a batch
const batch = pb.createBatch();
// Add operations
batch.collection('posts').create({
title: 'Post 1',
author: 'AUTHOR_ID',
});
batch.collection('posts').create({
title: 'Post 2',
author: 'AUTHOR_ID',
});
batch.collection('tags').update('TAG_ID', {
name: 'Updated Tag',
});
batch.collection('categories').delete('CAT_ID');
// Upsert (create or update based on id)
batch.collection('posts').upsert({
id: 'EXISTING_ID',
title: 'Updated Post',
});
// Send batch request
const results = await batch.send();
// Results is an array matching the order of operations
results.forEach((result, index) => {
if (result.status >= 400) {
console.error(`Operation ${index} failed:`, result.body);
} else {
console.log(`Operation ${index} succeeded:`, result.body);
}
});
Note: Batch operations must be enabled in Dashboard > Settings > Application.
Authentication Actions
List Auth Methods
Get available authentication methods for a collection:
const methods = await pb.collection('users').listAuthMethods();
console.log(methods.password.enabled); // true/false
console.log(methods.oauth2.enabled); // true/false
console.log(methods.oauth2.providers); // Array of OAuth2 providers
console.log(methods.otp.enabled); // true/false
console.log(methods.mfa.enabled); // true/false
Auth with Password
const authData = await pb.collection('users').authWithPassword(
'user@example.com', // username or email
'password123'
);
// Auth data is automatically stored in pb.authStore
console.log(pb.authStore.isValid); // true
console.log(pb.authStore.token); // JWT token
console.log(pb.authStore.record.id); // User ID
// Access the returned data
console.log(authData.token);
console.log(authData.record);
// With expand
const authData = await pb.collection('users').authWithPassword(
'user@example.com',
'password123',
{
expand: 'profile',
}
);
Auth with OAuth2
// Step 1: Get OAuth2 URL (usually done in UI)
const methods = await pb.collection('users').listAuthMethods();
const provider = methods.oauth2.providers.find(p => p.name === 'google');
// Redirect user to provider.authURL
window.location.href = provider.authURL;
// Step 2: After redirect, exchange code for token
const authData = await pb.collection('users').authWithOAuth2Code(
'google', // Provider name
'AUTHORIZATION_CODE', // From redirect URL
provider.codeVerifier, // From step 1
'https://yourapp.com/callback', // Redirect URL
{ // Optional data for new accounts
name: 'John Doe',
}
);
Auth with OTP (One-Time Password)
// Step 1: Request OTP
const otpRequest = await pb.collection('users').requestOTP('user@example.com');
// Returns: { otpId: "..." }
// Step 2: User enters OTP from email
// Step 3: Authenticate with OTP
const authData = await pb.collection('users').authWithOTP(
otpRequest.otpId,
'123456' // OTP from email
);
Auth Refresh
Refresh the current auth token and get updated user data:
// Refresh auth (useful on page reload)
const authData = await pb.collection('users').authRefresh();
// Check if still valid
if (pb.authStore.isValid) {
console.log('User is authenticated');
} else {
console.log('Token expired or invalid');
}
Email Verification
// Request verification email
await pb.collection('users').requestVerification('user@example.com');
// Confirm verification (on verification page)
await pb.collection('users').confirmVerification('VERIFICATION_TOKEN');
Password Reset
// Request password reset email
await pb.collection('users').requestPasswordReset('user@example.com');
// Confirm password reset (on reset page)
// Note: This invalidates all previous auth tokens
await pb.collection('users').confirmPasswordReset(
'RESET_TOKEN',
'newpassword123',
'newpassword123' // Confirm
);
Email Change
// Must be authenticated first
await pb.collection('users').authWithPassword('user@example.com', 'password');
// Request email change
await pb.collection('users').requestEmailChange('newemail@example.com');
// Confirm email change (on confirmation page)
// Note: This invalidates all previous auth tokens
await pb.collection('users').confirmEmailChange(
'EMAIL_CHANGE_TOKEN',
'currentpassword'
);
Impersonate (Superuser Only)
Generate a token to authenticate as another user:
// Must be authenticated as superuser
await pb.admins.authWithPassword('admin@example.com', 'password');
// Impersonate a user
const impersonateClient = pb.collection('users').impersonate('USER_ID', 3600);
// Returns a new client instance with impersonated user's token
// Use the impersonated client
const posts = await impersonateClient.collection('posts').getFullList();
// Access the token
console.log(impersonateClient.authStore.token);
console.log(impersonateClient.authStore.record);
Complete Examples
Example 1: Blog Post Search with Filters
async function searchPosts(query, categoryId, minViews) {
let filter = `title ~ "${query}" || content ~ "${query}"`;
if (categoryId) {
filter += ` && categories.id ?= "${categoryId}"`;
}
if (minViews) {
filter += ` && views >= ${minViews}`;
}
const result = await pb.collection('posts').getList(1, 20, {
filter: filter,
sort: '-created',
expand: 'author,categories',
});
return result.items;
}
Example 2: User Dashboard with Related Content
async function getUserDashboard(userId) {
// Get user's posts
const posts = await pb.collection('posts').getList(1, 10, {
filter: `author = "${userId}"`,
sort: '-created',
expand: 'categories',
});
// Get user's comments
const comments = await pb.collection('comments').getList(1, 10, {
filter: `user = "${userId}"`,
sort: '-created',
expand: 'post',
});
return {
posts: posts.items,
comments: comments.items,
};
}
Example 3: Advanced Filtering
// Complex filter example
const result = await pb.collection('posts').getList(1, 50, {
filter: `
(status = "published" || featured = true) &&
created >= "2023-01-01" &&
(tags.id ?= "important" || categories.id = "news") &&
views > 100 &&
author.email != ""
`,
sort: '-views,created',
expand: 'author.profile,tags,categories',
fields: '*,content:excerpt(300),author.name,author.email',
});
Example 4: Batch Create Posts
async function createMultiplePosts(postsData) {
const batch = pb.createBatch();
postsData.forEach(postData => {
batch.collection('posts').create(postData);
});
const results = await batch.send();
// Check for failures
const failures = results
.map((result, index) => ({ index, result }))
.filter(({ result }) => result.status >= 400);
if (failures.length > 0) {
console.error('Some posts failed to create:', failures);
}
return results.map(r => r.body);
}
Example 5: Pagination Helper
async function getAllRecordsPaginated(collectionName, options = {}) {
const allRecords = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await pb.collection(collectionName).getList(page, 500, {
...options,
skipTotal: true, // Skip count for performance
});
allRecords.push(...result.items);
hasMore = result.items.length === 500;
page++;
}
return allRecords;
}
Example 6: OAuth2 Authentication Flow
async function handleOAuth2Login(providerName) {
// Get OAuth2 methods
const methods = await pb.collection('users').listAuthMethods();
const provider = methods.oauth2.providers.find(p => p.name === providerName);
if (!provider) {
throw new Error(`Provider ${providerName} not available`);
}
// Store code verifier for later
sessionStorage.setItem('oauth2_code_verifier', provider.codeVerifier);
sessionStorage.setItem('oauth2_provider', providerName);
// Redirect to provider
window.location.href = provider.authURL;
}
// After redirect callback
async function handleOAuth2Callback(code) {
const codeVerifier = sessionStorage.getItem('oauth2_code_verifier');
const provider = sessionStorage.getItem('oauth2_provider');
const redirectUrl = window.location.origin + '/auth/callback';
try {
const authData = await pb.collection('users').authWithOAuth2Code(
provider,
code,
codeVerifier,
redirectUrl,
{
// Optional: data for new account creation
name: 'User',
}
);
// Success! User is now authenticated
window.location.href = '/dashboard';
} catch (error) {
console.error('OAuth2 authentication failed:', error);
}
}
Error Handling
try {
const record = await pb.collection('posts').create({
title: 'My Post',
});
} catch (error) {
if (error.status === 400) {
// Validation error
console.error('Validation errors:', error.data);
} else if (error.status === 403) {
// Permission denied
console.error('Access denied');
} else if (error.status === 404) {
// Not found
console.error('Collection or record not found');
} else {
console.error('Unexpected error:', error);
}
}
Best Practices
- Use Pagination: Always use pagination for large datasets
- Skip Total When Possible: Use
skipTotal: truefor better performance when you don’t need counts - Batch Operations: Use batch for multiple operations to reduce round trips
- Field Selection: Only request fields you need to reduce payload size
- Expand Wisely: Only expand relations you actually use
- Filter Before Sort: Apply filters before sorting for better performance
- Cache Auth Tokens: Auth tokens are automatically stored in
authStore, no need to manually cache - Handle Errors: Always handle authentication and permission errors gracefully
Related Documentation
- Collections - Collection configuration
- Relations - Working with relations
- API Rules and Filters - Filter syntax details
- Authentication - Detailed authentication guide
- Files - File uploads and handling