Environment Variables
Type-safe env var validation
Zagora lets you validate environment variables with the same schema system used for inputs and outputs.
Basic Usage
Use .env() to define and validate environment variables:
import { z } from 'zod';
import { zagora } from 'zagora';
const apiCall = zagora()
.env(z.object({
API_KEY: z.string().min(1),
API_URL: z.string().url(),
TIMEOUT: z.coerce.number().default(5000)
}))
.input(z.string())
.handler(({ env }, endpoint) => {
// env.API_KEY: string
// env.API_URL: string
// env.TIMEOUT: number
return fetch(`${env.API_URL}${endpoint}`, {
headers: { 'Authorization': `Bearer ${env.API_KEY}` },
signal: AbortSignal.timeout(env.TIMEOUT)
});
})
.callable({ env: process.env as any });Providing Env at Runtime
Pass environment variables via .callable():
const proc = zagora()
.env(z.object({
DATABASE_URL: z.string()
}))
.handler(({ env }) => connectToDatabase(env.DATABASE_URL))
.callable({ env: process.env as any });Coercion
Use .coerce to convert string env vars to proper types:
.env(z.object({
PORT: z.coerce.number(), // "3000" -> 3000
DEBUG: z.coerce.boolean(), // "true" -> true
TIMEOUT: z.coerce.number().default(5000)
}))Default Values
Defaults work as expected:
.env(z.object({
NODE_ENV: z.enum(['development', 'production']).default('development'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
MAX_RETRIES: z.coerce.number().default(3)
}))Optional Env Vars
Use .optional() for truly optional variables:
.env(z.object({
API_KEY: z.string(), // Required
SECONDARY_KEY: z.string().optional() // Optional
}))
.handler(({ env }) => {
// env.API_KEY: string
// env.SECONDARY_KEY: string | undefined
})Env Validation Errors
Invalid env vars result in a VALIDATION_ERROR:
const proc = zagora()
.env(z.object({
PORT: z.coerce.number().min(1).max(65535)
}))
.handler(({ env }) => env.PORT)
.callable({ env: { PORT: 'invalid' } });
const result = proc();
// result.ok === false
// result.error.kind === 'VALIDATION_ERROR'With autoCallable
When using autoCallable, provide env vars as the second argument to .env():
const proc = zagora({ autoCallable: true })
.env(
z.object({
API_KEY: z.string()
}),
process.env as any // Second argument: runtime env
)
.input(z.string())
.handler(({ env }, input) => {
return fetch(`/api/${input}`, {
headers: { 'X-API-Key': env.API_KEY }
});
});
// Direct call - no .callable() needed
proc('users');Without disableOptions
Pattern: Config Procedure
Create a config-loading procedure:
const getConfig = zagora()
.env(z.object({
DATABASE_URL: z.string(),
REDIS_URL: z.string().optional(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development')
}))
.handler(({ env }) => ({
database: { url: env.DATABASE_URL },
redis: env.REDIS_URL ? { url: env.REDIS_URL } : null,
auth: { secret: env.JWT_SECRET },
server: { port: env.PORT },
env: env.NODE_ENV
}))
.callable({ env: process.env as any });
const config = getConfig();
if (config.ok) {
startServer(config.data);
}Pattern: Per-Environment Config
const createApiClient = zagora()
.env(z.object({
API_URL: z.string().url(),
API_KEY: z.string()
}))
.input(z.string())
.handler(({ env }, endpoint) => {
return fetch(`${env.API_URL}${endpoint}`, {
headers: { 'Authorization': `Bearer ${env.API_KEY}` }
});
});
// Production
const prodClient = createApiClient.callable({
env: { API_URL: 'https://api.production.com', API_KEY: prodKey }
});
// Staging
const stagingClient = createApiClient.callable({
env: { API_URL: 'https://api.staging.com', API_KEY: stagingKey }
});Limitations
- Async schema validation for env vars is not supported
- Env vars are validated once at
.callable()time, not per-call
Next Steps
- Handler Options - Full options reference
- Context Management - Dependency injection