セキュリティ

T3 Envで環境変数を型安全に管理する導入ガイド

Next.jsプロジェクトでT3 Envを使って環境変数をZodで検証し、型安全に扱う方法をステップバイステップで解説

環境変数の管理は、アプリケーション開発において避けて通れない課題です。process.env.API_KEY のような参照は型安全ではなく、タイポや未設定の変数によるランタイムエラーの原因になります。T3 Env は、Zod を使って環境変数を検証し、完全な型安全性を提供するライブラリです。

T3 Env とは

T3 Env は、以下の機能を提供する環境変数管理ライブラリです:

  • 型安全性: すべての環境変数に TypeScript の型が付与される
  • ランタイム検証: Zod スキーマによるビルド時・起動時の検証
  • サーバー/クライアント分離: サーバー専用変数がクライアントに漏れることを防止
  • 開発体験の向上: IDE での補完とタイポの即座な検出

ステップ1: インストール

Next.js プロジェクトの場合

# npm
npm install @t3-oss/env-nextjs zod

# pnpm
pnpm add @t3-oss/env-nextjs zod

# yarn
yarn add @t3-oss/env-nextjs zod

# bun
bun add @t3-oss/env-nextjs zod

その他のフレームワークの場合

# コアパッケージを使用
npm install @t3-oss/env-core zod

ステップ2: 環境変数スキーマの定義

基本的な設定ファイルの作成

src/env.ts(または env.ts)を作成します:

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

export const env = createEnv({
  /**
   * サーバーサイド専用の環境変数
   * クライアントからアクセスするとエラーになります
   */
  server: {
    DATABASE_URL: z.string().url(),
    API_SECRET_KEY: z.string().min(1),
    NODE_ENV: z.enum(["development", "production", "test"]),
  },

  /**
   * クライアントサイドでも使用可能な環境変数
   * NEXT_PUBLIC_ プレフィックスが必須です
   */
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
    NEXT_PUBLIC_SITE_NAME: z.string().min(1),
  },

  /**
   * Next.js 13.4.4 以降では experimental__runtimeEnv を使用
   * クライアント変数のみ指定すれば OK
   */
  experimental__runtimeEnv: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME,
  },
});

Next.js 13.4.4 未満の場合

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

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  // すべての変数を手動で指定
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
});

ステップ3: アプリケーションでの使用

インポートして使用

import { env } from "@/env";

// サーバーコンポーネント・API ルートで使用
export async function GET() {
  const dbUrl = env.DATABASE_URL; // 型: string
  // ...
}

// クライアントコンポーネントで使用
export default function Component() {
  return <div>API: {env.NEXT_PUBLIC_API_URL}</div>;
}

サーバー変数へのクライアントアクセス防止

// クライアントコンポーネントで以下を実行すると
// ランタイムエラーが発生します
const secret = env.API_SECRET_KEY; // エラー!

ステップ4: 高度なスキーマ定義

数値への変換

server: {
  PORT: z
    .string()
    .transform((s) => parseInt(s, 10))
    .pipe(z.number()),

  // または Zod の coerce を使用
  MAX_CONNECTIONS: z.coerce.number(),
}

真偽値への変換

server: {
  // "true" / "false" のみ許可
  ENABLE_FEATURE: z
    .string()
    .refine((s) => s === "true" || s === "false")
    .transform((s) => s === "true"),

  // より緩い変換("0" や "false" を false に)
  DEBUG_MODE: z
    .string()
    .transform((s) => s !== "false" && s !== "0"),
}

オプショナルな変数

server: {
  // 未設定でも OK
  OPTIONAL_KEY: z.string().optional(),

  // デフォルト値を設定
  LOG_LEVEL: z.string().default("info"),
}

特定のパターンの検証

server: {
  // sk_ で始まる文字列のみ許可
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),

  // メールアドレス形式
  ADMIN_EMAIL: z.string().email(),

  // URL 形式
  WEBHOOK_URL: z.string().url(),
}

ステップ5: 空文字列の処理

.env ファイルで PORT= のように空文字列が設定されている場合の対処:

export const env = createEnv({
  server: {
    PORT: z.coerce.number().default(3000),
  },
  // 空文字列を undefined として扱う(推奨)
  emptyStringAsUndefined: true,
  // ...
});

