TypeScript SDK
Typed delivery client for Atlas CMS — read entries, pages, and media with full type safety instead of hand-writing fetch calls.
TypeScript SDK
@latellu/atlas-sdk is a small, typed client for the Atlas Public API (/api/v1/public/*).
It does the repetitive work the Quickstart does by hand:
- builds the request URL and attaches your
X-API-Keyheader, - unwraps the
{ success, data, meta }response envelope, - parses each entry's
datafield (the API sends it as a JSON string) into a real object, - merges the requested locale's translation over the base content,
- turns 404s into
nulland every other failure into a typedAtlasError.
Paired with the generated types from @latellu/atlas-cli, your content
types, slugs, and entry data are all type-checked at compile time. See
Type Generation.
Two packages, one workflow
@latellu/atlas-sdk is the runtime client. The types it uses come from
@latellu/atlas-cli, which generates an atlas.types.ts file from your workspace schema.
You can use the SDK without the CLI, but then slugs are unconstrained and data is unknown.
See Type Generation.
Install
npm install @latellu/atlas-sdk
# Generate the matching types for your workspace (recommended):
npx @latellu/atlas-cli generate --api-key=atlas_live_abc123xyz --output=./srcRequires Node 18+ (the SDK uses the global fetch). For older runtimes, pass your own
fetch implementation via the fetchImpl option below.
Create a client
import { createClient } from '@latellu/atlas-sdk';
import type { AtlasContentTypes } from './atlas.types'; // generated by @latellu/atlas-cli
export const atlas = createClient<AtlasContentTypes>({
url: 'https://api.atlas.latellu.com',
apiKey: process.env.ATLAS_API_KEY!,
});createClient<TSchema>(config) returns a client whose entries() method is constrained
to the content-type slugs in TSchema. Omit the type parameter and the client still works,
but slugs accept any string and entry data is typed as unknown.
Config options
| Option | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Atlas backend base URL, e.g. https://api.atlas.latellu.com. Trailing slashes are trimmed; do not include /api/v1/public — the SDK adds it. |
apiKey | string | Yes | Workspace API key. The workspace is derived from the key, so you never pass a workspace id. |
fetchImpl | typeof fetch | No | Custom fetch, useful for tests or non-standard runtimes. Defaults to the global fetch. |
createClient throws AtlasError immediately if url or apiKey is missing.
Entries
atlas.entries(type) returns a resource scoped to one content type. When you pass
AtlasContentTypes, the data of every entry is typed as that content type's interface.
List entries
const { items, total, page, pageSize } = await atlas
.entries('article')
.list({ locale: 'en', page: 1, limit: 20, sort: 'published_at:desc' });
for (const entry of items) {
console.log(entry.slug, entry.data.title); // entry.data is typed as Article
}list(options?) returns a ListResult:
| Field | Type | Description |
|---|---|---|
items | AtlasEntry<T>[] | The entries on this page, with data already parsed. |
total | number | Total matching entries across all pages. |
page | number | The current page number. |
pageSize | number | Number of entries per page. |
List options:
| Option | Type | Description |
|---|---|---|
locale | string | Locale to resolve content into, e.g. "en" or "id". |
page | number | 1-based page number. |
limit | number | Entries per page. |
sort | string | Sort expression field:direction, e.g. "published_at:desc" or "title:asc". |
Get one entry
const post = await atlas
.entries('article')
.get('getting-started-with-headless-cms', { locale: 'id' });
if (post === null) {
// Not found (HTTP 404)
} else {
console.log(post.data.title); // Indonesian title merged over the base
}get(slug, options?) returns a single AtlasEntry<T> or null when the entry does not
exist. When you pass locale, the matching translation is merged field-by-field over the
base data: translated fields win, untranslated fields fall back to the base value.
Every entry has this shape:
| Field | Type | Description |
|---|---|---|
id | string | Entry id. |
slug | string | URL slug. |
status | string | published, draft, or archived. The Public API returns published entries. |
published_at | string | null | ISO timestamp, or null if never published. |
data | T | The parsed entry data, typed as your content type. |
Pages
Pages are layout documents made of ordered blocks plus SEO metadata — see the Pages guide for the concept.
// Lightweight rows — no blocks, good for building a sitemap or nav.
const { items } = await atlas.pages.list({ locale: 'en' });
// Full page with SEO + blocks resolved for the locale.
const home = await atlas.pages.get('home', { locale: 'en' });
if (home) {
document.title = home.seo.title ?? 'Home';
for (const block of home.blocks) {
// blocks are sorted by `position`
renderBlock(block.type, block.data);
}
}pages.list(options?)→ListResult<AtlasPageSummary>(id, slug, status — no blocks). Samelocale/page/limitoptions as entries.pages.get(slug, options?)→AtlasPage | null. Resolvesseo(base overlaid with the locale'sseo_translations) andblocks(each block'sdataoverlaid with its locale translation), and returns blocks sorted byposition.
An AtlasPage has id, slug, status, seo (AtlasPageSEO: title, description,
keywords, og_image, canonical, …), and blocks (AtlasBlock[]: id, type,
position, data).
Media
const asset = await atlas.media.get(mediaId);
if (asset) {
console.log(asset.id);
}media.get(id) returns a MediaAsset ({ id, ...rest }) or null on 404. The asset is
returned as-is from the delivery API.
Error handling
There are two distinct "not found" behaviors, by design:
get()methods (entries().get,pages.get,media.get) returnnullfor a 404, so a missing-by-slug lookup is a normal value, not an exception.- Everything else — network failures, auth errors (401/403), rate limits (429),
validation errors, and 5xx — throws an
AtlasError.
import { AtlasError } from '@latellu/atlas-sdk';
try {
const { items } = await atlas.entries('article').list();
// ...
} catch (err) {
if (err instanceof AtlasError) {
console.error(err.status, err.code, err.message); // e.g. 401, "unauthorized", "..."
} else {
throw err;
}
}AtlasError exposes:
| Property | Type | Description |
|---|---|---|
message | string | The API's message, or a transport-level description. |
status | number | undefined | HTTP status code. Absent for network/transport errors. |
code | string | undefined | The API's machine-readable code, when present. |
See Errors for the full list of API status codes and their meanings.
Escape hatch: raw
For public delivery endpoints not yet wrapped by a resource, use the low-level requester. It applies the same base URL, API key, envelope unwrapping, and error handling:
const { data, meta } = await atlas.raw.get<MyShape>('/some/public/path', { locale: 'en' });The path is relative to /api/v1/public, so '/entries' hits /api/v1/public/entries.
Current limitations
Draft preview is not wrapped yet
The SDK reads published content. Previewing a draft entry requires a preview token
from the admin/dashboard side, which the SDK does not mint or consume yet. Until then,
call the GET /public/preview endpoint directly (see the
Entry API Reference).
- Localizable fields are typed as their base value in this release. The locale merge still happens at runtime; only the type does not yet distinguish per-locale shapes.