Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bini-env

npm npm downloads license vite hono typescript node

Zero-config environment variable system + Vite plugin for Bini.js

Powered by Hono — reads env vars from the Hono request context so variables are always resolved from the correct runtime binding, whether you're on Node.js, Bun, Deno, Vercel Edge, Netlify Edge, or Cloudflare Workers.


Features

  • Hono-nativegetEnv(c, key) / requireEnv(c, key) read directly from the Hono request context
  • Universal runtime — CF Workers, Node, Bun, Deno, Vercel Edge, Netlify Edge — Hono's adapter handles all of it
  • Zero dotenv — no .env parsing at runtime; vars come from the host platform
  • Vite-native dev.env loading, file watching, and server restarts are handled by Vite itself
  • Dev banner — prints ß Bini.js (dev/preview) and lists detected .env files on server start
  • Prefix controlBINI_ and VITE_ included out of the box, extend with your own
  • Tree-shakeable — nothing lands in the client bundle that shouldn't
  • Typed — full TypeScript support with an exported HonoContext type

Installation

pnpm add bini-env hono
# or
npm install bini-env hono
# or
yarn add bini-env hono

hono is a required peer dependency.


Quick Start

1. Register the Vite plugin

// vite.config.ts
import { defineConfig } from 'vite'
import { biniEnv } from 'bini-env'

export default defineConfig({
  plugins: [biniEnv()]
})

2. Read env vars in your Hono handlers

// src/app/api/hello.ts
import { Hono } from 'hono'
import { getEnv, requireEnv } from 'bini-env'

const app = new Hono()

app.post('/hello', async (c) => {
  try {
    const ctx = c as any

    // requireEnv throws if the var is missing — fail fast on required config
    const apiKey  = requireEnv(ctx, 'MY_API_KEY')

    // getEnv returns undefined if missing — use ?? to provide a default
    const appName = getEnv(ctx, 'APP_NAME')     ?? 'World'
    const timeout = parseInt(getEnv(ctx, 'TIMEOUT_MS') ?? '5000')

    return c.json({ message: `Hello, ${appName}!` })

  } catch (error: any) {
    if (error.message?.includes('[bini-env] Missing required')) {
      return c.json({ error: error.message }, 500)
    }
    return c.json({ error: 'Something went wrong.' }, 500)
  }
})

export default app

Usage Pattern

Always pass c explicitly. Cast it once at the top of the handler, then use ctx throughout.

app.post('/example', async (c) => {
  try {
    const ctx = c as any

    // Required vars — handler throws immediately if missing
    const dbUrl  = requireEnv(ctx, 'DATABASE_URL')
    const apiKey = requireEnv(ctx, 'STRIPE_SECRET_KEY')

    // Optional vars — fall back to sensible defaults
    const model      = getEnv(ctx, 'AI_MODEL')    ?? 'gpt-4o'
    const region     = getEnv(ctx, 'AWS_REGION')  ?? 'us-east-1'
    const maxRetries = parseInt(getEnv(ctx, 'MAX_RETRIES') ?? '3')
    const debug      = getEnv(ctx, 'DEBUG_MODE')  === 'true'

    // ... rest of handler

  } catch (error: any) {
    if (error.message?.includes('[bini-env] Missing required')) {
      return c.json({ error: error.message }, 500)
    }
    return c.json({ error: 'Something went wrong.' }, 500)
  }
})

The pattern in three steps:

  1. const ctx = c as any — once, at the top of the handler
  2. requireEnv(ctx, 'KEY') — for vars the handler cannot run without
  3. getEnv(ctx, 'KEY') ?? 'default' — for optional vars with sensible defaults

Environment Prefixes

BINI_ — server-side vars

BINI_ is bini-env's own prefix. Variables are not exposed to import.meta.env by default, making it the right choice for server-side config and secrets.

To additionally expose a BINI_ var to the client, add it to envPrefix explicitly:

biniEnv({ envPrefix: ['BINI_', 'VITE_', 'MY_PUBLIC_'] })

VITE_ — public client vars

VITE_ is Vite's built-in prefix. Any var starting with VITE_ is bundled into your client-side JavaScript and is publicly visible.

VITE_ANALYTICS_ID=UA-XXXX             # bundled into client JS — public
VITE_API_URL=https://api.example.com  # bundled into client JS — public
// accessible anywhere, including the browser
import.meta.env.VITE_ANALYTICS_ID

Choosing the right prefix

# ✓ use VITE_ for anything the browser needs
VITE_API_URL=https://api.example.com

# ✓ use BINI_ for server-side config
BINI_FEATURE_FLAG=true

# ✓ no prefix for secrets — read via getEnv(ctx, key) only
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...

Avoid defining the same variable under two different prefixes — your code will silently read whichever one it finds first.

Prefix summary

Prefix Exposed to browser Use for
BINI_ No (unless added to envPrefix) Server-side config
VITE_ Yes, always Public client config
No prefix No Secrets — server only

Platform Support

getEnv and requireEnv delegate to Hono's env(c) adapter, which reads from the correct source on every supported platform automatically. Your code never changes regardless of where it deploys.

Platform Runtime Env source How Hono reads it
Node.js Node System env / platform dashboard process.env
Bun Bun System env / platform dashboard process.env
Vercel Edge V8 isolate Project settings → Env Vars process.env
Netlify Edge Deno Site settings → Env Vars Deno.env.get()
Cloudflare Workers V8 isolate wrangler.toml / dashboard CF bindings via c.env
Deno Deploy Deno Project settings → Env Vars Deno.env.get()

