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

@jayalfredprufrock/confetti

v0.2.2

Published

Describe your backend API as a contract and get a fully-typed http client without a compile step.

Readme


Why

  • Ultra type-safe. Paths and return values are fully inferred — get('nested.prop') gives you autocomplete and the correct type, no casting.
  • Environments, side-by-side. Staging and prod values live next to each other in the config, not scattered across files.
  • Source of truth. Making something configurable only requires touching one file. Start with a default across environments and easily override per environment later.
  • Consumers resolve special values. confetti tracks what a value is (an env var, a remote secret, a default) and hands that metadata to the consumer — it doesn't dial AWS for you.

Install

npm add @jayalfredprufrock/confetti

Basic usage

import { makeConfig, DEFAULT, ENV, DATA, TYPE } from "@jayalfredprufrock/confetti";

const config = makeConfig({
  appName: "my-app",
  port: 3000,

  feature: {
    enabled: true,
    limit: 50,
  },

  apiUrl: {
    [DEFAULT]: "http://localhost:3000",
    staging: "https://api.staging.example.com",
    prod: "https://api.example.com",
  },

  dbPassword: {
    [ENV]: "DB_PASSWORD",
    [DATA]: "db/password",
    [DEFAULT]: "",
  },

  maxConnections: {
    [TYPE]: "number",
    [ENV]: "MAX_CONNECTIONS",
    [DEFAULT]: 10,
  },
});

// Pick an environment, then read values.
const cfg = config("prod");

// paths and types fully inferred
cfg.get("appName"); // => 'my-app'
cfg.get("apiUrl"); // => 'https://api.example.com'
cfg.get("port"); // => 3000  (typed as number)

Factory/Function pattern

Use the factory form when it's more convenient to produce multi-environment default values based on a naming convention:

const config = makeConfig((env: string) => ({
  serviceName: `my-app-${env}`,
  dbPassword: {
    [ENV]: "DB_PASSWORD",
    [DATA]: `${env}/password`,
    [DEFAULT]: "",
  },
}));

Individual values can also be functions, which also provides an escape hatch if you need to provide an object as a config leaf value.

const config = makeConfig({
  serviceName: (env) => `my-app-${env}`,
  objValue: (env) => ({ enabled: true, value: 42 }),
});

Sync reads with get

get(path) is synchronous. Per-env values are selected by precedence:

  1. explicit value (no multi-env object used)
  2. process.env[ENV] if set (coerced per [TYPE] — see below)
  3. the explicit per-env value (cfg.get('apiUrl') in prod returns the prod value)
  4. [DEFAULT], if present
  5. otherwise, throw
cfg.get("apiUrl"); // 'https://api.example.com'
cfg.get("feature.enabled"); // true  (typed as boolean)

// Entire subtrees are fine too — everything resolves synchronously.
cfg.get("feature"); // { enabled: true, limit: 50 }

// Omit the path to get the entire resolved config.
cfg.get(); // { appName, port, feature: {...}, apiUrl, ... }

If a leaf cannot be resolved syncronously, get throws. See resolve below for handling async configuration.

Declaring leaf types with [TYPE]

External values (env vars, fetched secrets) are strings by nature but your config likely wants them typed. Use [TYPE] to declare the runtime shape and drive both TypeScript inference and automatic coercion.

const config = makeConfig({
  port: { [TYPE]: "number", [ENV]: "PORT", [DEFAULT]: 3000 },
  featureFlag: { [TYPE]: "boolean", [DATA]: "flags/checkout-v2" },
  allowedOrigins: { [TYPE]: "string[]", [ENV]: "ALLOWED_ORIGINS", [DEFAULT]: [] },
});

const cfg = config("prod");
cfg.get("port"); // typed as number — env var "8080" coerced to 8080

Supported tags: "string" | "number" | "boolean" | "string[]" | "number[]" | "boolean[]".

Coercion rules (env vars and string fetcher returns):

