npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kailua-packages/headless-sdk

v0.3.0

Published

TypeScript-SDK fuer die Kailua Headless-CMS REST-API (apps/headless).

Readme

@kailua-packages/headless-sdk

Typsicheres TypeScript-SDK fuer die Kailua Headless-CMS REST-API (apps/headless).

Spiegelt die API unter /headless/api/v1 mit Bearer-Token-Authentifizierung (Authorization: Bearer hcms_live_...).

Installation

Workspace-Paket – im Monorepo via Workspace-Referenz:

{
  "dependencies": {
    "@kailua-packages/headless-sdk": "*"
  }
}

Schnellstart

import { createClient } from "@kailua-packages/headless-sdk"

const hcms = createClient({
  baseUrl: "https://acme.kailua-packages.ch/headless",
  token: process.env.HCMS_TOKEN!,
})

// Collections + Schema laden
const collections = await hcms.collections.list()

// Eintraege einer Collection abfragen (mit Generic fuer data)
const posts = await hcms.collection("blog").find<{ title: string; body: string }>({
  status: "published",
  locale: "de",
  sort: "title",
  order: "asc",
  page: 1,
  limit: 20,
  populate: ["author"],
  fields: ["title", "body"],
  filters: {
    featured: { op: "eq", value: true },
    price: { op: "gte", value: 10 },
    tag: { op: "in", value: ["news", "release"] },
  },
})

console.log(posts.data, posts.meta) // { data: Entry[], meta: { page, limit, total } }

API-Oberfläche

| Methode | Beschreibung | | --- | --- | | collections.list() | Alle API-fähigen Collections inkl. JSON-Schema | | collection(slug).find<T>(query?) | Eintragsliste mit Filtern/Sortierung/Pagination → { data, meta } | | collection(slug).findOne<T>(id, query?) | Einzelner Eintrag (populate/fields) | | collection(slug).create<T>(input) | Eintrag anlegen (write-Scope) | | collection(slug).update<T>(id, input) | Eintrag aktualisieren (write-Scope) | | collection(slug).delete(id) | Eintrag soft-loeschen (write-Scope) | | media.list(query?) | Medien-Assets → { data, meta } | | pages.list(opts?) | Page-Liste (ohne blocks) → { data, meta } | | pages.byPath(path, opts?) | Eine Page inkl. Block-Baum per Pfad → { page, collectionTags } \| null | | menus.list() | Alle Menues (key, name) | | menus.get(key) | Ein Menue inkl. verschachtelter Items → MenuData \| null | | theme.get() | Default-Theme inkl. Design-Tokens | | blocks.list() | Block-Palette (Built-ins ∪ DB-Definitionen) | | search<T>(q, opts?) | Volltextsuche ueber veroeffentlichte Eintraege | | openapi() | OpenAPI-Dokument der API |

Query-Builder

interface QueryParams {
  status?: string
  locale?: string
  environment?: string
  sort?: string
  order?: "asc" | "desc"
  page?: number
  limit?: number
  populate?: string[]
  fields?: string[]
  filters?: Record<string, { op: FilterOperator; value: unknown }>
}

Filter-Operatoren: eq, neq, gt, gte, lt, lte, contains, startsWith, in.

Abbildung auf die API-Query-Syntax:

  • { title: { op: "eq", value: "Foo" } }?title=Foo
  • { price: { op: "gte", value: 10 } }?price[gte]=10
  • { tag: { op: "in", value: ["a", "b"] } }?tag[in]=a,b

Filter — vollstaendige Beispiele (alle Operatoren)

const results = await hcms.collection("shop").find({
  filters: {
    // Gleichheit / Ungleichheit
    featured: { op: "eq", value: true },            // ?featured=true
    category: { op: "neq", value: "intern" },       // ?category[neq]=intern

    // Numerische Vergleiche (auch fuer date/datetime-Felder als ISO-String)
    price: { op: "gte", value: 10 },                // ?price[gte]=10
    stock: { op: "gt", value: 0 },                  // ?stock[gt]=0
    weight: { op: "lt", value: 5 },                 // ?weight[lt]=5
    rating: { op: "lte", value: 4.5 },              // ?rating[lte]=4.5

    // String-Operatoren
    title: { op: "contains", value: "CMS" },        // ?title[contains]=CMS
    slug: { op: "startsWith", value: "blog-" },     // ?slug[startsWith]=blog-

    // Listen-Match
    tag: { op: "in", value: ["news", "release"] },  // ?tag[in]=news,release
  },
})

Alle Filter im filters-Objekt werden UND-verknuepft. Fuer ODER-Verknuepfungen und verschachtelte Gruppen bietet die API zusaetzlich einen JSON-Filter (?filter={"$or":[...]} mit $and/$or/$eq/$ne/$gt/$gte/$lt/$lte/$contains/ $startsWith/$in/$notIn/$null, max. Tiefe 3) — siehe apps/headless/FRONTEND_INTEGRATION.md, Kapitel 9. Eine typisierte filter-Option im SDK ist geplant; bis dahin den JSON-Filter bei Bedarf per eigenem fetch gegen die API nutzen.

populate — Relationen & Media einbetten

populate loest Relationsfelder serverseitig auf. Punktpfade laden verschachtelt (maximal Tiefe 3); Media-Felder (image, file, gallery) im Pfad werden als vollstaendige Media-Objekte inkl. variants eingebettet:

const posts = await hcms.collection("blog").find<{
  title: string
  // nach populate: eingebetteter Author-Entry statt ID
  author: { id: string; data: { name: string; avatar: unknown } }
}>({
  populate: ["author", "author.avatar"], // → ?populate=author,author.avatar
  limit: 10,
})

// posts.data[0].data.author.data.avatar →
// { id, url, alt, width, height, focalX, focalY, variants: [{ label: "thumb", url, ... }] }

