Tiesen LogoYuki UI
Lib

Create Env

A utility to validate environment variables using Zod

Component Source

Environment Variables Validation

This utility leverages Standard Schema to ensure your environment variables are correctly typed and present. It provides a simple API to define server and client variables, set defaults, and skip validation in specific scenarios (like CI or linting).

Example Usage

env.ts
import * as validator from 'zod'
// or import { type as validator } from 'arktype'
// or import { valibot as validator } from 'valibot'

import { createEnv } from '@/lib/create-env'

export const env = createEnv({
  shared: {
    NODE_ENV: validator.enum(['development', 'production', 'test']),
    // NODE_ENV: validator("'development'|'production'|'test'") // arktype
  },

  server: {
    SERVER_VAR: validator.string(),
    // SERVER_VAR: validator('string') // arktype
  },

  clientPrefix: 'NEXT_PUBLIC_',
  client: {
    NEXT_PUBLIC_CLIENT_VAR: z.string(),
    // NEXT_PUBLIC_CLIENT_VAR: validator('string') // arktype
  },

  runtimeEnv: {
    ...process.env,
    NEXT_PUBLIC_CLIENT_VAR: process.env.NEXT_PUBLIC_CLIENT_VAR,
  },

  emptyStringAsUndefined: true,

  skipValidation:
    !!process.env.CI || process.env.npm_lifecycle_event === 'lint',
})

Installation

CLI

npx shadcn add https://yuki-ui.vercel.app/r/create-env.json

Manual

Install the following dependencies:

npm install zod #or arktype, valibot or any other Standard Schema compatible library

Copy and paste the following code into your project.

