npm package: @harshkhandelwalcs/env-doctor
Description: Validate that your environment variables actually work — not just that they exist
Author: harshkhandelwalcs
Latest version: 0.1.0

# env-doctor

> Validate that your environment variables **actually work** — not just that they exist.

Most tools check that `DATABASE_URL` is *present*. `env-doctor` checks that it can *connect* to the database.

```
  env-doctor  v0.1.0
  ────────────────────────────────────────────────────

  DATABASE_URL     ✓  passed (23ms)   PostgreSQL 16.2
  REDIS_URL        ✗  ECONNREFUSED — Connection refused
                      → Redis is not running. Start it: redis-server
  OPENAI_API_KEY   ✗  HTTP 401 Unauthorized — API key rejected
                      → Check your API key is correct and has not expired
  JWT_SECRET       ✓  passed          HMAC-SHA256 compatible, 64 chars
  NODE_ENV         ✓  passed          "production"
  PORT             ✗  Invalid port: "abc"
                      → Must be an integer between 1 and 65535

  ────────────────────────────────────────────────────

  ✗ 3 of 6 failed   3 passed   (487ms)
```

---

## Why?

| Tool | What it checks |
|------|---------------|
| `dotenv` | Loads `.env` into `process.env` |
| `dotenv-safe` | Checks required keys are present |
| `zod` / `envalid` | Validates types and formats |
| **`env-doctor`** | **Validates that the services actually respond** |

"Works on my machine" is usually a broken environment. `env-doctor` catches that before your app crashes in production.

---

## Install

```bash
npm install -D @harshkhandelwalcs/env-doctor
```

> **No required peer deps.** Only install what you need:
> ```bash
> npm install pg           # for check.postgres.connect()
> npm install ioredis      # for check.redis.ping()
> npm install nodemailer   # for check.smtp.connect()
> npm install mongodb      # for check.mongo.connect()
> npm install mysql2       # for check.mysql.connect()
> ```

---

## Quick Start

**1. Generate a config:**

```bash
npx env-doctor init > env.doctor.js
```

**2. Edit `env.doctor.js`:**

```js
// env.doctor.js
import { check } from '@harshkhandelwalcs/env-doctor'

export default {
  DATABASE_URL:  check.postgres.connect(),
  REDIS_URL:     check.redis.ping(),
  JWT_SECRET:    check.crypto.hmacable({ minLength: 32 }),
  API_URL:       check.http.reachable(),
  NODE_ENV:      check.format.oneOf('development', 'production', 'test'),
}
```

**3. Run it:**

```bash
npx env-doctor
```

**4. Add to your `package.json` so it runs before every dev session:**

```json
{
  "scripts": {
    "predev": "env-doctor",
    "dev": "node server.js"
  }
}
```

---

## All Checks

### `check.postgres.connect(options?)`
Connects to a PostgreSQL database and runs `SELECT version()`.

```js
DATABASE_URL: check.postgres.connect({ timeout: 5000 })
// DATABASE_URL=postgres://user:pass@localhost:5432/mydb
```

### `check.redis.ping(options?)`
Sends `PING` to Redis and expects `PONG`.

```js
REDIS_URL: check.redis.ping({ timeout: 3000 })
// REDIS_URL=redis://localhost:6379
// REDIS_URL=redis://:password@localhost:6379
```

### `check.mongo.connect(options?)`
Connects to MongoDB and runs `buildInfo`.

```js
MONGODB_URI: check.mongo.connect()
// MONGODB_URI=mongodb://localhost:27017/mydb
```

### `check.mysql.connect(options?)`
Connects to MySQL/MariaDB and runs `SELECT VERSION()`.

```js
MYSQL_URL: check.mysql.connect()
// MYSQL_URL=mysql://user:pass@localhost:3306/mydb
```

### `check.smtp.connect(options?)`
Authenticates with an SMTP server using `nodemailer.verify()`.

```js
SMTP_URL: check.smtp.connect()
// SMTP_URL=smtp://user:pass@smtp.example.com:587
// SMTP_URL=smtps://user:pass@smtp.example.com:465
```

---

### `check.http.reachable(options?)`
Makes a GET request and passes for any HTTP < 500 response (server is up).

```js
API_URL: check.http.reachable({ timeout: 5000 })
```

### `check.http.status(code, options?)`
Expects an exact HTTP status code.

```js
HEALTH_URL: check.http.status(200)
```