Pages, Menus & Theme

The Baukasten (page builder) delivery endpoints let a frontend render whole pages, navigation menus, the design theme and the available block palette — without hand-rolling fetch calls.

Pages

// List pages (lightweight — no `blocks`)
const pages = await hcms.pages.list({ locale: "de" })
// → { data: PageSummary[], meta: { page, limit, total } }

// Load one page including its block tree, by path
const result = await hcms.pages.byPath("/about", { locale: "de" })
if (result) {
  const { page, collectionTags } = result
  console.log(page.title, page.blocks)
} else {
  // 404 → byPath returns null (no throw)
}

byPath returns null when the API responds with 404 (page not found), so you can branch on null instead of catching an error.

The block tree is recursive. Each PageBlock carries its raw field values in data; blocks with slots expose their children under children (slot key → child blocks):

function render(blocks: PageBlock[]) {
  for (const block of blocks) {
    // block.type, block.data, block.resolved?.<field>
    for (const [slot, kids] of Object.entries(block.children ?? {})) {
      render(kids) // recurse into named slots
    }
  }
}

Data resolution (resolve)

Server-side data resolution is on by default: data-driven blocks get their referenced entries attached additively under block.resolved.<field> (either a single entry, an entries list, or a per-field error). data is never mutated, so this stays backwards-compatible.

Pass resolve: false to get the raw blocks (sends ?resolve=none) — useful if you resolve references yourself or want a maximally cacheable response:

const raw = await hcms.pages.byPath("/about", { resolve: false })
// raw.page.blocks[i].resolved is absent

When resolution is active and the page references collections, the API attaches the referenced collection cache-tags. The SDK surfaces them as result.collectionTags (hcms:<org>:collection:<slug>), which you can feed into your own tag-based fetch cache for fine-grained invalidation. For pages without data blocks (or with resolve: false) the array is empty.

Draft mode (previewToken)

To render a draft of a specific page (e.g. a preview link from the editor), pass its previewToken. It is forwarded as ?previewToken= and does not replace the Bearer token — it only unlocks the draft read for exactly that one page:

const preview = await hcms.pages.byPath("/about", {
  locale: "de",
  previewToken: params.get("previewToken") ?? undefined,
})

Menus

// All menus (key + name only)
const menus = await hcms.menus.list() // MenuSummary[]

// One menu including its nested, visible items
const main = await hcms.menus.get("main")
if (main) {
  // main.items is a tree; each item has children: MenuItem[]
  walk(main.items)
}

menus.get returns null on 404. Only visible items are returned, sorted by order.

Theme

const theme = await hcms.theme.get()
// theme.tokens is Record<group, Record<token, value>>, e.g.
// { colors: { primary: "#0af" }, spacing: { md: "16px" } }
const primary = theme.tokens.colors?.primary

When the organization has no default theme, id/name are null, isDefault is false and tokens is an empty object.

Blocks

const palette = await hcms.blocks.list() // BlockDefinitionInfo[]
// { slug, name, description, icon, category, source, allowsChildren,
//   slots, fields }

The palette merges built-in blocks with DB-defined blocks (DB wins on slug). description is only set for DB definitions; built-ins report null.

Fehler-Handling

Jede non-2xx-Antwort wirft einen HcmsApiError mit status (HTTP-Status), code (API-Fehlercode) und message:

import { HcmsApiError } from "@kailua-packages/headless-sdk"

try {
  await hcms.collection("blog").find()
} catch (err) {
  if (err instanceof HcmsApiError) {
    console.error(err.status, err.code, err.message) // z.B. 403 FORBIDDEN ...
  }
}

Fehlercodes der API: UNAUTHORIZED, TOKEN_INACTIVE, TOKEN_EXPIRED (alle 401), FORBIDDEN (403, fehlender Scope oder Collection-Scope), NOT_FOUND (404), BAD_REQUEST (400) und RATE_LIMIT_EXCEEDED (429).

Rate-Limits

Ist das Token-Rate-Limit ueberschritten, wirft das SDK einen HcmsApiError mit status: 429 und code: "RATE_LIMIT_EXCEEDED". Die API sendet dazu einen Retry-After-Header (Sekunden). Beispiel fuer einen einfachen Retry:

async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn()
  } catch (err) {
    if (err instanceof HcmsApiError && err.status === 429) {
      // Konservativ warten und einmal wiederholen. (Ein retryAfterSeconds-Feld
      // direkt auf HcmsApiError ist geplant.)
      await new Promise((r) => setTimeout(r, 5_000))
      return fn()
    }
    throw err
  }
}

const posts = await withRetry(() => hcms.collection("blog").find())

OpenAPI & Typgenerierung

hcms.openapi() liefert die OpenAPI-3.1-Spec der Organisation — nutzbar fuer Codegeneratoren wie openapi-typescript (siehe apps/headless/FRONTEND_INTEGRATION.md, Kapitel 8):

const spec = await hcms.openapi() // entspricht GET /openapi.json

Geplant (Phase 4): hcms types generate in der CLI (@kailua-packages/headless-cli) erzeugt direkt aus dem Collection-Schema eine Typendatei (cms/types.generated.ts) mit Interfaces pro Collection — inklusive Literal-Unions fuer select-Felder und discriminated unions fuer flexible_content. Der Client wird dann per Generic typisiert (createClient<HcmsCollectionMap>), bleibt aber ohne Typparameter rueckwaertskompatibel.

Eigenes fetch

createClient({ baseUrl, token, fetch }) akzeptiert ein optionales fetch (z.B. fuer Tests oder Node < 18). Standardmaessig wird das globale fetch verwendet.

Tests

npm run test --workspace=@kailua-packages/headless-sdk