Environment Variables
Type-safe environment variable validation with descriptive boot-time errors.
Overview
@tekir/env wraps envalid to give your application a single, validated snapshot of every environment variable it needs. If a required variable is missing or fails its validator, tekir throws a clear error at boot time, never during a live request.
bun add @tekir/envThe env.ts File
By convention your project keeps a single env.ts at the project root. It reads from .env (and the real process environment), validates every value, and exports a typed, frozen object. Import it anywhere via #env.
Tell tekir() to load the file at boot by passing envFile: 'env.ts'. Without that option, tekir does not import any env file: it leaves the path alone, which is what you want when an unrelated script in the project happens to be named env.ts.
NODE_ENV=development
APP_NAME=My App
APP_KEY= # run: tekir generate:key
HOST=0.0.0.0
PORT=3000
DB_PATH=./database/app.sqlite
LOG_LEVEL=infoimport { defineEnv, str, port, host } from '@tekir/env'
export default defineEnv({
NODE_ENV: str({ choices: ['development', 'production', 'test'], default: 'development' }),
APP_NAME: str({ default: 'tekir' }),
APP_KEY: str({ default: 'change-me-in-production' }),
HOST: host({ default: '0.0.0.0' }), // '0.0.0.0' = all interfaces, 'localhost' = local only
PORT: port({ default: 3000 }),
DB_PATH: str({ default: ':memory:' }),
LOG_LEVEL: str({ default: 'info' })
})Set up the #env import alias in your tsconfig.json:
{
"compilerOptions": {
"paths": {
"#env": ["./env.ts"]
}
}
}If a required variable (one without a default) is missing, tekir prints a structured error and exits before any route is ever registered:
# Example: if a required variable (no default) is missing from .env
# defineEnv({ DB_PASSWORD: str() }) ← no default = required
#
# Output:
Invalid env vars:
DB_PASSWORD Missing required environment variabledefineEnv()
defineEnv(schema) accepts a record of envalid validator calls, passes process.env through cleanEnv, and returns a frozen, fully-typed object. Call it exactly once, at the top of env.ts, and export the result as the default export.
import { defineEnv, str, port } from '@tekir/env'
export default defineEnv({
APP_KEY: str(),
PORT: port({ default: 3000 })
})The return type is automatically inferred from the schema, env.PORT is number, env.APP_KEY is string, and so on. Accessing a key not in the schema throws at runtime (via envalid's proxy).
Built-in Validators
All validators accept an optional options object with at least a default property. Omitting default makes the variable required.
import { defineEnv, str, num, bool, port, url, email, json, host } from '@tekir/env'
export default defineEnv({
// str: any string value (required if no default)
APP_KEY: str(),
APP_NAME: str({ default: 'tekir' }),
// num: parsed to a JavaScript number
MAX_RETRIES: num({ default: 3 }),
// bool: accepts "true"/"false"/"1"/"0" → boolean
DEBUG: bool({ default: false }),
// port: integer 1–65535
PORT: port({ default: 3000 }),
// url: must be a valid URL string
REDIS_URL: url({ default: 'redis://localhost:6379' }),
// email: must be a valid e-mail address
MAIL_FROM: email({ default: '[email protected]' }),
// json: parsed with JSON.parse, returns the object/array
FEATURE_FLAGS: json<{ darkMode: boolean }>({ default: { darkMode: false } }),
// host: hostname or IP address (no protocol, no port)
DB_HOST: host({ default: 'localhost' })
})Restricting values with choices
Pass a choices array to any str() validator to restrict the accepted values to a fixed set. An out-of-range value is treated as invalid at boot.
import { defineEnv, str } from '@tekir/env'
export default defineEnv({
LOG_LEVEL: str({ choices: ['debug', 'info', 'warn', 'error'], default: 'info' }),
NODE_ENV: str({ choices: ['development', 'production', 'test'] })
})Custom Validators
Use makeValidator(parseFn) to create a validator for any value that the built-ins do not cover. The parse function receives the raw string and should either return the coerced value or throw an error.
import { defineEnv, makeValidator } from '@tekir/env'
// makeValidator wraps a parsing function and returns an envalid validator.
// Throw to signal an invalid value.
const hexColor = makeValidator<string>((input) => {
if (/^#[0-9a-fA-F]{6}$/.test(input)) return input
throw new Error(`Expected a hex color (#rrggbb), got: ${input}`)
})
const positiveInt = makeValidator<number>((input) => {
const n = parseInt(input, 10)
if (isNaN(n) || n <= 0) throw new Error('Must be a positive integer')
return n
})
export default defineEnv({
BRAND_COLOR: hexColor({ default: '#3b82f6' }),
WORKER_COUNT: positiveInt({ default: 4 })
})Reading Values
Import the env object from #env and access properties directly. Every property is fully typed based on the validator used in defineEnv():
// Import the typed env object via the #env import map
import env from '#env'
// Access properties directly: fully typed
const host = env.HOST // string ('0.0.0.0' or 'localhost')
const port = env.PORT // number
const key = env.APP_KEY // stringUse it in config files to wire environment values into your application configuration:
import env from '#env'
export default {
name: env.APP_NAME,
host: env.HOST,
port: env.PORT,
key: env.APP_KEY,
env: env.NODE_ENV
}Or anywhere in your application code:
import env from '#env'
import type { HttpContext } from '@tekir/core'
export function show({ response }: HttpContext) {
return response.ok({
status: 'ok',
env: env.NODE_ENV,
debug: env.DEBUG
})
}