export function createEnv<  TPrefix extends string,  TShared extends Record<string, StandardSchemaV1>,  TServer extends Record<string, StandardSchemaV1>,  TClient extends Record<string, StandardSchemaV1>,  TResult extends {    [TKey in keyof (TShared & TServer & TClient)]: StandardSchemaV1.InferOutput<      (TShared & TServer & TClient)[TKey]    >  },  TDeriveEnv extends Record<string, unknown> = Record<string, unknown>,>(opts: {  /*   * Whather the code is running on server or client side   * @default window === undefined   */  isServer?: boolean  /*   * Environment variable schemas shared between server and client   * @example: NODE_ENV, VERCEL_URL, etc.   */  shared: TShared  /*   * Server-only environment variable schemas   * @example: DATABASE_URL, SECRET_KEY, etc.   */  server: {    [TKey in keyof TServer]: TKey extends `${TPrefix}${string}`      ? `${TKey} should not prefix with ${TPrefix}`      : TServer[TKey]  }  /*   * Client-only environment variable schemas   * @example: NEXT_PUBLIC_API_URL, etc.   */  clientPrefix: TPrefix  client: {    [TKey in keyof TClient]: TKey extends `${TPrefix}${string}`      ? TClient[TKey]      : `${TKey extends string ? TKey : never} should prefix with ${TPrefix}`  }  /*   * The runtime environment variables to validate, typically process.env.   *   * For client-only variables, avoid spreading process.env directly.   * @see https://github.com/vercel/next.js/discussions/34957   *   * @example   * ```ts   * // Incorrect: client variable will be undefined   * const env = createEnv({   *   ...   *   client: { PUBLIC_VAR: z.string() },   *   runtimeEnv: { ...process.env }   * })   *   * env.PUBLIC_VAR // undefined   *   * // Correct: client variable is set properly   * const env = createEnv({   *   ...   *   client: { PUBLIC_VAR: z.string() },   *   runtimeEnv: { PUBLIC_VAR: process.env.PUBLIC_VAR }   * })   *   * env.PUBLIC_VAR // 'some-value'   * ```   */  runtimeEnv:    | { [TKey in keyof TResult]: string | number | boolean | undefined }    | Record<string, unknown>  /*   * A function to derive additional environment variables based on the validated ones.   * @param env The validated environment variables.   * @returns An object containing derived environment variables.   * @example   * ```ts   * const env = createEnv({   *   ...   * }, (env) => ({   *   IS_PRODUCTION: env.NODE_ENV === 'production',   * }))   *   * env.IS_PRODUCTION // true or false   */  deriveEnv?: (env: TResult) => TDeriveEnv  /*   * Whether to treat empty strings as undefined values.   * @default false   */  emptyStringAsUndefined?: boolean  /*   * Whether to skip validation of environment variables.   * @default false   */  skipValidation?: boolean}): TResult & TDeriveEnv {  if (opts.emptyStringAsUndefined)    for (const [key, value] of Object.entries(opts.runtimeEnv))      if (value === '') delete opts.runtimeEnv[key]  const isServer = opts.isServer    ? opts.isServer    : (globalThis as unknown as { window: unknown }).window === undefined  const envs = isServer    ? { ...opts.shared, ...opts.client, ...opts.server }    : { ...opts.shared, ...opts.client }  const parsedEnvs = parseEnvs(opts.runtimeEnv, envs as never)  if (!opts.skipValidation && !parsedEnvs.success)    throw new Error(      `❌ Environment variables validation failed:\n${parsedEnvs.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n')}`,    )  const envData = parsedEnvs.success ? parsedEnvs.data : {}  if (opts.deriveEnv) Object.assign(envData, opts.deriveEnv(envData as TResult))  return new Proxy(envData as TResult & TDeriveEnv, {    get(target, prop) {      if (!isServer && prop in opts.server)        throw new Error(          `❌ Attempted to access a server-side environment variable on the client`,        )      return target[prop as keyof typeof target]    },  })}function parseEnvs<  TSchemas extends Record<string, StandardSchemaV1>,  TData extends {    [TKey in keyof TSchemas]: StandardSchemaV1.InferOutput<TSchemas[TKey]>  },>(  data: Record<string, unknown>,  schemas: TSchemas,):  | { success: true; data: TData }  | { success: false; issues: StandardSchemaV1.Issue[] } {  if (Object.keys(schemas).length === 0)    return { success: true, data: {} as TData }  const results: TData = {} as TData  const issues: StandardSchemaV1.Issue[] = []  for (const [key, schema] of Object.entries(schemas)) {    const value = data[key]    const validated = schema['~standard'].validate(value)    if (validated instanceof Promise)      throw new Error('Async schema validation is not supported')    if ('issues' in validated && validated.issues)      issues.push({        path: [key],        message: validated.issues.map((issue) => issue.message).join(', '),      })    else results[key as keyof TData] = validated.value as TData[typeof key]  }  if (issues.length > 0) return { success: false, issues }  return { success: true, data: results }}/** The Standard Schema interface. */interface StandardSchemaV1<Input = unknown, Output = Input> {  /** The Standard Schema properties. */  readonly '~standard': StandardSchemaV1.Props<Input, Output>}declare namespace StandardSchemaV1 {  /** The Standard Schema properties interface. */  export interface Props<Input = unknown, Output = Input> {    /** The version number of the standard. */    readonly version: 1    /** The vendor name of the schema library. */    readonly vendor: string    /** Validates unknown input values. */    readonly validate: (      value: unknown,      options?: StandardSchemaV1.Options | undefined,    ) => Result<Output> | Promise<Result<Output>>    /** Inferred types associated with the schema. */    readonly types?: Types<Input, Output> | undefined  }  /** The result interface of the validate function. */  export type Result<Output> = SuccessResult<Output> | FailureResult  /** The result interface if validation succeeds. */  export interface SuccessResult<Output> {    /** The typed output value. */    readonly value: Output    /** A falsy value for `issues` indicates success. */    readonly issues?: undefined  }  export interface Options {    /** Explicit support for additional vendor-specific parameters, if needed. */    readonly libraryOptions?: Record<string, unknown> | undefined  }  /** The result interface if validation fails. */  export interface FailureResult {    /** The issues of failed validation. */    readonly issues: ReadonlyArray<Issue>  }  /** The issue interface of the failure output. */  export interface Issue {    /** The error message of the issue. */    readonly message: string    /** The path of the issue, if any. */    readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined  }  /** The path segment interface of the issue. */  export interface PathSegment {    /** The key representing a path segment. */    readonly key: PropertyKey  }  /** The Standard Schema types interface. */  export interface Types<Input = unknown, Output = Input> {    /** The input type of the schema. */    readonly input: Input    /** The output type of the schema. */    readonly output: Output  }  /** Infers the input type of a Standard Schema. */  export type InferInput<Schema extends StandardSchemaV1> = NonNullable<    Schema['~standard']['types']  >['input']  /** Infers the output type of a Standard Schema. */  export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<    Schema['~standard']['types']  >['output']}

On this page