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

composepdf

v0.1.0

Published

Typed client for the Compose PDF API — render server-side templates to PDF, with the payload types generated from each template's own data contract.

Readme

composepdf

Typed client for the Compose PDF API.

Design the template in the browser. Send data. Get a print-ready PDF back — and get the payload's TypeScript types from the template itself, so the shape you send is checked at compile time rather than discovered from a 400.

npm i composepdf

No dependencies. Works in Node 18+, Bun, Deno, Cloudflare Workers and any browser-like runtime with fetch.

Render

import { writeFile } from 'node:fs/promises';
import { createClient } from 'composepdf';

const composepdf = createClient({ apiKey: process.env.COMPOSEPDF_API_KEY! });

const { bytes } = await composepdf.render('cnv_…', {
  data: {
    customer: { name: 'Acme Inc.' },
    items: [
      { name: 'Design work', price: 1200 },
      { name: 'Hosting', price: 90 },
    ],
  },
});

await writeFile('invoice.pdf', bytes);

The template id comes from composepdf.listTemplates() or from the studio. Rendering always uses the published version, never a work-in-progress draft.

Types from your templates

Every published template declares what data it reads, and the API serves that declaration. composepdf types turns it into one file:

npx composepdf types --out src/composepdf-types.ts
import { createClient } from 'composepdf';
import type { Templates } from './composepdf-types';

const composepdf = createClient<Templates>({ apiKey: process.env.COMPOSEPDF_API_KEY! });

await composepdf.render('cnv_…', { data: { customer: { name: 'Acme Inc.' } } });
//                       ^ only your template ids   ^ checked against that template

Nothing is hand-maintained: the interface is the template's own data contract, so republishing a template with a new field and regenerating is the whole migration. Commit the generated file — it has no timestamp, so an unchanged workspace regenerates to no diff.

The command reads COMPOSEPDF_API_KEY from the environment (--key overrides), and writes composepdf-types.ts unless given --out.

Errors say which path was wrong

A payload the template cannot accept is rejected before the render starts, naming every offending path:

import { ComposePdfError } from 'composepdf';

try {
  await composepdf.render('cnv_…', { data });
} catch (e) {
  if (e instanceof ComposePdfError && e.code === 'data_contract_violation') {
    for (const issue of e.issues) {
      console.error(issue.path, issue.message); // items[3].price  expected number
    }
  }
}

ComposePdfError also carries status, requestId (quote it to support) and retryAfter for 429/503.

Many records, one file

const { bytes, records } = await composepdf.render('cnv_…', {
  dataList: [invoiceA, invoiceB, invoiceC],
});

One document per record, collated into a single PDF in order. data and dataList are mutually exclusive.

Long renders

const job = await composepdf.renderAsync('cnv_…', { dataList: manyRecords });

let render = await composepdf.getRender(job.id);
while (!render.done) {
  await new Promise((r) => setTimeout(r, 1000));
  render = await composepdf.getRender(job.id);
}

const bytes = await composepdf.downloadRender(job.id);

An accepted job always keeps its bytes. A synchronous render keeps them only with options: { store: true }.

Retries that cannot double-charge

await composepdf.render('cnv_…', { data }, { idempotencyKey: `order-${orderId}` });

Presenting the same key again replays the stored PDF without rendering or billing a second time.

API

| | | |---|---| | listTemplates() | Published templates this key can render | | render(id, request?, options?) | The PDF, as bytes | | renderAsync(id, request?, options?) | A job id, for renders past the synchronous budget | | getSchema(id, options?) | JSON Schema, the paths the template reads, and a sample payload | | getSchemaSource(id, 'ts' \| 'zod', options?) | The same contract as source | | listRenders(params?) / getRender(id) / downloadRender(id) | Render history and stored PDFs | | getUsage() | This month's usage — read it to stop before a 402 | | health() | Open route; answers without a key |

The surface is exactly the published OpenAPI document (GET /v1/openapi.json). Full reference: composepdf.com/docs.

Keep the key server-side

An API key can render and download every document in its scope. Do not ship it to a browser. If you need a live preview inside your own product, use the embedded preview — it takes a scoped embed token instead, and renders in the visitor's browser.

License

MIT