Methodology

Environment Variables

Using .env files with Zod for typesafe environment variables.

Environment Variables

In a TypeScript project, .env files are used to manage environment variables, which are configuration settings that can vary between different environments (e.g., development, testing, production). These files typically store sensitive information like API keys, database connection strings, and other environment-specific configurations.

Example .env file

.env
API_KEY=your_api_key_here
DATABASE_URL=your_database_connection_string

🧩 Type-Safe Environment Variables in Nuxt 4 with Zod

Managing environment variables safely is crucial in TypeScript projects. Using Zod, you can validate and type your .env values at runtime to ensure your app never starts with missing or invalid configuration.

πŸš€ Install Dependencies

First, install Zod:

npm install zod

🧱 Create a .env File

Example .env file:

.env
API_URL=https://api.example.com
API_KEY=my-secret-key
PUBLIC_ANALYTICS_ID=G-XXXXXXX

🧱 Configure Runtime Variables in nuxt.config.ts

In Nuxt, environment variables are divided into two categories:

Private (server-only): accessible only on the server

Public (client-exposed): must be nested under public in your config

Example:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiUrl: process.env.API_URL,
    apiKey: process.env.API_KEY,
    public: {
      analyticsId: process.env.PUBLIC_ANALYTICS_ID,
    },
  },
});

🧠 Define a Zod Schema for Validation

Create a file called env.ts (or config/env.ts):

env.ts
import { z } from "zod";
import { useRuntimeConfig } from "#imports";

// Define your schema
const configSchema = z.object({
  apiUrl: z.string().url(),
  apiKey: z.string().min(1),
  public: z.object({
    analyticsId: z.string().min(1),
  }),
});

export function getValidatedConfig() {
  const runtimeConfig = useRuntimeConfig();
  const parsed = configSchema.safeParse(runtimeConfig);

  if (!parsed.success) {
    console.error("❌ Invalid environment configuration:", parsed.error.flatten().fieldErrors);
    throw new Error("Invalid runtime configuration");
  }

  return parsed.data;
}

🧩 Use the Typed env Object

Now, anywhere in your app β€” for example, in a server route, plugin, or composable β€” you can import and use your validated config safely:

server/api/hello.ts
import { getValidatedConfig } from "../utils/env";

export default defineEventHandler(() => {
  const config = getValidatedConfig();

  console.log(config.apiUrl); // βœ… string (validated URL)
  console.log(config.public.analyticsId); // βœ… string (validated)
  
  return { message: "Environment variables validated!" };
});

🧭 Type Safety in Client Code

On the client side, you can safely access only public variables:

composables/useAnalytics.ts
export const useAnalytics = () => {
  const config = useRuntimeConfig();

  // Public runtime config is type-safe too
  const analyticsId = config.public.analyticsId;

  console.log("Analytics ID:", analyticsId);
};

⚑ Optional: Split for Different Environments

You can even extend this pattern for multiple environments, e.g., .env.development, .env.production, and load them dynamically based on NODE_ENV.

Example:

env.ts
dotenv.config({ path: `.env.${process.env.NODE_ENV || "development"}` });

βœ… Benefits

  • Runtime validation of environment variables
  • Compile-time type safety with TypeScript
  • Prevents silent startup errors
  • Centralized configuration management

🧭 Example Project Structure

my-app/
β”œβ”€ app/
β”‚  β”œβ”€ env.ts
β”‚  β”œβ”€ index.ts
|  |- app.config.ts
β”œβ”€ .env
β”œβ”€ package.json
β”œβ”€ nuxt.config.ts
β”œβ”€ tsconfig.json

With this setup, your environment variables are validated, parsed, and type-safe, preventing runtime surprises and improving developer confidence.