Quill: a tiny config-loader nobody asked for that 2.4k people now use
A 4kb TypeScript library for loading typed configuration from env vars + JSON/YAML overlays, with a test mode that catches missing keys at build time. Open source, MIT, no telemetry, no opinions about your runtime.
- Role
- Author and maintainer
- Year
- 2023
Problem
Every Node service I worked on reinvented config loading. Same five bugs every time: env vs. file precedence, type coercion, secret redaction in logs, multi-environment overlays, and "wait, why is this undefined in production."
Outcome
2.4k weekly downloads. 38 contributors. Used in production at three companies I know of and probably more I do not.
Stack
- TypeScript
- Vitest
- tsup
- Changesets
- GitHub Actions
Why it exists
I was on my fourth Node service in three years that had a config.ts file with the same hand-rolled process.env.X || defaults.x pattern, the same forgotten type coercion bug for booleans (process.env.FEATURE_FLAG === 'true' is the canonical wrong solution), and the same on-call incident shape: a missing env var that nobody caught because the schema lived in three engineers’ heads.
Existing libraries either did too much (full DI containers, classes for everything) or too little (just env-var parsing, no overlay). I wanted a small tool that did one job: take a typed schema, validate the environment at startup, and fail fast with a useful error message.
What it does
import { defineConfig } from 'quill';
export const config = defineConfig({
port: { type: 'number', default: 3000 },
databaseUrl: { type: 'string', secret: true, required: true },
featureFlags: {
enableNewCheckout: { type: 'boolean', default: false },
},
});
That’s the API. Everything else is a side effect of three properties:
- Schema-driven type coercion.
'true','1', and'yes'all becometrue.'false','0', and'no'all becomefalse. Anything else throws at startup, not at the first request that reads it. - Secrets are tagged. Anything marked
secret: trueis redacted inconsole.log(config), in error messages, and in the structured logger output. You don’t have to remember. - A
quill checkCLI. Reads your schema, reads your.env, tells you what’s missing, what’s typed wrong, and what’s set but unused. Runs in CI. Catches drift before it becomes a 3am page.
How it grew
I shipped 1.0 on a Sunday. Posted it once. Forgot about it for two months. When I came back, four people had filed issues, two of which were real. I fixed them, shipped 1.1, and that was the entire growth strategy for the first year.
The only thing I did intentionally was write good error messages. Most of the early adopters told me — unprompted — that they tried it because the README’s “what happens when X is missing” example was funny, and stayed because the actual error matched the example.
What I learned about maintaining OSS
- Most issues are not bugs; they are mismatched expectations. I now respond to every issue with a question first, not an answer.
- Saying no is a feature. Quill has had three “could you also support…” asks turn into long thoughtful threads where the answer was no, and the asker said “actually, you’re right, I’ll keep that out of my code too.”
- The contributors are smarter than me. Someone added Deno support in a 200-line PR I would have spent a weekend bikeshedding the interface for. I merged it.
Concrete outcome
- 2.4k weekly downloads on npm, steady for the past 9 months.
- 38 unique contributors across 87 merged PRs.
- Used in production at three companies who told me directly. Probably more I haven’t heard from.
- One CVE filed and patched within 14 hours, against a transitive dependency. No reports of exploitation.
- Zero scope creep. It still does exactly what 1.0 did, just better.