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 NODE_ENV
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
.
ValidatorAdapter<TResult>
interface from schema-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 tells schema-env
if validation succeeded (and the typed data) or failed (with standardized error details).
3. Pass an instance of your adapter to createEnv
or createEnvAsync
using the validator
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.