Using Environment Variables in API Routes

Read environment variables in API routes with getEnv and requireEnv.

Overview

In API routes, environment variables are read with getEnv(c, key) and requireEnv(c, key). Both read from the Hono request context via hono/adapter - that is what makes them work on every runtime.

.env or hosting dashboard
MY_API_KEY=sk_live_…
APP_NAME=Bini
via hono/adapter
getEnv(ctx, key)
requireEnv(ctx, key)
API handler
const key = requireEnv(
ctx, 'MY_API_KEY'
)
Auto-imported: getEnv and requireEnv are auto-imported in API routes - you do not need to write the import from bini-env manually.
Always pass c explicitly. In TypeScript, 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

app
api
ƒhello.ts
ƒemail.ts
ƒconfig.ts
/api/hello
/api/email
/api/config
src/app/api/hello.ts
// src/app/api/hello.ts
import { Hono } from 'hono'

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.

src/app/api/example.ts
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 nodemailer from 'nodemailer'

const app = new Hono()

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

    const smtpHost = requireEnv(ctx, 'SMTP_HOST')
    const smtpUser = requireEnv(ctx, 'SMTP_USER')
    const smtpPass = requireEnv(ctx, 'SMTP_PASS')
    const fromEmail = requireEnv(ctx, 'FROM_EMAIL')

    const smtpPort = parseInt(getEnv(ctx, 'SMTP_PORT') ?? '587')
    const secure = getEnv(ctx, 'SMTP_SECURE') === 'true'
    const debug = getEnv(ctx, 'DEBUG_MODE') === 'true'
    const appName = getEnv(ctx, 'APP_NAME') ?? 'Bini.js App'

    const transporter = nodemailer.createTransport({
      host: smtpHost,
      port: smtpPort,
      secure,
      auth: { user: smtpUser, pass: smtpPass },
      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:

src/app/api/config.ts
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) {
    if (error.message?.includes('[bini-env] Missing required')) {
      return c.json(
        {
          error: 'Configuration error',
          details: error.message,
        },
        500
      )
    }

    return c.json({ error: 'Something went wrong' }, 500)
  }
})

On failure, the terminal shows:

>_Terminal
[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 code - getEnv 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 the 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.
Was this helpful?