Zod v4 Patterns
Practical Zod v4 patterns for environment variable schemas. All examples assume the following import:
import { defineEnv } from '@vite-env/core'
import { z } from 'zod'Zod v4 error parameter
Zod v4 uses error instead of message for custom error messages in .refine() and .superRefine(). Using message will have no effect.
Required String
API_KEY: z.string().min(1)Accepts any non-empty string. Rejects an empty string "".
URL
VITE_API_URL: z.url()Validates URL format. This is Zod v4 shorthand for z.string().url(). Rejects strings that are not valid URLs.
String Boolean
VITE_DEBUG: z.stringbool()Accepts "true", "false", "1", "0", "yes", "no", "on", "off" (case-insensitive) and coerces them to a JavaScript boolean. Useful for feature flags that are set as strings in .env files.
Coerced Number
PORT: z.coerce.number()Converts the string value to a number. Rejects values that cannot be parsed as a number (e.g. "abc").
Constrained Number
DB_POOL_SIZE: z.coerce.number().int().min(1).max(100)Converts to number, then enforces: must be an integer, at least 1, at most 100. All constraints are checked after coercion.
Enum
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error'])Accepts exactly one of the listed string values. Rejects anything else, including values that are close but not exact (e.g. "DEBUG").
Optional
SENTRY_DSN: z.string().optional()The variable does not need to be set. Produces undefined when absent. Useful for variables that are only needed in specific environments.
Default Value
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')Uses the default when the variable is not set. The inferred type is string (not string | undefined) because a value is always present after parsing.
Custom Validation
OPENAI_API_KEY: z.string().refine(val => val.startsWith('sk-'), {
error: 'Must start with sk-',
})Runs an arbitrary predicate after basic type checks pass. The error property sets the failure message. Note that Zod v4 uses error, not message — using message will silently produce a generic error.
Combining Patterns
Patterns compose naturally:
export default defineEnv({
server: {
DATABASE_URL: z.url(),
JWT_SECRET: z.string().min(32),
DB_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),
OPENAI_API_KEY: z
.string()
.refine(v => v.startsWith('sk-'), { error: 'Must start with sk-' }),
SENTRY_DSN: z.string().optional(),
},
client: {
VITE_API_URL: z.url(),
VITE_APP_NAME: z.string().min(1),
VITE_DEBUG: z.stringbool().default(false),
VITE_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
},
})