Skip to content

Usage – fluent-wp-client

Unified post-like resource API (recommended)
const posts = wp.content('posts');
const recentPosts = await posts.list({ perPage: 10 });
const post = await posts.item('hello-world');
const books = wp.content('books');
const allBooks = await books.listAll();
const genres = await wp.terms('genre').list({ perPage: 100 });
content('posts') / content('pages') / content(resource)
terms('categories') / terms('tags') / terms(resource)

Each builder exposes:

  • .list(filter?) — one page
  • .listAll(filter?) — auto-paginates all pages
  • .listPaginated(filter?) — returns { data, total, totalPages, page, perPage }
  • .item(idOrSlug) — single item (awaitable)
  • .create() / .update() / .delete() — mutations
  • .describe() — JSON Schema discovery

content(resource).item(idOrSlug) also supports .getContent() for { raw, rendered, protected }. Pass { embed: true } or { embed: ['author', 'wp:term'] } to request embedded relations.

ResourceExtras
media().upload(), .getImageUrl()
users().me()
comments()—
settings().get(), .update(), .describe() (singleton)

All collection resources share .list(), .listAll(), .listPaginated(), .item(), .create(), .update(), .delete(), .describe(). Pass { embed: true } on .item() or .list() to request embedded data.

const results = await wp.searchContent('hello');
const pages = await wp.searchContent('about', { type: 'post', subtype: 'page' });
const mixed = await wp.searchContent('test', { subtype: ['post', 'page', 'book'] });

Returns { id, title, url, type, subtype }[].

Typed core filters accept extra unknown keys. CamelCase names become snake_case query params automatically:

const posts = await wp.content('posts').list({
search: 'hello',
include: [4, 8],
titleSearch: 'Hello', // becomes title_search
});

All read and mutation helpers accept optional custom headers:

const posts = await wp.content('posts').list(
{ perPage: 5 },
{ headers: { 'x-custom-header': 'value' } },
);
  • loginWithJwt(credentials) / validateJwtToken(token?)
  • getAbilities() / getAbility(name) / getAbilityCategories()
  • executeGetAbility() / executeRunAbility() / executeDeleteAbility()
  • ability<TInput, TOutput>(name) — fluent builder with .get(), .run(), .delete(), .describe()
  • request(options) — low-level REST request with response metadata
  • setHeaders(name, value) / hasAuth()
  • getRuntime() — internal runtime access for add-ons and wrappers

WordPress’s native _embed parameter tells the server to inline related data (author, terms, featured media, etc.) directly in the response. Extraction helpers let you pull typed data out of the embedded response without manual casting.

Pass embed as an option on .item() or .list():

Request all embeddable relations
const post = await wp.content('posts').item('hello-world', { embed: true });
const posts = await wp.content('posts').list({ perPage: 10, embed: true });
Selective embed (reduces WordPress server-side work)
const post = await wp.content('posts').item('hello-world', {
embed: ['author', 'wp:term'],
});

Import extraction helpers from the root package:

Extract typed relations from an embedded post
import {
getEmbeddedAuthor,
getEmbeddedTerms,
getEmbeddedFeaturedMedia,
getEmbeddedParent,
} from 'fluent-wp-client';
const post = await wp.content('posts').item('hello-world', { embed: true });
const author = getEmbeddedAuthor(post); // WordPressAuthor | null
const categories = getEmbeddedTerms(post, 'category'); // WordPressCategory[]
const tags = getEmbeddedTerms(post, 'post_tag'); // WordPressTag[]
const media = getEmbeddedFeaturedMedia(post); // WordPressMedia | null
const parent = getEmbeddedParent(post); // WordPressPostLike | null

Use getEmbeddableLinkKeys() to inspect which relations a post supports before requesting them:

Discover embeddable relations
import { getEmbeddableLinkKeys } from 'fluent-wp-client';
const post = await wp.content('posts').item(1, { fields: ['id', '_links'] });
const embeddable = getEmbeddableLinkKeys(post);
// ['author', 'wp:term', 'wp:featuredmedia', 'replies', ...]
ApproachWhen to use
embed: trueYou need all relations — author, terms, media, parent, replies
embed: ['author', 'wp:term']You only need specific relations — reduces server work
No embed (default)Lean payloads — fetch relations separately if needed

When ACF fields are configured with show_in_rest: true, ACF relation data can be embedded and extracted:

