schema-env
Version:
Type-safe environment variable validation for Node.js using Zod schemas or custom adapters. Load .env files, expand variables, fetch async secrets, and validate process.env at startup.
325 lines (233 loc) • 13.9 kB
Markdown
# schema-env: Your App's Smart Instruction Checker!
<p align="center">
<a href="https://www.npmjs.com/package/schema-env">
<img src="https://img.shields.io/npm/v/schema-env.svg" alt="npm version" />
</a>
<a href="https://img.shields.io/npm/dm/schema-env.svg">
<img src="https://img.shields.io/npm/dm/schema-env.svg" alt="Downloads per month" />
</a>
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT" />
</a>
<a href="https://app.codecov.io/gh/devvictrix/schema-env">
<img src="https://img.shields.io/badge/coverage-95.9%25-brightgreen.svg?style=flat-square" alt="Test Coverage" />
</a>
<img src="https://img.shields.io/badge/types-TypeScript-blue.svg" alt="TypeScript Support" />
<a href="https://github.com/devvictrix/schema-env/blob/main/ai/AI_INSTRUCTIONS.md">
<img src="https://img.shields.io/badge/Developed%20with-AI%20Assistance-blueviolet?style=flat-square" alt="Developed with AI Assistance" />
</a>
</p>
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:**
1. **Reading a "Rulebook" (Schema):** You tell `schema-env` what notes your app expects and what they should look like.
2. **Checking the "Notes" (.env files & system):** It looks at the notes you've provided.
3. **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](https://zod.dev/) 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):**
```bash
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.
```typescript
// 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.
```ini
# .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`):**
```typescript
// 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:
```typescript
settings = createEnv({
schema: envSchema, // Your usual rulebook
expandVariables: true, // Set this to true!
});
```
Then, in your `.env` file:
```ini
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.
```typescript
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!
```typescript
// 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:
```typescript
// 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`.
<details>
<summary><strong>Advanced: More on Custom Validation Adapters</strong></summary>
To use a custom validation library (like Joi, Yup, or your own):
1. **Define your environment type and schema** using your chosen library.
2. **Implement the `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/`](https://github.com/devvictrix/schema-env/tree/main/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.
</details>
## 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):**
1. Default values in your rulebook (schema).
2. Values from your `.env` file(s) (and expanded if you turned that on).
3. Values from your computer's actual environment (these are like global settings).
**For `createEnvAsync` (the one for secrets):**
1. Default values in your rulebook (schema).
2. Values from your `.env` file(s) (expanded if on).
3. Values fetched from your `secretsSources` (the secret vaults).
4. 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 OR `validator`)
- `validator`: Your custom rulebook checker. (Use this OR `schema`)
- `dotEnvPath`: Which `.env` file(s) to read. (e.g., `'./.env.custom'` or `['./.env.base', './.env.local']`). Defaults to just `./.env`. Can be `false` to load no `.env` files.
- `expandVariables`: `true` or `false` to turn on smart links in `.env` files. (Defaults to `false`)
- `secretsSources`: (Only for `createEnvAsync`) 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.
## License
[MIT](LICENSE) © [devvictrix (AI Assisted)](https://github.com/devvictrix)