### `check.http.bearerAuth(url, options?)`
Validates an API key by calling a URL with it as a `Bearer` token.
Fails if the server returns `401` or `403`.

```js
OPENAI_API_KEY:  check.http.bearerAuth('https://api.openai.com/v1/models'),
GITHUB_TOKEN:    check.http.bearerAuth('https://api.github.com/user'),
STRIPE_API_KEY:  check.http.bearerAuth('https://api.stripe.com/v1/customers?limit=1'),
```

---

### `check.crypto.hmacable(options?)`
Validates a secret is long and usable as an HMAC-SHA256 key.

```js
JWT_SECRET: check.crypto.hmacable({ minLength: 32 })
```

### `check.crypto.minLength(n)`
```js
SESSION_SECRET: check.crypto.minLength(64)
```

### `check.crypto.base64()`
```js
ENCRYPTION_KEY: check.crypto.base64()
```

### `check.crypto.jwt()`
Validates structural JWT format (3 base64url parts).

```js
REFRESH_TOKEN: check.crypto.jwt()
```

---

### `check.format.oneOf(...values)`
```js
NODE_ENV: check.format.oneOf('development', 'production', 'test')
LOG_LEVEL: check.format.oneOf('debug', 'info', 'warn', 'error')
```

### `check.format.url()`
```js
SITE_URL: check.format.url()
```

### `check.format.email()`
```js
ADMIN_EMAIL: check.format.email()
```

### `check.format.port(options?)`
```js
PORT: check.format.port({ min: 1024, max: 65535 })
```

### `check.format.uuid()`
```js
TENANT_ID: check.format.uuid()
```

### `check.format.regex(pattern, hint?)`
```js
AWS_REGION: check.format.regex(/^[a-z]{2}-[a-z]+-\d$/, 'e.g. us-east-1')
```

---

## Chaining Checks

Pass an array to run checks in sequence — stops at first failure:

```js
ENCRYPTION_KEY: [
  check.crypto.notEmpty(),
  check.crypto.minLength(32),
  check.crypto.base64(),
],
```

---

## CLI Options

```
npx env-doctor [options]

Options:
  -c, --config <path>   Path to config file (default: env.doctor.js)
  -e, --env <file>      .env file to load (default: .env)
  --timeout <ms>        Timeout per check (default: 10000)
  --json                Output results as JSON
  --no-exit             Do not exit with code 1 on failure
  -V, --version         Show version
  -h, --help            Show help

Commands:
  run (default)         Run all checks
  init                  Print a starter env.doctor.js to stdout
```

---

## Programmatic API

```ts
import { run, check } from '@harshkhandelwalcs/env-doctor'

const summary = await run({
  DATABASE_URL: check.postgres.connect(),
  JWT_SECRET:   check.crypto.hmacable({ minLength: 32 }),
}, {
  envFile: '.env.test',   // which .env file to load
  timeout: 5000,          // ms per check
  exitOnFail: false,      // don't call process.exit
})

console.log(summary.passed.length)  // 1
console.log(summary.failed.length)  // 0
console.log(summary.duration)       // total ms

for (const result of summary.failed) {
  console.log(result.key, result.result.error, result.result.hint)
}
```

---

## CI / CD Integration

**GitHub Actions:**
```yaml
- name: Check environment
  run: npx env-doctor --json
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    JWT_SECRET:   ${{ secrets.JWT_SECRET }}
```

**Exit codes:**
- `0` — all checks passed
- `1` — one or more checks failed

---

## Is It Worth It?

### Backend developers (Node.js, Express, Fastify, NestJS)
**High value.** Every check is relevant:
- DB connections catch wrong credentials, wrong host, DB not created
- Redis checks catch missing password, wrong port
- SMTP checks catch firewall blocking port 587
- Secret checks catch JWT secrets that are too short

### Frontend developers (React, Vue, Vite, Next.js)
**Medium value** — mainly useful checks:
- `check.http.reachable()` for your API base URL
- `check.http.bearerAuth()` for third-party API keys (OpenAI, Stripe, etc.)
- `check.format.url()` for `VITE_API_URL`, `NEXT_PUBLIC_*` vars
- `check.format.oneOf()` for `NODE_ENV`

### Full-stack (Next.js, Nuxt, Remix, SvelteKit)
**Highest value.** You have both — API routes with DB access AND frontend env vars. Add `env-doctor` to your `predev` script and never debug a broken env again.

---

## License

MIT