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

@writbee/core

v0.2.1

Published

Typed client for the Writbee public content API. No dependencies, no framework.

Readme

@writbee/core

Typed client for the Writbee public content API. No dependencies, no framework, no environment assumptions — it runs in Node, Bun, Deno, a Cloudflare Worker, an Astro frontmatter block or an Express route.

If you are on Next.js, use @writbee/next instead: it wraps this package with Next's cache and a guard that keeps your secret key out of the browser.

npm install @writbee/core

Usage

import { createWritbeeClient } from "@writbee/core"

const writbee = createWritbeeClient({
  baseUrl: "https://api.writbee.com/api",
  apiKey: process.env.WRITBEE_SECRET_KEY!,
})

const posts = await writbee.getPosts()
const post = await writbee.findPost("hello-world")

| Method | Returns | | --- | --- | | getPosts(options?) | The first page of posts, newest first | | listPosts(options?) | { posts, nextCursor, prevCursor } — one page at a time | | allPosts(options?) | An async iterator over every post | | getPost(slug, options?) | One post, throwing not_found when there is none | | findPost(slug, options?) | One post, or null |

getPost and findPost both exist because both cases are real: a post page wants notFound() when the slug is gone, while a background job wants the failure to ring. Every options argument takes an AbortSignal as signal.

The API wraps its responses in data. This package unwraps it, so that convention never leaks into your code.

Paging

The list endpoint is paginated by cursor: 20 posts per page by default, 100 at most. getPosts() hands you the first page, which is the whole feed for most sites.

When it is not, listPosts exposes the cursor:

let page = await writbee.listPosts({ limit: 50 })

while (page.nextCursor) {
  page = await writbee.listPosts({ limit: 50, cursor: page.nextCursor })
}

...and allPosts walks it for you, one page in memory at a time — which is what a sitemap or a full rebuild wants:

for await (const post of writbee.allPosts()) {
  console.log(post.slug)
}

prevCursor goes back, through that same cursor option — a cursor carries the direction it points in, so there is nothing else to pass. It is null on the first page, as nextCursor is on the last, which makes both of them the condition for rendering a link:

{prevCursor && <Link href={`/blog?cursor=${prevCursor}`}>Newer</Link>}
{nextCursor && <Link href={`/blog?cursor=${nextCursor}`}>Older</Link>}

A cursor points at the last post you were handed rather than at a page number, so a post published while you are paging cannot shift the pages under you or make one show up twice. The flip side is that there are no page numbers and no total count — you move one page at a time, in either direction.

Keys

A publishable (pk_) key is restricted to the domains you list in the dashboard and is safe in a browser. A secret (sk_) key is not: it receives no CORS header, and it belongs only on a server. Passing one while window exists throws immediately, rather than shipping the key in your page source.

Errors

Every failure is a WritbeeError with a code you can branch on, so you never have to parse a message:

import { WritbeeError } from "@writbee/core"

try {
  await writbee.getPost(slug)
} catch (error) {
  if (error instanceof WritbeeError && error.code === "not_found") {
    // ...
  }
}

| code | Meaning | | --- | --- | | unauthorized | 401 — the key is missing or invalid | | forbidden | 403 — a pk_ key used from a domain outside its allow-list | | not_found | 404 — no published post with that slug | | validation_error | 422 — a limit outside 1..100, or an unreadable cursor | | server_error | 5xx | | network | the request never completed, so status is 0 | | unknown | anything else |

Replacing fetch

fetch is the one extension point, and the only one an adapter needs:

createWritbeeClient({
  baseUrl,
  apiKey,
  fetch: (url, init) => fetch(url, { ...init, cache: "force-cache" }),
})

@writbee/next uses it to add Next's revalidate and cache tags; a test can use it to answer without a network.

Notes

  • ESM only. A CommonJS caller needs await import("@writbee/core").
  • The client holds no module-level state, so two copies in one node_modules cannot interfere with each other.