Type-safe environment variables com Zod ou envalid
1. Introdução ao problema: variáveis de ambiente sem tipo
Em projetos TypeScript, process.env é tipado como { [key: string]: string | undefined }. Isso significa que qualquer acesso a uma variável de ambiente retorna string | undefined, mesmo que você saiba que ela existe. O problema se manifesta de várias formas:
// Exemplo clássico de problema em runtime
const port = process.env.PORT; // tipo: string | undefined
const server = app.listen(port); // erro silencioso: port pode ser undefined
// Conversão manual sem validação
const apiUrl = process.env.API_URL as string; // cast inseguro
const timeout = parseInt(process.env.TIMEOUT); // NaN silencioso
// Valores booleanos problemáticos
const isProduction = process.env.NODE_ENV === 'production'; // string comparada com string
Confiar apenas em .env.example não resolve o problema porque:
- O arquivo exemplo pode estar desatualizado
- Não há garantia de que o desenvolvedor copiou todas as variáveis
- TypeScript não valida o conteúdo do .env em tempo de compilação
- Erros só aparecem em runtime, frequentemente em produção
2. Zod: schema-first para validação rigorosa
Zod permite definir schemas que validam e transformam variáveis de ambiente com tipo seguro:
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().positive().default(3000),
DATABASE_URL: z.string().url(),
REDIS_HOST: z.string().default('localhost'),
ENABLE_FEATURE_X: z.coerce.boolean().default(false),
API_RATE_LIMIT: z.coerce.number().int().min(1).max(1000),
});
// Parsing com tratamento de erro graceful
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ Variáveis de ambiente inválidas:', result.error.format());
process.exit(1);
}
const env = result.data;
// env agora tem tipos inferidos automaticamente
// env.PORT é number, env.NODE_ENV é 'development' | 'production' | 'test'
O safeParse é preferível ao parse porque permite tratamento de erro sem lançar exceções. O tipo inferido via z.infer pode ser exportado para uso em toda a aplicação:
export type Env = z.infer<typeof envSchema>;
3. Envalid: abordagem declarativa e leve
Envalid oferece uma API mais concisa para validação de variáveis de ambiente:
import { cleanEnv, str, num, bool, url, email, host } from 'envalid';
const env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ['development', 'production', 'test'] }),
PORT: num({ default: 3000, devDefault: 8080 }),
DATABASE_URL: url(),
ADMIN_EMAIL: email({ default: 'admin@example.com' }),
REDIS_HOST: host({ default: 'localhost' }),
ENABLE_FEATURE_X: bool({ default: false, devDefault: true }),
API_RATE_LIMIT: num({ default: 100, devDefault: 50 }),
});
// env já é tipado automaticamente
// env.PORT é number, env.NODE_ENV tem union type
Envalid expõe um objeto tipado sem necessidade de cast manual. A função cleanEnv já valida e transforma os valores, retornando um objeto com tipos corretos. Para ambientes de desenvolvimento, devDefault permite valores diferentes sem poluir o .env.
4. Comparação prática: Zod vs Envalid
| Característica | Zod | Envalid |
|---|---|---|
| Tamanho (minified) | ~12KB | ~3KB |
| Flexibilidade | Alta (transformações, refinamentos) | Média (validadores predefinidos) |
| Curva de aprendizado | Moderada | Baixa |
| Validação condicional | Suporte nativo | Limitada |
| Integração com TypeScript | Excelente (inferência automática) | Boa |
Quando usar cada um:
- Envalid: Projetos pequenos a médios, APIs REST simples, aplicações com poucas variáveis de ambiente. Sua simplicidade reduz boilerplate.
// Exemplo com envalid para projeto pequeno
import { cleanEnv, str, num } from 'envalid';
export const env = cleanEnv(process.env, {
PORT: num({ default: 3000 }),
DATABASE_URL: str(),
});
- Zod: Sistemas complexos com validação condicional, transformações customizadas ou schemas compartilhados entre frontend e backend.
// Zod permite refinamentos customizados
const envSchema = z.object({
DATABASE_URL: z.string().url().refine(
(url) => url.startsWith('postgres://') || url.startsWith('mysql://'),
{ message: 'URL deve ser PostgreSQL ou MySQL' }
),
FEATURE_FLAGS: z.string().transform((str) => str.split(',')),
});
5. Integração com frameworks e build tools
A validação deve ocorrer no início da aplicação, antes de qualquer outro código:
// entry point (ex: src/index.ts)
import 'dotenv/config'; // carrega .env
import { env } from './config/env'; // valida imediatamente
// Se a validação falhar, o app nem inicia
console.log(`Servidor rodando na porta ${env.PORT}`);
Para Next.js, crie um arquivo de configuração separado:
// src/lib/env.ts (Next.js)
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url(),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
// Validação em tempo de build
export const env = envSchema.parse(process.env);
No Vite, use import.meta.env com validação similar:
// src/env.ts (Vite)
import { z } from 'zod';
const clientSchema = z.object({
VITE_API_URL: z.string().url(),
VITE_ENABLE_ANALYTICS: z.coerce.boolean(),
});
export const env = clientSchema.parse(import.meta.env);
6. Type safety em tempo de compilação e runtime
Para garantir type safety completo, declare tipos globais:
// src/types/env.d.ts
import { z } from 'zod';
const envSchema = z.object({
PORT: z.coerce.number(),
DATABASE_URL: z.string().url(),
});
export type Env = z.infer<typeof envSchema>;
declare global {
namespace NodeJS {
interface ProcessEnv extends Env {}
}
}
Use satisfies para garantir que o schema cubra todas as variáveis do .env:
import { z } from 'zod';
const envSchema = {
PORT: z.coerce.number(),
DATABASE_URL: z.string().url(),
// TypeScript reclama se faltar alguma variável
} satisfies Record<string, z.ZodTypeAny>;
const parsed = z.object(envSchema).parse(process.env);
Isso elimina a necessidade de as string ou outros casts inseguros.
7. Boas práticas e armadilhas comuns
Nunca exponha schemas de validação no frontend — isso vaza a estrutura do seu ambiente:
// ❌ ERRADO: schema exposto no bundle do frontend
import { envSchema } from '../shared/schemas';
// ✅ CORRETO: apenas valores validados são exportados
export const env = envSchema.parse(process.env);
Lidando com variáveis opcionais sem perder strictness:
const envSchema = z.object({
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
// optional() + default() garante que sempre terá um valor
});
Em monorepos, compartilhe schemas entre packages:
// packages/shared/src/env.ts
import { z } from 'zod';
export const sharedEnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
LOG_LEVEL: z.string().default('info'),
});
// packages/api/src/env.ts
import { sharedEnvSchema } from '@myorg/shared';
import { z } from 'zod';
const apiEnvSchema = sharedEnvSchema.extend({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
});
8. Conclusão e próximos passos
Validar variáveis de ambiente com tipo seguro elimina uma classe inteira de bugs: erros de configuração que só apareceriam em produção. Tanto Zod quanto Envalid resolvem o problema, cada um com seu equilíbrio entre simplicidade e flexibilidade.
Próximos passos recomendados:
- Adicione ts-reset para melhorar a tipagem de process.env no escopo global
- Use dotenv-flow para gerenciar múltiplos ambientes (.env.development, .env.production)
- Explore o operador satisfies do TypeScript 4.9+ para validação em tempo de compilação
- Considere type-only imports para evitar código morto no bundle
A validação com tipo não é um luxo — é uma necessidade em qualquer aplicação TypeScript profissional. Invista alguns minutos na configuração inicial e evite horas de debugging em produção.
Referências
- Documentação oficial do Zod — Guia completo de schemas, validação e inferência de tipos com Zod
- Documentação oficial do Envalid — Repositório com exemplos e API completa para validação de variáveis de ambiente
- TypeScript: Documentação de ProcessEnv — Como estender tipos globais do Node.js para variáveis de ambiente
- Dotenv-flow: Gerenciamento de ambientes — Extensão do dotenv para múltiplos arquivos
.envpor ambiente - ts-reset: Tipos mais seguros para APIs nativas — Biblioteca que melhora a tipagem de
process.enve outras APIs JavaScript - Next.js: Environment Variables — Documentação oficial sobre variáveis de ambiente no Next.js com exemplos de validação
- Vite: Env Variables and Modes — Como o Vite gerencia variáveis de ambiente e modos de execução