@multocms/connector
v1.5.1
Published
Official JavaScript/TypeScript client library for MultoCMS REST API with automatic authentication and token management
Maintainers
Readme
@multocms/connector
Official JavaScript/TypeScript client library for MultoCMS REST API.
This package provides a type-safe, easy-to-use interface for connecting to MultoCMS APIs with automatic authentication, token management, and request/response handling.
Features
- ✅ Automatic Authentication - Handles API key exchange for JWT tokens
- ✅ Token Management - Automatic token refresh on expiration
- ✅ TypeScript Support - Full type definitions for all endpoints
- ✅ Simple API - Clean, intuitive method names
- ✅ Error Handling - Consistent error responses
- ✅ Environment Variables - Works seamlessly with
.envfiles
Installation
npm install @multocms/connectorQuick Start
1. Set Environment Variables
Create a .env file or set environment variables:
MULTOCMS_BASE_URL=https://your-site.com/api/v1
MULTOCMS_API_KEY=your-key-id.your-secret
MULTOCMS_API_SECRET=your-secret
# Optional: Cache duration in seconds (default: 0 = no cache)
MULTOCMS_CACHE_REVALIDATE=60Or you can pass them directly to the constructor:
import { MultoCMSClient } from '@multocms/connector';
const client = new MultoCMSClient({
baseUrl: 'https://your-site.com/api/v1',
apiKey: 'your-key-id.your-secret',
apiSecret: 'your-secret',
cacheRevalidate: 0, // 0 = no cache (default), 60 = cache 60s, false = use Next.js default
});2. Use the Client
import { MultoCMSClient } from '@multocms/connector';
// Client auto-authenticates with API key if provided
const client = new MultoCMSClient({
baseUrl: process.env.MULTOCMS_BASE_URL!, // e.g., 'https://your-site.com/api/v1'
// API key/secret from environment or passed directly
});
// Get the current site (associated with your API key)
// Client will auto-authenticate if API key is provided
const siteResponse = await client.getCurrentSite();
if (siteResponse.success && siteResponse.data) {
const site = siteResponse.data;
console.log('Site:', site.display_name);
console.log('Tagline:', site.tagline);
// Hero banner will be null if not set
if (site.hero_banner) {
console.log('Hero banner:', site.hero_banner.url);
console.log('Sizes available:', Object.keys(site.hero_banner.sizes || {}));
}
} else {
console.error('Error getting site:', siteResponse.error);
}
// Get all posts
const response = await client.getPosts();
if (response.success) {
console.log('Posts:', response.data);
} else {
console.error('Error:', response.error);
}
// Get a single post by slug
const postResponse = await client.getPost('hello-world', {
post_type: 'post',
include: 'author,categories,featured_image',
});
if (postResponse.success) {
const post = postResponse.data;
console.log('Post title:', post.title);
console.log('Featured image:', post.featured_image?.url);
}Authentication
The client supports two authentication methods:
1. API Key (Recommended for Server-to-Server)
const client = new MultoCMSClient({
baseUrl: 'https://your-site.com/api/v1',
apiKey: process.env.MULTOCMS_API_KEY!,
apiSecret: process.env.MULTOCMS_API_SECRET!,
});
// Client automatically exchanges API key for JWT tokens
await client.authenticate();2. Manual JWT Token
If you already have tokens:
const client = new MultoCMSClient({
baseUrl: 'https://your-site.com/api/v1',
accessToken: 'your-access-token',
refreshToken: 'your-refresh-token',
});Token Refresh
The client automatically refreshes tokens when they expire. You can disable this:
const client = new MultoCMSClient({
baseUrl: 'https://your-site.com/api/v1',
apiKey: process.env.MULTOCMS_API_KEY!,
apiSecret: process.env.MULTOCMS_API_SECRET!,
autoRefresh: false, // Disable automatic refresh
});Or manually refresh:
await client.refreshAccessToken();API Methods
Sites
// Get all sites (includes settings for each site)
const sitesResponse = await client.getSites();
if (sitesResponse.success && sitesResponse.data) {
sitesResponse.data.forEach(site => {
console.log('Site:', site.display_name);
console.log('Settings:', site.settings);
// Each site includes all non-authentication settings
console.log('Posts per page:', site.settings.posts_per_page);
});
}
// Get a single site by ID (numeric or MongoDB ObjectId)
// This also includes all settings
const siteResponse = await client.getSite(2); // or client.getSite('507f1f77bcf86cd799439011');
if (siteResponse.success && siteResponse.data) {
const site = siteResponse.data;
console.log('Site name:', site.display_name);
console.log('Settings:', site.settings);
// Access specific settings
console.log('Site title:', site.settings.site_title);
console.log('Image sizes:', site.settings.image_sizes);
}
// Get the current site (site associated with the API key)
// This is especially useful when using API key authentication
// IMPORTANT: Make sure you've authenticated first!
await client.authenticate(); // Required if using API key
const currentSite = await client.getCurrentSite();
// Or use the shorthand:
const currentSite2 = await client.getSite('current'); // or 'me'
// Check if request was successful
if (currentSite.success && currentSite.data) {
const site = currentSite.data;
console.log('Site name:', site.display_name);
console.log('Site tagline:', site.tagline);
// Access hero banner information (will be null if not set)
if (site.hero_banner) {
const heroBanner = site.hero_banner;
console.log('Hero banner URL:', heroBanner.url);
console.log('Thumbnail:', heroBanner.sizes?.thumbnail?.url);
console.log('Medium size:', heroBanner.sizes?.medium?.url);
console.log('Large size:', heroBanner.sizes?.large?.url);
// Access custom sizes (like "hero")
if (heroBanner.sizes?.hero) {
console.log('Hero size:', heroBanner.sizes.hero.url);
console.log('Hero dimensions:', heroBanner.sizes.hero.width, 'x', heroBanner.sizes.hero.height);
}
// You can also access any custom size by name
const customSize = heroBanner.sizes?.['your-custom-size-name'];
if (customSize) {
console.log('Custom size URL:', customSize.url);
}
} else {
console.log('No hero banner set for this site');
}
// Access site logo (will be null if not set)
if (site.logo) {
console.log('Site logo URL:', site.logo.url);
console.log('Logo alt text:', site.logo.alt_text);
}
// Access site favicon (will be null if not set)
if (site.favicon) {
console.log('Favicon URL:', site.favicon.url);
}
// Access site settings (all settings except authentication)
console.log('Site settings:', site.settings);
console.log('Site name:', site.settings.site_title);
console.log('Posts per page:', site.settings.posts_per_page);
console.log('Image sizes:', site.settings.image_sizes);
} else {
console.error('Error:', currentSite.error);
}Posts
Include Options:
The include parameter accepts comma-separated values:
author- Include author detailscategories- Include category termstags- Include tag termsfeatured_image- Include featured image detailsmeta_box_fields- Include metabox fields (custom fields from metaboxes)content- Include full post content (for list endpoints)
// List posts with filters
const posts = await client.getPosts({
status: 'published',
post_type: 'post',
page: 1,
per_page: 20,
search: 'keyword',
include: 'author,categories,meta_box_fields',
});
// Access metabox fields (custom fields from metaboxes)
if (posts.success && posts.data) {
posts.data.forEach(post => {
if (post.meta_box_fields) {
// Access metabox fields
console.log('Event schedule:', post.meta_box_fields.event_schedule);
console.log('Phone:', post.meta_box_fields.phone);
console.log('Email:', post.meta_box_fields.email);
// Event schedule example (array of date/time entries)
if (post.meta_box_fields.event_schedule) {
post.meta_box_fields.event_schedule.forEach((entry: any) => {
console.log(`Date: ${entry.date}, Time: ${entry.start_time} - ${entry.end_time}`);
});
}
// Post reference example (single post ID or array of post IDs)
if (post.meta_box_fields.related_posts) {
// Can be a single post ID (string) or array of post IDs
const relatedPostIds = Array.isArray(post.meta_box_fields.related_posts)
? post.meta_box_fields.related_posts
: [post.meta_box_fields.related_posts];
console.log('Related post IDs:', relatedPostIds);
}
}
});
}
// Get posts from this month (published this month)
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
const thisMonthsPosts = await client.getPosts({
status: 'published',
date_from: startOfMonth.toISOString(),
date_to: endOfMonth.toISOString(),
date_field: 'published_at', // Optional, defaults to 'published_at'
});
// Get posts published in a specific date range
const dateRangePosts = await client.getPosts({
status: 'published',
date_from: '2025-01-01T00:00:00Z',
date_to: '2025-01-31T23:59:59Z',
date_field: 'published_at',
});
// Filter by creation date instead
const createdThisMonth = await client.getPosts({
date_from: startOfMonth.toISOString(),
date_to: endOfMonth.toISOString(),
date_field: 'created_at',
});
// Filter posts by term (taxonomy term)
// Get posts with a specific term by ID
const postsByTerm = await client.getPosts({
term_id: '507f1f77bcf86cd799439011',
status: 'published',
});
// Get posts with a specific term by slug
const postsByTermSlug = await client.getPosts({
term_slug: 'technology',
status: 'published',
});
// Get posts with multiple terms (posts that have ANY of the terms)
const postsByMultipleTerms = await client.getPosts({
term_ids: '507f1f77bcf86cd799439011,507f1f77bcf86cd799439012',
status: 'published',
});
// Filter by term and restrict to a specific taxonomy
const categoryPosts = await client.getPosts({
term_slug: 'technology',
taxonomy: 'category',
status: 'published',
});
// Featured image is always included if present (no need to request it)
if (posts.success && posts.data) {
posts.data.forEach(post => {
if (post.featured_image) {
console.log('Featured image URL:', post.featured_image.url);
console.log('Alt text:', post.featured_image.alt_text);
console.log('MIME type:', post.featured_image.mime_type);
// Access different sizes
if (post.featured_image.sizes) {
console.log('Thumbnail:', post.featured_image.sizes.thumbnail?.url);
console.log('Medium:', post.featured_image.sizes.medium?.url);
console.log('Large:', post.featured_image.sizes.large?.url);
// Access custom sizes
const customSize = post.featured_image.sizes['your-custom-size'];
if (customSize) {
console.log('Custom size:', customSize.url);
}
}
}
});
}
// Get single post by ID or slug
const post = await client.getPost('hello-world', {
post_type: 'post',
include: 'author,categories,tags,meta_box_fields',
});
// Access metabox fields
if (post.success && post.data?.meta_box_fields) {
const metaFields = post.data.meta_box_fields;
console.log('Event schedule:', metaFields.event_schedule);
console.log('Phone:', metaFields.phone);
// Post reference fields (can be single ID or array of IDs)
if (metaFields.related_posts) {
const relatedPostIds = Array.isArray(metaFields.related_posts)
? metaFields.related_posts
: [metaFields.related_posts];
console.log('Related post IDs:', relatedPostIds);
}
// ... other metabox fields
}
// Featured image is always included
if (post.success && post.data?.featured_image) {
const featuredImage = post.data.featured_image;
console.log('Featured image:', featuredImage.url);
console.log('Dimensions:', featuredImage.width, 'x', featuredImage.height);
}
// Create a post
const newPost = await client.createPost({
title: 'My New Post',
content: '<p>Post content</p>',
excerpt: 'Post excerpt',
status: 'published',
post_type: 'post',
});
// Update a post (full)
await client.updatePost(postId, {
title: 'Updated Title',
content: '<p>Updated content</p>',
});
// Partially update a post
await client.patchPost(postId, {
status: 'published',
});
// Delete a post
await client.deletePost(postId);
// Get post meta (custom fields)
const metaResponse = await client.getPostMeta(postId);
if (metaResponse.success) {
metaResponse.data?.forEach(meta => {
console.log(`${meta.meta_key}: ${meta.meta_value}`);
});
}
// Update post meta (batch update with object)
await client.updatePostMeta(postId, {
'_mb_phone': '(123) 456-7890',
'_mb_email': '[email protected]',
});
// Update post meta (batch update with array)
await client.updatePostMeta(postId, [
{ meta_key: '_mb_phone', meta_value: '(123) 456-7890' },
{ meta_key: '_mb_email', meta_value: '[email protected]' },
]);
// Update a single post meta value
await client.updatePostMetaValue(postId, '_mb_phone', '(123) 456-7890');Media
// List media files
const media = await client.getMedia({
page: 1,
per_page: 20,
search: 'image',
mime_type: 'image/jpeg',
});
// Get single media file
const mediaItem = await client.getMediaItem(mediaId);
// Upload a media file
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
const uploadResponse = await client.uploadMedia(file, {
alt_text: 'A beautiful image',
caption: 'Image caption',
folder_id: '507f1f77bcf86cd799439011', // Optional: upload to specific folder
});
if (uploadResponse.success && uploadResponse.data) {
console.log('Uploaded media ID:', uploadResponse.data.id);
console.log('File URL:', uploadResponse.data.filepath);
console.log('Image sizes:', uploadResponse.data.sizes);
} else {
console.error('Upload failed:', uploadResponse.error);
}
}
// Upload with File object (Node.js)
import fs from 'fs';
const fileBuffer = fs.readFileSync('./image.jpg');
const blob = new Blob([fileBuffer], { type: 'image/jpeg' });
const file = new File([blob], 'image.jpg', { type: 'image/jpeg' });
const uploadResponse = await client.uploadMedia(file, {
alt_text: 'My image',
});
// Upload to a specific folder by name
const folderResponse = await client.getFolderByName('Images');
if (folderResponse.success && folderResponse.data) {
const uploadResponse = await client.uploadMedia(file, {
alt_text: 'My image',
folder_id: folderResponse.data.id,
});
}Media Folders
// List all folders
const folders = await client.getFolders();
// List folders in a specific parent folder
const subfolders = await client.getFolders({
parent_id: '507f1f77bcf86cd799439011',
});
// Search folders by name
const searchResults = await client.getFolders({
search: 'images',
});
// Get folder by exact name (convenience method)
const folderResponse = await client.getFolderByName('Images');
if (folderResponse.success && folderResponse.data) {
console.log('Folder ID:', folderResponse.data.id);
console.log('File count:', folderResponse.data.file_count);
console.log('Subfolder count:', folderResponse.data.subfolder_count);
}
// Get folder by name in a specific parent
const subfolderResponse = await client.getFolderByName('2025', '507f1f77bcf86cd799439011');Menus
// List all menus
const menus = await client.getMenus();
// Get menu by ID
const menu = await client.getMenu(menuId);
// Get menu by location slug
const mainMenu = await client.getMenuByLocation('primary');Taxonomies & Terms
// List all taxonomies
const taxonomies = await client.getTaxonomies();
// Get single taxonomy
const categoryTax = await client.getTaxonomy('category');
// List terms for a taxonomy
const categories = await client.getTerms('category', {
page: 1,
per_page: 50,
});
// Get single term
const category = await client.getTerm('category', termId);
// Create a term
const newCategory = await client.createTerm('category', {
name: 'News',
slug: 'news',
description: 'News category',
});
// Update a term
await client.updateTerm('category', termId, {
name: 'Updated Category',
});
// Delete a term
await client.deleteTerm('category', termId);Users
// List users (requires manage_users permission)
const users = await client.getUsers({
page: 1,
per_page: 20,
search: 'john',
});
// Get current authenticated user
const currentUser = await client.getCurrentUser();
// Get single user
const user = await client.getUser(userId);
// Create user
const newUser = await client.createUser({
username: 'johndoe',
email: '[email protected]',
password: 'secure-password',
first_name: 'John',
last_name: 'Doe',
});
// Update user
await client.updateUser(userId, {
first_name: 'Jane',
});
// Delete user
await client.deleteUser(userId);Settings
// Get all settings
const settings = await client.getSettings();
// Get single setting
const siteName = await client.getSetting('site_name');
// Update a setting
await client.updateSetting('site_name', 'My Site', {
type: 'string',
description: 'Site name',
});Response Format
All methods return a consistent response format:
interface ApiResponse<T> {
success: boolean;
data?: T; // Present when success === true
error?: { // Present when success === false
code: string;
message: string;
details?: any;
};
meta?: {
timestamp: string;
version: string;
pagination?: {
total: number;
count: number;
per_page: number;
current_page: number;
total_pages: number;
};
};
links?: {
self?: string;
next?: string;
prev?: string;
first?: string;
last?: string;
};
}Error Handling
const response = await client.getPost('invalid-post');
if (!response.success) {
console.error('Error Code:', response.error?.code);
console.error('Error Message:', response.error?.message);
switch (response.error?.code) {
case 'NOT_FOUND':
// Handle 404
break;
case 'UNAUTHORIZED':
// Handle 401 - might need to re-authenticate
await client.authenticate();
break;
case 'FORBIDDEN':
// Handle 403 - insufficient permissions
break;
default:
// Handle other errors
}
}TypeScript Support
Full TypeScript types are included:
import { MultoCMSClient, Post, Site, Media, User } from '@multocms/connector';
const client = new MultoCMSClient({
baseUrl: process.env.MULTOCMS_BASE_URL!,
});
const response = await client.getPost('hello-world');
if (response.success) {
// response.data is typed as Post
const post: Post = response.data;
// TypeScript knows about all Post properties
console.log(post.title); // ✅ string
console.log(post.slug); // ✅ string
console.log(post.featured_image?.url); // ✅ string | undefined
}Utility Methods
// Manually set tokens
client.setAccessToken('token');
client.setRefreshToken('refresh-token');
// Get current access token
const token = client.getAccessToken();
// Clear tokens (logout)
client.logout();Environment Variables
The client automatically reads these environment variables:
MULTOCMS_BASE_URL- Base URL for the API (required)MULTOCMS_API_KEY- API key ID.secret formatMULTOCMS_API_SECRET- API secret (if not in key format)MULTOCMS_ACCESS_TOKEN- Pre-existing access tokenMULTOCMS_REFRESH_TOKEN- Pre-existing refresh token
Examples
Next.js API Route
// app/api/posts/route.ts
import { MultoCMSClient } from '@multocms/connector';
export async function GET() {
const client = new MultoCMSClient({
baseUrl: process.env.MULTOCMS_BASE_URL!,
});
const response = await client.getPosts({
status: 'published',
per_page: 10,
});
if (response.success) {
return Response.json(response.data);
}
return Response.json(
{ error: response.error?.message },
{ status: 400 }
);
}React Server Component
// app/posts/page.tsx
import { MultoCMSClient } from '@multocms/connector';
export default async function PostsPage() {
const client = new MultoCMSClient({
baseUrl: process.env.MULTOCMS_BASE_URL!,
});
const response = await client.getPosts({
status: 'published',
per_page: 10,
});
if (!response.success) {
return <div>Error: {response.error?.message}</div>;
}
return (
<div>
<h1>Posts</h1>
{response.data?.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<div dangerouslySetInnerHTML={{ __html: post.excerpt }} />
</article>
))}
</div>
);
}Node.js Script
// scripts/sync-posts.ts
import { MultoCMSClient } from '@multocms/connector';
import dotenv from 'dotenv';
dotenv.config();
async function syncPosts() {
const client = new MultoCMSClient({
baseUrl: process.env.MULTOCMS_BASE_URL!,
});
const response = await client.getPosts({
status: 'published',
per_page: 100,
});
if (response.success) {
console.log(`Found ${response.data?.length} posts`);
response.data?.forEach((post) => {
console.log(`- ${post.title} (${post.slug})`);
});
}
}
syncPosts();Changelog
v1.3.7 (Latest)
- NEW: Configurable cache revalidation via
cacheRevalidateoption orMULTOCMS_CACHE_REVALIDATEenv var - FIX: Fixed authentication retry logic to properly rebuild fetch options with updated headers
- Set to
0for no caching (default),60for 60 seconds, etc. - Set to
falseto use Next.js default caching behavior
License
MIT
Support
For issues, questions, or contributions, please visit the MultoCMS Web Site.
