Using Environment Variables in API Routes

Learn how to read environment variables in your API routes using getEnv and requireEnv.

Overview

In API routes, environment variables are read using getEnv(ctx, key) and requireEnv(ctx, key). Both are auto-imported in API routes and read from the Hono request context via hono/adapter.

Always pass c explicitly. Cast it once at the top of the handler as const ctx = c as any, then use ctx throughout. No process.env fallbacks — every read is request-scoped.

Basic Usage

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

const app = new Hono()

app.get('/hello', (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

Required vs Optional

Use requireEnv for variables your app cannot run without. Use getEnv with ?? for optional configuration.

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'

    return c.json({ model, region, maxRetries, debug })

  } 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)
  }
})
FunctionUse forBehavior
requireEnv(ctx, key)Required config — app cannot run withoutThrows if missing or empty
getEnv(ctx, key) ?? defaultOptional config — fallback to defaultReturns undefined if missing

Complete Example

A full API endpoint that uses environment variables for configuration:

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

const app = new Hono()

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

    // Required — the app cannot send email without these
    const smtpHost = requireEnv(ctx, 'SMTP_HOST')
    const smtpUser = requireEnv(ctx, 'SMTP_USER')
    const smtpPass = requireEnv(ctx, 'SMTP_PASS')
    const fromEmail = requireEnv(ctx, 'FROM_EMAIL')

    // Optional — with sensible defaults
    const smtpPort = parseInt(getEnv(ctx, 'SMTP_PORT') ?? '587')
    const secure = getEnv(ctx, 'SMTP_SECURE') === 'true'
    const debug = getEnv(ctx, 'DEBUG_MODE') === 'true'

    // Optional — use ?? for fallbacks
    const appName = getEnv(ctx, 'APP_NAME') ?? 'Bini.js App'

    const transporter = nodemailer.createTransport({
      host: smtpHost,
      port: smtpPort,
      secure: secure,
      auth: { user: smtpUser, pass: smtpPass },
      debug: debug,
    })

    const { to, subject, text } = await c.req.json()

    if (!to || !subject || !text) {
      return c.json({ error: 'Missing required fields: to, subject, text' }, 400)
    }

    await transporter.sendMail({
      from: fromEmail,
      to,
      subject: `[${appName}] ${subject}`,
      text,
    })

    return c.json({ 
      success: true, 
      message: 'Email sent',
      from: fromEmail,
      app: appName,
    })

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

export default app

Error Handling

Always handle errors from requireEnv gracefully:

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

    const apiKey = requireEnv(ctx, 'API_KEY')
    const secret = requireEnv(ctx, 'SECRET_TOKEN')

    return c.json({ configured: true })

  } catch (error: any) {
    // requireEnv throws an error with a descriptive message
    if (error.message?.includes('[bini-env] Missing required')) {
      return c.json({ 
        error: 'Configuration error', 
        details: error.message 
      }, 500)
    }
    
    // Other errors
    return c.json({ error: 'Something went wrong' }, 500)
  }
})

On failure, the terminal shows:

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

Production Notes

  • Set vars in production.env files are only loaded during development. In production, set variables in your hosting platform's dashboard.
  • No platform-specific codegetEnv and requireEnv work on Node.js, Bun, Deno, Vercel Edge, Netlify Edge, and Cloudflare Workers.
  • Never expose secrets — Never return secret values in API responses. Only return configuration status.
  • Use BINI_ for client vars — Use BINI_ prefix for client-side public config. No prefix for server-only secrets.
The same API code runs unchanged across all platforms. bini-env reads from the correct source on every platform automatically.