JavaScript client
@nodepress-cms/client is a small, typed client for the NodePress API. It has no dependencies and only needs fetch, so it runs in Node 18+, browsers, Deno, Bun and edge runtimes. It builds the query strings for you, including nested where filters, and gives you typed entries when you generate types from your content types.
Install
Section titled “Install”npm install @nodepress-cms/clientCreate a client
Section titled “Create a client”import { createClient } from '@nodepress-cms/client';
const cms = createClient({ url: 'https://cms.example.com' });| Option | Meaning |
|---|---|
url |
The backend, e.g. https://cms.example.com. Required. A trailing /api is fine. |
apiKey |
Sent as X-API-Key. Needed for writes and for unpublished stages. |
token |
A JWT, sent as Authorization: Bearer … |
locale |
Default language for every read |
fallback |
Default for fallback on every read |
environment |
'staging' reads the staging environment (X-NodePress-Environment); needs credentials |
headers |
Extra headers for every request |
fetch |
A custom fetch, e.g. one with Next.js cache options, or a mock in tests |
Read entries
Section titled “Read entries”collection(type) gives you the entries of one content type:
const posts = cms.collection('blog_post');
// GET /api/blog_postconst { data, meta } = await posts.list({ where: { featured: { eq: true }, author: { slug: { eq: 'jane' } } }, sort: 'createdAt:desc', limit: 10, populate: ['author'],});
// GET /api/blog_post/hello-world, or null when there's no published entryconst post = await posts.get('hello-world', { populate: ['author'] });list() takes every option of the REST list endpoint:
| Option | Example |
|---|---|
where |
{ price: { gte: 10 }, OR: [{ category: { eq: 'news' } }, { featured: { eq: true } }] } |
sort |
'slug:asc' |
search |
'headless cms' |
page, limit |
2, 50 |
cursor |
meta.nextCursor from the previous page |
fields |
['title', 'cover'] |
populate |
['author', 'author.company'] |
locale, fallback |
'fr', true |
stage |
'draft' (needs apiKey or token) |
get(slug, options) takes locale, fallback, populate, fields and stage. See Querying for what each one does.
in and notIn take arrays: { category: { in: ['news', 'guides'] } }. On a relation field, a nested object filters on the related entry’s fields, as in author: { slug: { eq: 'jane' } }.
Every entry
Section titled “Every entry”all() walks through every matching entry, page by page, using cursor paging (page paging when you pass search):
for await (const post of cms.collection('blog_post').all({ where: { featured: { eq: true } } })) { console.log(post.slug);}It fetches 100 entries per request unless you pass limit.
Types from your content types
Section titled “Types from your content types”Generate TypeScript types for your content types, then pass them to createClient:
npx create-nodepress-app generate:typesimport { createClient } from '@nodepress-cms/client';import type { NodePressContentTypes } from './types/nodepress';
const cms = createClient<NodePressContentTypes>({ url: 'https://cms.example.com' });
const post = await cms.collection('blog_post').get('hello-world');post?.data.title; // stringcms.collection('typo'); // type error: not a content typeContent type names, data fields, where keys and create/update payloads are then type-checked. Run the command again after changing a content type. See generate:types for where the file is written and its options.
Write entries
Section titled “Write entries”Writes need an apiKey with write or all access, or a token:
const cms = createClient({ url: process.env.CMS_URL!, apiKey: process.env.CMS_WRITE_KEY });const posts = cms.collection('blog_post');
// POST /api/blog_post (published unless status is 'draft')await posts.create({ slug: 'from-the-client', status: 'draft', data: { title: 'Hello' } });
// PUT /api/blog_post/from-the-clientawait posts.update('from-the-client', { title: 'Hello again', body: '<p>…</p>' });
// DELETE /api/blog_post/from-the-client (moves it to the trash)await posts.delete('from-the-client');create takes slug, data and optionally locale and status. update and delete take { locale } as their last argument.
Images
Section titled “Images”imageUrl() builds a resized, re-encoded URL for an uploaded image through the media transform endpoint:
const src = cms.imageUrl(post?.data.cover, { w: 800, format: 'auto' });// https://cms.example.com/api/media/1730000000000-3f2a9c1e.jpg/transform?w=800&format=autoIt accepts an image field value ({ url, alt }) or a URL string. Options: w, h, q, fit (cover, contain, fill, inside, outside) and format (auto, webp, avif, jpeg, png). URLs that don’t point at /uploads/, such as images served from an S3 bucket, come back unchanged. See Media for how sizes are rounded and cached.
GraphQL
Section titled “GraphQL”const data = await cms.graphql<{ blogPosts: { data: { slug: string }[] } }>( `query($limit: Int) { blogPosts(limit: $limit) { data { slug } } }`, { limit: 5 },);graphql() posts to /graphql and returns data. It throws when the response has errors. See GraphQL.
Other helpers
Section titled “Other helpers”| Method | Calls | Returns |
|---|---|---|
cms.locales() |
GET /api/locales |
{ locales, defaultLocale } |
cms.submitForm(slug, data, { captchaToken }) |
POST /api/submit/:slug |
the submission result. See Forms |
Errors
Section titled “Errors”A response other than 2xx throws a NodePressError with the HTTP status and the parsed response body:
import { NodePressError } from '@nodepress-cms/client';
try { await cms.collection('blog_post').create({ slug: 'hello', data: {} });} catch (err) { if (err instanceof NodePressError && err.status === 422) { console.log((err.body as { errors: string[] }).errors); }}get() is the exception: a 404 returns null instead of throwing.
Next.js
Section titled “Next.js”Pass Next.js caching options through a custom fetch:
const cms = createClient({ url: process.env.CMS_URL!, fetch: (input, init) => fetch(input, { ...init, next: { revalidate: 60 } }),});For a complete Next.js, Astro or Nuxt site built on the client, start from a frontend starter.
Live preview
Section titled “Live preview”The client also ships the helpers your site needs to show live edits in the entry editor’s preview pane:
import { onPreviewUpdate, enableClickToEdit } from '@nodepress-cms/client/preview';
onPreviewUpdate((update) => render(update.data), { adminOrigin: 'https://cms.example.com' });enableClickToEdit({ adminOrigin: 'https://cms.example.com' });They’re also exported from the package root, with isInPreviewFrame(). See Live preview for the setup.