| Tag | Expected raw | | --------- | --------------------------------------------------- | | string | as-is | | number | Number(raw); empty or NaN throws | | boolean | exactly "true" or "false"; anything else throws | | T[] | JSON.parse + array check + element type check |

[TYPE] also constrains [DEFAULT] and per-env values at compile time — { [TYPE]: "number", [DEFAULT]: "nope" } is a type error.

When [ENV] or [DATA] are present without [TYPE]: values are required to be strings (both [DEFAULT] and per-env overrides). The fetcher must also return a string. If you need a non-string here, add [TYPE].

Async reads with resolve

resolve(path, fetcher) hands off to your code whenever a leaf can't be satisfied synchronously. You decide how to resolve it — read AWS Secrets Manager, call Vault, hit Parameter Store, whatever.

const password = await cfg.resolve("dbPassword", async (ctx) => {
  // ctx = { env: 'prod', envVar: 'DB_PASSWORD', data: 'prod/db/password', default: '' }
  const secret = await secretsClient.getSecretValue({ SecretId: ctx.data });
  return secret.SecretString;
});

// Omit the path to resolve the entire config — fetcher is invoked per leaf that needs it.
const fullConfig = await cfg.resolve(async (ctx) => {
  /* ... */
});

Rules:

  • Explicit values and env overrides still win — the fetcher is only called when there's no sync value.
  • Return undefined from the fetcher to fall back to [DEFAULT].
  • If [TYPE] is declared, a string return is coerced; a non-string must match [TYPE] exactly or it throws.
  • Resolving a subtree calls the fetcher once per leaf that needs it; static leaves pass through untouched.

Walking the config with entries

entries() returns a lazy iterator that yields [path, entry] for every leaf. Use it to drive downstream tooling — synthesize IaC secret resources, check for unset values in CI, etc.

for (const [path, entry] of cfg.entries()) {
  if (entry.envVar) {
    console.log(`${path} ← process.env.${entry.envVar}`);
  }
  if (entry.data) {
    console.log(`${path} ← secret @ ${entry.data}`);
  }
}

Each entry has the shape:

{ path: string; value?: unknown; default?: unknown; envVar?: string; data?: unknown; type?: TypeTag }

value is present if a syncronous value is available — useful for distinguishing "already known" from "needs fetching" without resolving anything.

Pass a subtree path to scope iteration to that part of the config. Paths are typed — only subtree paths compile, leaves are rejected.

for (const [path, entry] of cfg.entries("feature")) {
  // path is e.g. "feature.enabled", "feature.limit"
}

Real-world use cases

Generate a required env var list for your deploy pipeline

const required = Array.from(cfg.entries())
  .filter(([, entry]) => entry.envVar && entry.value === undefined)
  .map(([, entry]) => entry.envVar!);

Synthesize Terraform / Pulumi secret resources

for (const [path, entry] of cfg.entries()) {
  if (!entry.data) continue;
  new aws.secretsmanager.Secret(path, { name: entry.data as string });
}

Fetch everything you need at boot

const secrets = await cfg.resolve("secrets", async ({ data }) => {
  return await secretsClient
    .getSecretValue({ SecretId: data as string })
    .then((r) => r.SecretString);
});

Validate config is ready before starting

for (const [path, entry] of cfg.entries()) {
  if (entry.value === undefined && entry.default === undefined && !entry.data) {
    throw new Error(`Config '${path}' has no resolvable value.`);
  }
}

API

| Method | Signature | Notes | | ------------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | makeConfig | (config \| (env) => config) => Confetti | Accepts a config object or factory fn. | | confetti(env) | (env: string) => Accessor | Binds an environment. | | accessor.get | (path?) => value | Sync. Path is optional — omit to get the full resolved config. Throws if async resolution is required. | | accessor.resolve | (path?, fetcher) => Promise<value> | Async. Path is optional — omit to resolve the full config. Fetcher invoked per leaf that needs it. | | accessor.entries | (startPath?) => IterableIterator<[string, Entry]> | Lazy iterator of every leaf with its metadata; optionally scoped to a subtree. |

License

MIT