ステップ6: エラーハンドリングのカスタマイズ

バリデーションエラーのカスタマイズ

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
  },
  // バリデーション失敗時のハンドラー
  onValidationError: (issues) => {
    console.error("❌ 環境変数の検証に失敗しました:");
    for (const issue of issues) {
      const path = issue.path?.join(".") || "unknown";
      console.error(`  - ${path}: ${issue.message}`);
    }
    process.exit(1);
  },
  // クライアントからサーバー変数へのアクセス時
  onInvalidAccess: (variable) => {
    throw new Error(
      `🚨 サーバー変数 "${variable}" にクライアントからアクセスしようとしました`
    );
  },
  // ...
});

ステップ7: バリデーションのスキップ

Docker ビルド時など、すべての環境変数が揃っていない状況での対処:

export const env = createEnv({
  // ...
  skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
});

注意: バリデーションをスキップすると、型とランタイム値が一致しなくなる可能性があります。本番環境では必ず検証を有効にしてください。

ステップ8: プリセットの使用

Vercel プリセット

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

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  // Vercel の環境変数を自動的に追加
  extends: [vercel()],
  experimental__runtimeEnv: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
});

// Vercel 固有の変数にアクセス可能
console.log(env.VERCEL_URL);
console.log(env.VERCEL_ENV);

利用可能なプリセット

プリセット説明
vercel()Vercel プラットフォーム変数
railway()Railway プラットフォーム
render()Render.com
fly()Fly.io
netlify()Netlify
upstashRedis()Upstash Redis
uploadthingV6()UploadThing v6

ステップ9: 複数の設定を組み合わせる

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

// データベース用のプリセット
const databaseEnv = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    DATABASE_POOL_SIZE: z.coerce.number().default(10),
  },
  runtimeEnv: process.env,
});

// 認証用のプリセット
const authEnv = createEnv({
  server: {
    AUTH_SECRET: z.string().min(32),
    AUTH_URL: z.string().url(),
  },
  runtimeEnv: process.env,
});

// メインの設定
export const env = createEnv({
  server: {
    APP_NAME: z.string(),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  extends: [vercel(), databaseEnv, authEnv],
  experimental__runtimeEnv: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
});

// すべての変数にアクセス可能
env.DATABASE_URL;
env.AUTH_SECRET;
env.APP_NAME;
env.VERCEL_URL;

ステップ10: 推奨されるプロジェクト構成

ファイル構成

src/
├── env.ts           # T3 Env の設定
├── app/
│   ├── layout.tsx
│   └── page.tsx
└── lib/
    └── db.ts        # env を使用

.env ファイルの例

# .env.local(開発用)

# サーバー変数
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=dev-secret-key-12345

# クライアント変数
NEXT_PUBLIC_API_URL=http://localhost:3000/api
NEXT_PUBLIC_SITE_NAME=My App

.env.example の作成

# .env.example(テンプレート)

# サーバー変数
DATABASE_URL=
API_SECRET_KEY=

# クライアント変数
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_SITE_NAME=

トラブルシューティング

ビルド時にエラーが発生する

環境変数が設定されていない場合、ビルドが失敗します:

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

解決策: CI/CD 環境で必要な環境変数を設定するか、skipValidation を使用してください。

クライアントで環境変数が undefined になる

NEXT_PUBLIC_ プレフィックスが付いているか確認してください:

// ❌ 間違い - クライアントからアクセスできない
client: {
  API_URL: z.string().url(),
}

// ✅ 正しい
client: {
  NEXT_PUBLIC_API_URL: z.string().url(),
}

型エラー: Property does not exist

env.ts が正しくインポートされているか確認してください:

// ❌ 間違い
import env from "./env";

// ✅ 正しい
import { env } from "./env";

まとめ

T3 Env を導入することで:

  1. 型安全性: すべての環境変数に型が付き、IDE 補完が効く
  2. 早期エラー検出: 起動時に設定ミスを検出できる
  3. セキュリティ向上: サーバー変数がクライアントに漏れることを防止
  4. 保守性向上: 環境変数のドキュメントとしても機能

既存プロジェクトへの導入も簡単で、段階的に移行することも可能です。まずは重要な環境変数から始めて、徐々にすべての変数を T3 Env で管理するようにしましょう。