ACF embed and extraction
import {
getAcfFieldPosts,
getAcfFieldPost,
getAcfFieldTerms,
} from 'fluent-wp-client';
const post = await wp.content('posts').item('my-post', {
embed: ['acf:post', 'acf:term'],
});
const relatedPosts = getAcfFieldPosts(post, 'acf_related_posts'); // WordPressPost[]
const featuredPost = getAcfFieldPost(post, 'acf_featured_post'); // WordPressPostLike | null
const relatedTerms = getAcfFieldTerms(post, 'acf_related_genres'); // WordPressTerm[]
Embed keyDataExtraction helper
authorPost authorgetEmbeddedAuthor()
wp:termCategories, tags, custom taxonomiesgetEmbeddedTerms()
wp:featuredmediaFeatured imagegetEmbeddedFeaturedMedia()
upParent post/pagegetEmbeddedParent()
repliesCommentsgetEmbeddedReplies()
acf:postACF post object / relationship fieldsgetAcfFieldPosts(), getAcfFieldPost()
acf:termACF taxonomy fieldsgetAcfFieldTerms()
import type { WordPressCustomPost, WordPressPostBase } from 'fluent-wp-client';
type Artifact = WordPressCustomPost<{ type: 'artifact' }>; // sparse — optional fields
type ContentBook = WordPressPostBase & { type: 'book' }; // strict — requires content fields
const posts = wp.content('posts');
const created = await posts.create({ title: 'My post', status: 'draft' });
await posts.update(created.id, { title: 'Updated', status: 'publish' });
await posts.delete(created.id, { force: true });
const cat = await wp.terms('categories').create({ name: 'News' });
await wp.terms('categories').update(cat.id, { name: 'Company News' });
await wp.terms('categories').delete(cat.id, { force: true });
const comment = await wp.comments().create({ post: 42, content: 'Nice!', status: 'approve' });
await wp.comments().update(comment.id, { content: 'Updated.' });
await wp.comments().delete(comment.id, { force: true });
const media = await wp.media().upload({
file: imageBlob, filename: 'cover.jpg', mimeType: 'image/jpeg',
title: 'Cover', alt_text: 'Book cover',
});
await wp.media().delete(media.id, { force: true });
const me = await wp.users().me();
const settings = await wp.settings().get();
await wp.settings().update({ title: 'New Title' });

For CPT and custom taxonomy CRUD, see the custom endpoints guide.

WordPress validates requests upstream. Use .describe(), .explore(), or the CLI when you want local validation in your app:

import { z } from 'zod';
const desc = await wp.content('posts').describe();
const createSchema = z.fromJSONSchema(desc.schemas.create!);
const validatedInput = createSchema.parse({ title: 'Validated', status: 'draft' });
const created = await wp.content('posts').create(validatedInput);

See the validation guide for real-time discovery, explore(), CLI-generated schemas, Astro, and AI SDK patterns.

import { withBlocks } from 'fluent-wp-client/blocks';
const wpBlocks = withBlocks(wp);
const blocks = await wpBlocks.content('posts').item('hello-world').blocks().get();

See the Gutenberg content guide for the full workflow.

All runtime failures thrown by the client use WordPressClientError (or a subclass). Use instanceof to narrow to specific error types, or check error.kind for serialization/logging:

  • WordPressHttpError — non-2xx HTTP responses (includes status, statusText, wpCode, wpMessage, responseBody)
  • WordPressAuthError — authentication precondition failures
  • WordPressNetworkError — fetch-level connectivity failures
  • WordPressTimeoutError — request timeouts
  • WordPressParseError — response body parsing failures
  • WordPressConfigError — missing or invalid client configuration
  • WordPressDiscoveryError — schema discovery failures
  • WordPressInvalidRequestError — malformed input or precondition violations
  • WordPressBlockValidationError — block tree validation failures (from fluent-wp-client/blocks)

Local application-side schema validation stays in your validator of choice after .describe() or .explore(). Those validator-native errors are not wrapped by the client.

import { WordPressClientError, WordPressHttpError } from 'fluent-wp-client';
try {
await wp.content('posts').create({ title: 'Hello' });
} catch (error) {
if (error instanceof WordPressHttpError) {
console.log(error.status); // e.g. 401
console.log(error.wpCode); // e.g. 'rest_cannot_create'
console.log(error.wpMessage); // e.g. 'Sorry, you are not allowed...'
}
// Catch-all for any client error
if (error instanceof WordPressClientError) {
console.log(error.kind); // e.g. 'WP_API_ERROR'
console.log(error.retryable); // Can the caller retry?
}
}

Every error supports toJSON() for structured logging and cross-boundary transport:

catch (error) {
if (error instanceof WordPressClientError) {
console.log(JSON.stringify(error));
// {"name":"WordPressHttpError","kind":"WP_API_ERROR","message":"...","retryable":false,"status":401,...}
}
}

Cross-origin absolute URLs are rejected before requests are sent.

  • TypeScript92%
  • PHP6%
  • JavaScript2%