Official Node.js client for Draftbase, the MDX-based headless CMS for React developers. Zero runtime dependencies, uses global fetch, fully typed — use it to fetch published content, manage entries/content types/media, and sync your CMS schema into TypeScript types, from any Node.js backend or framework (Next.js, Astro, Remix, SvelteKit, Nuxt, Express, Cloudflare Workers).
pnpm add @draftbase/sdk
# or: npm install @draftbase/sdk
# or: yarn add @draftbase/sdkimport { createClient } from "@draftbase/sdk";
const draftbase = createClient({ apiKey: process.env.DRAFTBASE_API_KEY! });Options: apiKey (required), baseUrl (default https://api.draftbase.co), environment (default envId applied to delivery/entries reads, overridable per call), retries (read requests only, default 2), cacheTtlMs (cache for read requests, default 0 = disabled), cache ("memory" default or "disk"), diskCacheDir (only for cache: "disk", default an OS-temp folder).
Use a delivery-scoped key for the top-level getEntries/getEntry/graphql methods, and a management-scoped key for everything under entries, contentTypes, media, webhooks.
The client itself is framework-agnostic (plain Node.js, global fetch) — only the calling convention changes per framework. Instantiate createClient once in a shared module and import it wherever you need content.
// lib/draftbase.ts
import { createClient } from "@draftbase/sdk";
export const draftbase = createClient({ apiKey: process.env.DRAFTBASE_API_KEY! });// app/blog/[slug]/page.tsx
import { draftbase } from "@/lib/draftbase";
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const entry = await draftbase.getEntry(slug);
if (!entry) return notFound();
return <article>{entry.fields.title}</article>;
}Also works in Route Handlers (app/api/**/route.ts) and Server Actions — anywhere Node.js fetch runs server-side.
---
// src/pages/blog/[slug].astro
import { draftbase } from "../../lib/draftbase";
const entry = await draftbase.getEntry(Astro.params.slug);
---
<h1>{entry.fields.title}</h1>// app/routes/blog.$slug.tsx
import { draftbase } from "~/lib/draftbase";
import { data } from "react-router";
export async function loader({ params }: Route.LoaderArgs) {
const entry = await draftbase.getEntry(params.slug!);
if (!entry) throw data(null, { status: 404 });
return { entry };
}// src/routes/blog/[slug]/+page.server.ts
import { draftbase } from "$lib/draftbase";
import { error } from "@sveltejs/kit";
export async function load({ params }) {
const entry = await draftbase.getEntry(params.slug);
if (!entry) error(404);
return { entry };
}// server/api/blog/[slug].ts
import { draftbase } from "~/server/utils/draftbase";
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, "slug");
const entry = await draftbase.getEntry(slug!);
if (!entry) throw createError({ statusCode: 404 });
return entry;
});import express from "express";
import { draftbase } from "./lib/draftbase.js";
const app = express();
app.get("/blog/:slug", async (req, res) => {
const entry = await draftbase.getEntry(req.params.slug);
if (!entry) return res.sendStatus(404);
res.json(entry);
});All of the above use getEntry/getEntries (delivery-scoped, published-only reads) — swap in entries.*/contentTypes.*/media.* (management-scoped) the same way for authoring/admin UIs.
const { entries, nextCursor } = await draftbase.getEntries({
contentTypeId: "blogPost", // optional
locale: "en-US", // optional
limit: 25, // optional, max 100, default 25
after: nextCursor, // optional, cursor pagination
});
const entry = await draftbase.getEntry("<entry id>"); // null if not foundgetEntries/getEntry responses are CDN-cached at the edge (per API key, keyed on the full query) — a cache hit is served without reaching the origin, so it doesn't count against your org's rate limit. Cache misses do.
Pin a client to one environment (matches each entry's envId, e.g. "staging" vs "production"):
const draftbase = createClient({ apiKey, environment: "staging" });
await draftbase.getEntries(); // envId=staging
await draftbase.getEntries({ envId: "production" }); // per-call overrideSame delivery-scoped, published-only data as getEntries/getEntry, queryable as GraphQL (Query.entries, Query.entry, matching args including envId):
const data = await draftbase.graphql<{ entry: { fields: { title: string } } }>(
`query($id: ID!) { entry(id: $id) { fields } }`,
{ id: "<entry id>" },
);Throws GraphqlError (with an errors array) if the response has GraphQL errors.
await draftbase.entries.list({ contentTypeId, locale, status }); // any status, all filters optional
await draftbase.entries.get(id); // null if not found
await draftbase.entries.create({ contentTypeId, locale, fields }); // -> { id }, starts as "draft"
await draftbase.entries.update(id, fields); // replaces fields, bumps version, snapshots a revision
await draftbase.entries.updateStatus(id, "published"); // draft | review | published | archived
await draftbase.entries.rollback(id, version); // restore fields from a past revision
await draftbase.entries.delete(id);
await draftbase.entries.schedulePublish(id, "2026-01-01T09:00:00Z"); // ISO 8601, replaces any existing schedule
await draftbase.entries.cancelSchedule(id);await draftbase.contentTypes.list();
await draftbase.contentTypes.get(id);
await draftbase.contentTypes.create({ name, fields }); // -> { id }
await draftbase.contentTypes.update(id, { name, fields });
await draftbase.contentTypes.delete(id); // fails if entries still reference itImages are resized (max 1920x1920 by default, org-configurable), converted to WebP, and served off a CDN — asynchronously, right after upload. confirmUpload returns immediately with status: "pending"; poll media.get until it flips to "ready" (or "failed").
const { url, fields, s3Key } = await draftbase.media.getUploadUrl({
fileName,
contentType,
});
const form = new FormData();
for (const [key, value] of Object.entries(fields)) form.append(key, value);
form.append("file", file); // must be the last field
await fetch(url, { method: "POST", body: form }); // S3 presigned POST — enforces the org's size limit
const { id } = await draftbase.media.confirmUpload({
s3Key,
contentType,
altText,
});
const asset = await draftbase.media.get(id); // { status: "pending" | "ready" | "failed", width, height, url, ... }Per-org defaults (max 1920x1920px, 5MB, WebP conversion on) — override, or read what's active:
await draftbase.orgs.getMediaSettings(); // { enabled, maxWidth, maxHeight, maxUploadBytes }
await draftbase.orgs.updateMediaSettings({
maxWidth: 2560,
maxUploadBytes: 10 * 1024 * 1024,
});
await draftbase.orgs.updateMediaSettings({ enabled: false }); // skip resize/convert, keep originals as-isawait draftbase.webhooks.list();
await draftbase.webhooks.create({
url,
events: ["entry.moved_to_review"],
includeContent: true,
envId: "production",
}); // -> { id, secret }
await draftbase.webhooks.delete(id);Webhook requests include a versioned event envelope and HMAC signatures. Use entry.moved_to_review with includeContent: true to trigger an external Claude skill or Python/JavaScript Review Readiness runner as an example.
interface BlogPostFields {
title: string;
body: string;
}
const { entries } = await draftbase.getEntries<BlogPostFields>({
contentTypeId: "blogPost",
});
entries[0].fields.title; // stringNon-2xx responses (other than a 404, which resolves to null) throw DraftbaseError with status and message.
import { DraftbaseError } from "@draftbase/sdk";
try {
await draftbase.getEntries();
} catch (err) {
if (err instanceof DraftbaseError) console.error(err.status, err.message);
}- Read requests (
getEntries/getEntry/graphql/entries.list/entries.get/contentTypes.list/contentTypes.get) retry automatically on network errors or429/502/503/504, with exponential backoff (300ms,600ms, ...). Disable withretries: 0. - Mutations (
create/update/delete/...) are never auto-retried — they aren't idempotent. - Set
cacheTtlMsoncreateClientto cache read responses for that long (default0, disabled). Create a second client with a differentcacheTtlMsif you need both cached and uncached reads in one process. cache: "memory"(default) caches per client instance/process.cache: "disk"persists across processes underdiskCacheDir(default an OS-temp folder) — Node-only, and only useful where the filesystem is writable and persistent between invocations (a long-running server or local dev, not typical serverless/edge runtimes).
Pull your org's content types and generate a .d.ts with one interface per content type:
npx draftbase-sync --api-key <management-key> --out src/types/draftbase.d.ts
# or: DRAFTBASE_API_KEY=... npx draftbase-sync --out src/types/draftbase.d.tsRe-run whenever content types change (e.g. a predev/CI step) to keep Entry<BlogPostFields> etc. in sync with the CMS schema.
If you're an agent implementing Draftbase in a project, follow this checklist:
- Install:
pnpm add @draftbase/sdk(ornpm/yarnequivalent — detect the project's package manager first). - Never hardcode API keys. Read
apiKeyfrom an environment variable (DRAFTBASE_API_KEYor similar) — add it to.env.exampleif the project has one, and confirm it's in.gitignore, don't commit it. - Pick the right key scope:
deliverykey for read-only published content (getEntries/getEntry/graphql);managementkey for anything underentries/contentTypes/media/webhooks. Ask the user which they have if unclear — adeliverykey cannot call management methods and will 401/403. - All methods are async and return typed data directly (no
.datawrapper) —entries.list()etc. — exceptgetEntry/entries.get, which resolve tonullon a 404 instead of throwing. Handle thatnullcase explicitly. - Don't wrap calls in retry loops — reads already retry internally (see Retries & caching); adding your own doubles the backoff.
- Generate types before writing content-shape code: run
npx draftbase-sync --api-key <management-key> --out <path>first, then import the generated interfaces as theEntry<T>type param — don't hand-write field interfaces that can drift from the live schema. - This package has zero runtime dependencies and works in any Node/Next.js context (route handlers, server components, scripts) — it is not usable in a browser bundle (no
apiKeyshould ever ship client-side).
What is Draftbase?
Draftbase is a lightweight, MDX-based headless CMS built for React and Next.js developers. Content is authored as MDX/markdown with typed fields, then delivered via REST, GraphQL, or this SDK, and rendered with @draftbase/renderer into React, Vue, or static HTML.
How is @draftbase/sdk different from calling the REST API directly?
It adds typed responses, automatic retries with backoff on transient read failures, optional response caching, cursor pagination handling, and a draftbase-sync CLI that generates TypeScript interfaces from your live content types — all of that would otherwise be hand-rolled fetch boilerplate.
Does this work with the Next.js App Router / React Server Components?
Yes — every method returns a plain Promise, so await draftbase.getEntry(id) works directly inside an async Server Component or Route Handler with no extra data-fetching library.
Can I use this SDK in the browser? No — it's a server-side client. API keys are secrets and must never ship to a browser bundle; call this SDK from a server component, route handler, loader, or backend, and expose only the data you need to the client.
How do I keep TypeScript types in sync with my CMS schema?
Run npx draftbase-sync --api-key <management-key> --out <path> (see Content type sync) whenever content types change; it regenerates one interface per content type from the live schema.
- npm
- Source (
packages/sdk) - Issues
@draftbase/renderer— renders the MDX this SDK fetches- draftbase.co — product site
- API reference — full REST API this SDK wraps
- MCP server docs
- Framework support
- Docs
- Pricing