schema-env: Your App's Smart Instruction Checker!
Ever tried to build a LEGO set without the right pieces or with confusing instructions? Your app can feel the same way if its "environment variables" (special settings it needs to run) are wrong!
schema-env
is like a super-helpful assistant that checks these settings for your Node.js app before it even starts. It makes sure everything is A-OK, so your app can run smoothly and reliably.
TL;DR (Too Long; Didn't Read)
schema-env
makes your app safer by checking its settings (like API keys, port numbers) against a rulebook (your Zod schema or custom adapter) right at the start. It can read settings from.env
files, special files for development/production, and even secret vaults! If something's wrong, it tells you immediately.
DX Highlights (Developer Experience Wins!)
- ✅ Peace of Mind: No more "Oops, I forgot that setting!" errors in production.
- 📖 Clear Rules: Define exactly what your app needs, in one place.
- 🤝 Team-Friendly: Everyone knows what settings are required.
- 🤖 Async & Flexible: Works with modern setups, including fetching secrets.
- 🧩 Use Your Favorite Tools: Zod is built-in, but you can plug in Joi, Yup, etc.
- 💡 Smart & Simple API: Easy to get started, powerful when you need it.
What's an "Environment Variable"? And Why Check Them?
Think of environment variables as little notes you give your app:
PORT=3000
(Tells your app which door to use for web traffic)API_KEY=supersecret123
(A secret password to talk to another service)NODE_ENV=development
(Tells your app if it's in "practice mode" or "live mode")
If these notes are missing, misspelled, or have the wrong kind of info (like text where a number should be), your app might get confused, crash, or even worse, do something unexpected!
schema-env
helps by:
- Reading a "Rulebook" (Schema): You tell
schema-env
what notes your app expects and what they should look like. - Checking the "Notes" (.env files & system): It looks at the notes you've provided.
- Giving a Thumbs Up or Down: If all notes match the rulebook, great! If not, it stops your app and tells you exactly what's wrong.
This makes your app:
- 👍 More Reliable: Fewer surprise crashes.
- 🔒 More Secure: Helps ensure secret keys are present and correctly formatted.
- 🛠️ Easier to Debug: Find configuration problems instantly.
Features - What Can This Assistant Do?
- 🔍 Checks Your Settings (Validation): Makes sure settings are the right type (text, number, URL, etc.) and follow your rules. Uses the popular Zod library by default, but you can bring your own!
- 📄 Reads
.env
Files: Automatically loads settings from.env
files – a common way to store them. - 🌳 Understands Different "Moods" (Environments): Can load different settings for "development" (
.env.development
), "production" (.env.production
), etc. - ➕ Handles Multiple Instruction Sheets: You can have a base set of settings and then override them with local ones.
- 🔗 Smart Links in Settings (Variable Expansion): Lets one setting use the value of another (e.g.,
FULL_URL = ${BASE_URL}/api
). - 🤫 Fetches Secret Settings (Asynchronous): Can get super-secret settings from secure vaults before checking everything.
- 🥇 Knows Who's Boss (Clear Precedence): If a setting is defined in multiple places,
schema-env
knows which one to use. - 🛡️ Doesn't Change Global Settings: It won't mess with your computer's main settings.
- 🗣️ Clear Error Messages: Tells you all the problems at once, not one by one.
- 🤖 AI-Powered Helper: This library was built with the help of an AI assistant!
Let's Get Started! (Basic Magic)
1. Install schema-env
and zod
(our default rulebook maker):
npm install schema-env zod
# or
yarn add schema-env zod
2. Create Your Rulebook (envSchema.ts
):
Tell schema-env
what settings your app needs.
// envSchema.ts
import { z } from "zod"; // Zod helps us make the rules!
export const envSchema = z.object({
// Rule 1: NODE_ENV should be "development" or "production". Default to "development".
NODE_ENV: z.enum(["development", "production"]).default("development"),
// Rule 2: PORT should be a number. If not given, use 3000.
PORT: z.coerce.number().default(3000),
// Rule 3: GREETING_MESSAGE must be text, and you *must* provide it!
GREETING_MESSAGE: z.string().min(1, "Oops! You forgot the greeting message!"),
});
// This creates a TypeScript type for our validated settings - super handy!
export type Env = z.infer<typeof envSchema>;
3. Write Down Your App's Settings (.env
file):
Create a file named .env
in the main folder of your project.
# .env
GREETING_MESSAGE="Hello from schema-env!"
PORT="8080"
(Notice we didn't put `NODEENV` here? Our rulebook says it defaults to "development"!)_
4. Tell schema-env
to Check Everything (in your app's main file, like index.ts
or server.ts
):
// index.ts
import { createEnv } from "schema-env";
import { envSchema, Env } from "./envSchema.js"; // Use .js for modern JavaScript modules
let settings: Env; // This will hold our correct settings
try {
// Time for the magic check!
settings = createEnv({ schema: envSchema });
console.log("✅ Hooray! All settings are correct!");
} catch (error) {
console.error("❌ Oh no! Something's wrong with the settings.");
// schema-env already printed the detailed error messages for us!
process.exit(1); // Stop the app, because settings are bad.
}
// Now you can safely use your settings!
console.log(`The app says: ${settings.GREETING_MESSAGE}`);
console.log(`Running in ${settings.NODE_ENV} mode on port ${settings.PORT}.`);
// Go ahead and start your amazing app!
// startMyApp(settings);
If you run this and your .env
file is missing GREETING_MESSAGE
or PORT
is not a number, schema-env
will tell you!
Doing More Cool Things!
Different Settings for Different "Moods" (e.g., Development vs. Production)
If you have a setting NODE_ENV
(like in our example), schema-env
is extra smart:
- If
NODE_ENV=development
, it will also try to load settings from a file named.env.development
. - If
NODE_ENV=production
, it will look for.env.production
.
Settings in these specific files will override settings from the main .env
file.
Settings That Depend on Other Settings (Variable Expansion)
Want API_URL
to be ${HOSTNAME}/api
? Easy!
First, tell schema-env
you want to do this:
settings = createEnv({
schema: envSchema, // Your usual rulebook
expandVariables: true, // Set this to true!
});
Then, in your .env
file:
HOSTNAME="http://mycoolsite.com"
API_URL="${HOSTNAME}/v1/data"
schema-env
will figure out API_URL
should be http://mycoolsite.com/v1/data
.
Using Multiple .env
Files
Sometimes you want a base set of settings and then some local ones that only you use.
settings = createEnv({
schema: envSchema,
dotEnvPath: [".env.defaults", ".env.local"], // Checks .env.defaults, then .env.local
});
Later files in the list override earlier ones. And the "mood" specific file (like .env.development
) still gets checked after all of these!
For the Pros: Super Secret Settings & Your Own Rules!
Getting Secrets from a Secure Vault (Async Magic with createEnvAsync
)
Some settings, like database passwords, are too secret for .env
files. You might keep them in a "secrets manager" (like AWS Secrets Manager, HashiCorp Vault, etc.). schema-env
can fetch these before it checks all your rules!
// mySecretFetcher.ts
import type { SecretSourceFunction } from "schema-env";
export const fetchMyDatabasePassword: SecretSourceFunction = async () => {
console.log("🤫 Asking the secret vault for the DB password...");
// In real life, you'd use a library here to talk to your secrets manager.
// We'll pretend it takes a moment:
await new Promise((resolve) => setTimeout(resolve, 50));
return {
DB_PASSWORD: "ultra-secret-password-from-vault",
};
};
Then, in your app:
// index.ts
import { createEnvAsync } from "schema-env"; // Note: createEnvAsync!
import { envSchema, Env } from "./envSchema.js"; // Your schema needs to expect DB_PASSWORD
import { fetchMyDatabasePassword } from "./mySecretFetcher.js";
async function startAppSafely() {
let settings: Env;
try {
settings = await createEnvAsync({
// await is important here!
schema: envSchema,
secretsSources: [fetchMyDatabasePassword], // Add your secret fetchers here
});
console.log("✅ Secrets fetched and all settings are correct!");
// console.log(`DB Password's first letter: ${settings.DB_PASSWORD[0]}`); // Be careful logging secrets!
} catch (error) {
console.error(
"❌ Oh no! Something went wrong with settings (maybe secrets?)."
);
process.exit(1);
}
// startMyApp(settings);
}
startAppSafely();
Don't Like Zod? Bring Your Own Rulebook Checker! (Custom Adapters)
If your team already uses another library like Joi or Yup to define rules, you can tell schema-env
to use that instead of Zod!
You'll need to create a small "adapter" that teaches schema-env
how to talk to your chosen library. This involves implementing the ValidatorAdapter
interface provided by schema-env
.
To use a custom validation library (like Joi, Yup, or your own):
- Define your environment type and schema using your chosen library.
- Implement the
ValidatorAdapter<TResult>
interface fromschema-env
. This adapter will:- Take the merged environment data as input.
- Use your chosen library to validate this data.
- Return a
ValidationResult<TResult>
object, which tellsschema-env
if validation succeeded (and the typed data) or failed (with standardized error details).
- Pass an instance of your adapter to
createEnv
orcreateEnvAsync
using thevalidator
option. You'll also need to provide the expected result type as a generic argument (e.g.,createEnv<undefined, MyCustomEnvType>({ validator: myAdapter })
).
For a complete, runnable example showing how to create and use a custom adapter with Joi, please see the examples/custom-adapter-joi/
directory in this repository. It includes:
_ A Joi schema definition (env.joi.ts
).
_ The Joi adapter implementation (joi-adapter.ts
). * An example of how to use it (index.ts
).
This demonstrates the flexibility of schema-env
in integrating with various validation workflows.
Who Wins? The Order of Settings (Precedence)
If a setting is defined in multiple places, here's who wins (highest number wins):
For createEnv
(the simpler one):
- Default values in your rulebook (schema).
- Values from your
.env
file(s) (and expanded if you turned that on). - Values from your computer's actual environment (these are like global settings).
For createEnvAsync
(the one for secrets):
- Default values in your rulebook (schema).
- Values from your
.env
file(s) (expanded if on). - Values fetched from your
secretsSources
(the secret vaults). - Values from your computer's actual environment.
Quick Look at the Main Tools (API Reference)
createEnv(options)
- Checks settings right away.
- If something is wrong, it stops and tells you (throws an error).
- Returns your perfectly validated settings.
async createEnvAsync(options)
- Can fetch secrets from vaults first.
- Then checks all settings.
- If something is wrong, it tells you by rejecting its Promise.
- If all good, its Promise gives you the validated settings.
Key Options (for both tools):
schema
: Your Zod rulebook. (Use this ORvalidator
)validator
: Your custom rulebook checker. (Use this ORschema
)dotEnvPath
: Which.env
file(s) to read. (e.g.,'./.env.custom'
or['./.env.base', './.env.local']
). Defaults to just./.env
. Can befalse
to load no.env
files.expandVariables
:true
orfalse
to turn on smart links in.env
files. (Defaults tofalse
)secretsSources
: (Only forcreateEnvAsync
) A list of functions that go fetch your secrets.
Want to Help or Have Ideas? (Contributing)
That's awesome! We'd love your help.
- New ideas, bug reports, and improvements are always welcome. Feel free to open an issue or a pull request.