My App
Packages

@httpjpg/env

Type-safe environment variable validation with Zod

@httpjpg/env

Type-safe environment variable validation using Zod and @t3-oss/env-nextjs.

Installation

pnpm add @httpjpg/env

Features

  • Runtime validation - Validates env vars at build time
  • TypeScript types - Autocomplete for all env vars
  • Server/Client separation - Clear separation of env vars
  • Zod schemas - Custom validation rules
  • Error messages - Clear error messages for missing vars

Configuration

The package validates these environment variables:

Server-side Variables

// Only accessible on server
STORYBLOK_MANAGEMENT_TOKEN: z.string().min(1)
STORYBLOK_SPACE_ID: z.string().min(1)
STORYBLOK_PREVIEW_TOKEN: z.string().min(1)
SPOTIFY_CLIENT_ID: z.string().min(1)
SPOTIFY_CLIENT_SECRET: z.string().min(1)
SPOTIFY_REFRESH_TOKEN: z.string().min(1)

Client-side Variables

// Accessible in browser (must have NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN: z.string().min(1)
NEXT_PUBLIC_ENVIRONMENT: z.enum(['development', 'staging', 'production'])

Usage

Import Environment Variables

import { env } from '@httpjpg/env';

// Server-side only
const token = env.STORYBLOK_MANAGEMENT_TOKEN;
const spaceId = env.STORYBLOK_SPACE_ID;

// Client-side accessible
const publicToken = env.NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN;

TypeScript Autocomplete

import { env } from '@httpjpg/env';

env. // TypeScript shows all available env vars

Environment Files

.env.example

Template for required variables:

.env.example
# Storyblok Management API
STORYBLOK_MANAGEMENT_TOKEN=your-management-token
STORYBLOK_SPACE_ID=your-space-id

# Storyblok Content Delivery API
STORYBLOK_PREVIEW_TOKEN=your-preview-token
NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN=your-access-token

# Spotify Integration
SPOTIFY_CLIENT_ID=your-client-id
SPOTIFY_CLIENT_SECRET=your-client-secret
SPOTIFY_REFRESH_TOKEN=your-refresh-token

# Environment
NEXT_PUBLIC_ENVIRONMENT=development

.env.local

Your actual values (never commit):

.env.local
STORYBLOK_MANAGEMENT_TOKEN=abc123...
STORYBLOK_SPACE_ID=123456
# ... etc

Validation Errors

If a required env var is missing, you'll see:

❌ Invalid environment variables:
{
  STORYBLOK_MANAGEMENT_TOKEN: [ 'Required' ]
}

Adding New Variables

1. Update Schema

Edit packages/env/src/env.mjs:

import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    // Add server-side var
    MY_NEW_SECRET: z.string().min(1),
  },
  client: {
    // Add client-side var (must have NEXT_PUBLIC_ prefix)
    NEXT_PUBLIC_MY_VAR: z.string().optional(),
  },
  runtimeEnv: {
    MY_NEW_SECRET: process.env.MY_NEW_SECRET,
    NEXT_PUBLIC_MY_VAR: process.env.NEXT_PUBLIC_MY_VAR,
  },
});

2. Update .env.example

# My Feature
MY_NEW_SECRET=your-secret
NEXT_PUBLIC_MY_VAR=your-value

3. Use in Code

import { env } from '@httpjpg/env';

const secret = env.MY_NEW_SECRET; // Server-only
const publicVar = env.NEXT_PUBLIC_MY_VAR; // Client-accessible

Custom Validation

Use Zod for custom validation:

server: {
  // URL validation
  API_URL: z.string().url(),

  // Email validation
  ADMIN_EMAIL: z.string().email(),

  // Number with min/max
  PORT: z.coerce.number().min(1000).max(9999),

  // Enum
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),

  // Optional with default
  TIMEOUT: z.coerce.number().default(5000),
}

Best Practices

  1. Never commit .env.local - Add to .gitignore
  2. Use NEXT_PUBLIC_ prefix for client-side vars
  3. Keep secrets server-side - Don't expose in browser
  4. Update .env.example when adding new vars
  5. Use strict validation - Add constraints with Zod
  6. Document each variable - Add comments in .env.example

Security

✅ Do

// Server-side API calls
const api = await fetch('https://api.example.com', {
  headers: {
    'Authorization': `Bearer ${env.SECRET_API_KEY}`,
  },
});

❌ Don't

// DON'T expose secrets to client
const ClientComponent = () => {
  // This would expose the secret!
  const secret = env.SECRET_API_KEY; // ❌ ERROR
};

Client-side Usage

// Client components can only access NEXT_PUBLIC_ vars
'use client';

export function ClientComponent() {
  const publicVar = env.NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN; // ✅ OK
}

Troubleshooting

Missing Required Variable

Error: Missing required environment variables:
- STORYBLOK_MANAGEMENT_TOKEN

Solution: Add the variable to .env.local

Invalid Value

Error: Invalid environment variables:
{
  PORT: [ 'Expected number, received string' ]
}

Solution: Check the value type matches the schema

Client-side Access Error

Error: Cannot access server-side env var in client component

Solution: Add NEXT_PUBLIC_ prefix or move logic to server

Integration

Next.js

The env package is automatically imported:

// next.config.ts
import { env } from '@httpjpg/env';

export default {
  env: {
    CUSTOM_VAR: env.MY_VAR,
  },
};

Storyblok Sync

// packages/storyblok-sync/src/api.ts
import { env } from '@httpjpg/env';

const token = env.STORYBLOK_MANAGEMENT_TOKEN;
const spaceId = env.STORYBLOK_SPACE_ID;

API Routes

// app/api/spotify/route.ts
import { env } from '@httpjpg/env';

export async function GET() {
  const clientId = env.SPOTIFY_CLIENT_ID;
  // ... use in server-side logic
}