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

@velora-cms/client

v0.14.0

Published

Typed client for Velora's public REST API — content, media, document types, and templates, over a pluggable transport.

Readme

@velora-cms/client

A typed client for reading and writing Velora CMS content — content, media, document types, and templates — from any code. One API surface, three ways to hold it.

npm install @velora-cms/client

Three ways to hold it

1. Inside your own Velora server. A running Velora server exposes an already-connected handle at app.velora — no URL, no API key, nothing to configure. Add a route in your project's server.js and call it directly:

app.get("/blog", async (request, reply) => {
  const posts = await app.velora.content.list();
  return reply.view("blog", { posts: posts.items });
});

2. Remotely, with an API key. Anywhere else — a serverless function, a mobile app's backend, a separate service — point createClient at the site's URL with a key minted in the admin under Settings:

import { createClient } from "@velora-cms/client";

const velora = createClient({
  url: "https://your-site.com",
  apiKey: process.env.VELORA_API_KEY,
});

const home = await velora.content.getByPath("/");

3. Remotely, without a key. If the site has turned on VELORA_PUBLIC_READS, reads work with no key at all — handy for a static frontend calling the API straight from the browser:

const velora = createClient({ url: "https://your-site.com" });
const home = await velora.content.getByPath("/");

All three return the exact same typed VeloraClient — the same methods, the same shapes, the same errors — so code written against one works against the others unchanged.

Quick start

import { createClient } from "@velora-cms/client";

const velora = createClient({ url: "https://your-site.com", apiKey });

// Fetch one item by id or by its published path.
const page = await velora.content.getById("f47a...");
const about = await velora.content.getByPath("/about");

// Locale-aware reads: fall back from a translation to a base locale.
const frPage = await velora.content.getById("f47a...", { locale: "fr", fallback: "en" });

// List and search — every list result is one page plus a lazy walker.
const results = await velora.content.list({ search: "release notes" });
for await (const item of results.all()) {
  console.log(item.path);
}

// Writes need a write-scope key.
const draft = await velora.content.create({
  documentTypeId: "...",
  parentId: null,
  slug: "hello-world",
  data: {},
});
await velora.content.update(draft.id, { data: { title: "Hello, world" } });
await velora.content.publish(draft.id);

// Delete moves content to the recycle bin — history is preserved; restore
// from the admin. A node with children rejects the call (409) unless you
// pass { cascade: true }, which deletes it and every descendant with it.
await velora.content.delete(draft.id);

Media

Reading the library works with a read key (or no key, under VELORA_PUBLIC_READS); uploading, updating, and deleting a file all need a write key, the same as the content write methods above:

// List and search, same page-plus-.all() shape as content.
const files = await velora.media.list({ search: "logo" });
for await (const file of files.all()) {
  console.log(file.filename, velora.media.urlFor(file));
}

// Upload takes the raw bytes plus a filename; visibility defaults to
// "private" when you don't pass one.
const upload = await velora.media.upload(fileBytes, {
  filename: "logo.png",
  contentType: "image/png",
  visibility: "public",
});

// Flip visibility later — v1 patches visibility only.
await velora.media.update(upload.id, { visibility: "private" });

await velora.media.delete(upload.id);

A public file's URL works for anyone; a private file's URL carries a short-lived token and 404s without it. media.urlFor(file) always returns the right one — it's the downloadUrl the server already computed for that item (fresh on every read for a private file), not a URL this package builds itself, so always call it on a file you just fetched or uploaded rather than caching the URL past a visibility change.

Binding defaults with with()

with() returns a derived client with locale/fallback bound, so you don't have to repeat them on every call — a per-call option still wins:

const fr = velora.with({ locale: "fr", fallback: "en" });
const page = await fr.content.getByPath("/about"); // reads as fr, falls back to en

Errors

A failed call rejects with one of two shapes. An HTTP-level failure — any non-2xx response, from any transport — is a VeloraApiError carrying the server's status and message. A transport-level failure — no network to reach, or (over app.velora) the in-process call itself throwing — is a plain Error instead, with no status. A content create/update rejected for failing field validation also carries fieldFailures, but check its length, not just whether it's set: a whole-record failure (an unknown document type, a tree rule) carries an empty array there and puts the real reason in message instead. Full detail: https://docs.velora-cms.com/v1/using-velora/client/#errors.

import { VeloraApiError } from "@velora-cms/client";

try {
  await velora.content.update(id, { data: { title: "" } });
} catch (error) {
  if (!(error instanceof VeloraApiError)) throw error; // network/transport failure
  if (error.fieldFailures?.length) {
    for (const failure of error.fieldFailures) {
      console.log(failure.fieldId, failure.key);
    }
  }
}

An upload past the site's configured size limit rejects the same way, with status 413 and a message that leads with a plain-language hint before the server's own:

try {
  await velora.media.upload(hugeFile, { filename: "movie.mp4" });
} catch (error) {
  if (error instanceof VeloraApiError && error.status === 413) {
    // error.message starts with "This file exceeds the site's upload limit."
  }
}

Security model

  • Read key — published content only, safe to use even in a browser. Drafts are indistinguishable from nonexistent content to any keyed caller.
  • Write key — server-side only, ever. Never ship a write key to a browser, mobile app, or any client you don't control.
  • The in-process handle (app.velora) — trusted because it is the site's own server process; no token exists for it, and none is needed.

Recommended pattern for a browser frontend: don't call the remote API directly with a write key. Instead, the browser talks to your own routes; your routes validate the request, then call app.velora.content.* (or app.velora.media.*) themselves.

Custom transports

Every method in the surface is built on a single Transport function — (request) => response. createClient uses the built-in fetch transport; createClientFromTransport accepts any transport, which is how @velora-cms/server wires up the in-process app.velora handle without a network hop, and how this package's own tests stub a server without running one.

Source & support

Source: velora-starter. Docs: https://docs.velora-cms.com. Apache-2.0.