Cloudflare note: Secrets set via wrangler secret put are only available inside the fetch handler via c.env — this is a Workers architecture constraint. getEnv(ctx, key) reads them correctly as long as you pass c.


How It Works

The plugin — biniEnv()

Does two things: tells Vite which env prefixes to expose to import.meta.env, and prints the ß Bini.js banner with detected .env files when the dev or preview server starts.

// what biniEnv() does internally
config() {
  return { envPrefix: ['BINI_', 'VITE_', ...yourExtras] }
}

On server start you will see:

  ß Bini.js (dev)
  ➜  Environments: .env.local, .env
  ➜  Local:   http://localhost:3000/
  ➜  Network: http://192.168.1.7:3000/

Vite handles everything else natively: loading .env, .env.local, .env.[mode] files during dev, watching and restarting on change, injecting prefixed vars into import.meta.env at build time, and HMR when env files change. bini-env does not reimplement any of that.

The env functions — getEnv / requireEnv

Both functions read exclusively from env(c) via hono/adapter — the Hono request context. There are no process.env fallbacks. Every read is request-scoped and resolved by Hono's adapter for the current platform.

dotenv is never used. In production, vars are set in your hosting platform's environment config.


API Reference

getEnv(c, key)

Returns string | undefined. Reads from the Hono request context.

app.get('/config', async (c) => {
  const ctx = c as any

  const region   = getEnv(ctx, 'AWS_REGION') ?? 'us-east-1'
  const logLevel = getEnv(ctx, 'LOG_LEVEL')  ?? 'info'
  const debug    = getEnv(ctx, 'DEBUG_MODE') === 'true'

  return c.json({ region, logLevel, debug })
})

requireEnv(c, key)

Returns string. Throws immediately if the variable is missing or empty. Logs a descriptive error to the terminal on failure.

app.post('/send-email', async (c) => {
  try {
    const ctx = c as any

    const smtpHost = requireEnv(ctx, 'SMTP_HOST')
    const smtpPass = requireEnv(ctx, 'SMTP_PASS')
    const smtpPort = parseInt(getEnv(ctx, 'SMTP_PORT') ?? '587')

    // ... send email

    return c.json({ sent: true })

  } catch (error: any) {
    if (error.message?.includes('[bini-env] Missing required')) {
      return c.json({ error: error.message }, 500)
    }
    return c.json({ error: 'Failed to send email.' }, 500)
  }
})

On failure, the terminal will show:

[bini-env] error  Missing required environment variable: "SMTP_HOST"
  -> Set it in your platform's env config or hosting dashboard.

biniEnv(options?)

Vite plugin. Registers BINI_ and VITE_ as env prefixes and optionally adds more.

biniEnv()
// or with extra prefixes
biniEnv({ envPrefix: ['MY_PUBLIC_'] })
Option Type Default Description
envPrefix string | string[] [] Extra prefixes to expose to import.meta.env, in addition to BINI_ and VITE_

biniLogger

Vite-style terminal logger. Use it in your own Bini.js plugins or server-side code.

import { biniLogger } from 'bini-env'

biniLogger.info('Server ready')
biniLogger.warn('Missing optional var')
biniLogger.error('Something broke', error)

HonoContext

Exported type (Context from Hono). Use it to type helper functions that group env reads, so you only cast c as any once per entry point.

import type { HonoContext } from 'bini-env'

function readDbConfig(c: HonoContext) {
  const ctx = c as any
  return {
    url:      requireEnv(ctx, 'DATABASE_URL'),
    poolSize: parseInt(getEnv(ctx, 'DB_POOL_SIZE') ?? '10'),
    ssl:      getEnv(ctx, 'DB_SSL') !== 'false',
  }
}

Performance

Metric Dev Prod
File reads 0 0
Runtime cost ~0ms 0
Bundle impact Minimal Tree-shaken

No dotenv. No disk reads. No caching layer. getEnv is a direct call to Hono's adapter on every invocation — request-scoped and correct.


Troubleshooting

"Env var is undefined in production"

Vars set in .env files are only loaded by Vite during local development. In production, set your variables in your hosting platform's environment dashboard (Vercel, Netlify, Cloudflare, etc.).

"Works in dev, undefined in prod"

Same as above. Local dev works because Vite loads .env files automatically. Production requires platform-level configuration.

"Cloudflare secret not found"

Secrets set via wrangler secret put are only available via c.env inside a handler — which is exactly what getEnv(ctx, key) reads. Ensure you are passing c to the function.

"TypeScript error: Context not assignable to HonoContext"

Hono 4.12+ added a symbol to HonoRequest that can break structural assignability in strict TypeScript projects. Cast once per handler:

const ctx = c as any

"Types not found"

Add to your tsconfig.json or entry file:

/// <reference types="vite/client" />

Compatibility

Tool Version Notes
Vite 8.x envPrefix hook is fully compatible; empty prefix array is never passed
Hono 4.x hono/adapter env() is stable across all 4.x releases
TypeScript 5.x Full type support via exported HonoContext
Node.js ≥ 20.19 Minimum required version

Contributing

PRs are welcome. The goal of this library is to stay minimal and runtime-cost-free. Contributions that add unnecessary complexity, increase bundle size, or introduce startup overhead will not be accepted.


License

MIT © Bini.js Team

About

Universal environment variable loader and Vite plugin for Bini.js. Automatically loads .env files in development, reads env vars from the host in production, and works across Node.js, Deno, Bun, and Vite edge functions — with zero configuration.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages