Documentation Next.js

はじめに

Next.js App Routerでは、Server ComponentsとClient Componentsで環境変数の扱いが異なります。この記事では、安全で効率的な環境変数管理のベストプラクティスを解説します。

環境変数の種類

種類プレフィックスアクセス可能な場所
サーバー専用なしServer Components, API Routes, Server Actions
公開用NEXT_PUBLIC_すべて(クライアント含む)

環境変数ファイル

ファイルの優先順位

# 優先順位(高い順)
.env.$(NODE_ENV).local  # .env.development.local, .env.production.local
.env.local              # ローカル環境用(gitignore推奨)
.env.$(NODE_ENV)        # .env.development, .env.production
.env                    # デフォルト

環境別ファイル例

# .env(デフォルト値)
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://localhost:5432/myapp
# .env.development
NEXT_PUBLIC_API_URL=http://localhost:3001
DATABASE_URL=postgresql://localhost:5432/myapp_dev
LOG_LEVEL=debug
# .env.production
NEXT_PUBLIC_API_URL=https://api.production.example.com
# 本番のDATABASE_URLはVercelなどで設定
# .env.local(gitignore必須)
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_xxxx
OPENAI_API_KEY=sk-xxxx

.gitignore設定

# .gitignore
.env*.local
.env.local
.env.development.local
.env.test.local
.env.production.local

型安全な環境変数管理

Zodによるバリデーション

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  // サーバー専用(必須)
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  OPENAI_API_KEY: z.string().startsWith('sk-'),

  // サーバー専用(オプション)
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  REDIS_URL: z.string().url().optional(),

  // 公開用
  NEXT_PUBLIC_APP_NAME: z.string().default('MyApp'),
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_ENABLE_ANALYTICS: z
    .string()
    .transform((val) => val === 'true')
    .default('false'),

  // ビルド情報
  VERCEL_GIT_COMMIT_SHA: z.string().optional(),
  VERCEL_ENV: z.enum(['production', 'preview', 'development']).optional(),
});

// 型エクスポート
export type Env = z.infer<typeof envSchema>;

// バリデーション関数
function validateEnv(): Env {
  const parsed = envSchema.safeParse(process.env);

  if (!parsed.success) {
    console.error('❌ Invalid environment variables:');
    console.error(parsed.error.flatten().fieldErrors);
    throw new Error('Invalid environment variables');
  }

  return parsed.data;
}

// シングルトンでエクスポート
export const env = validateEnv();

使用例

// Server Component
import { env } from '@/lib/env';

export default async function Page() {
  // 型安全にアクセス
  const apiKey = env.STRIPE_SECRET_KEY; // string型
  const logLevel = env.LOG_LEVEL;       // 'debug' | 'info' | 'warn' | 'error'

  return <div>App: {env.NEXT_PUBLIC_APP_NAME}</div>;
}

クライアント用環境変数

// lib/env-client.ts
import { z } from 'zod';

const clientEnvSchema = z.object({
  NEXT_PUBLIC_APP_NAME: z.string().default('MyApp'),
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_ENABLE_ANALYTICS: z
    .string()
    .transform((val) => val === 'true')
    .default('false'),
});

export type ClientEnv = z.infer<typeof clientEnvSchema>;

export const clientEnv: ClientEnv = {
  NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME || 'MyApp',
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || '',
  NEXT_PUBLIC_ENABLE_ANALYTICS:
    process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
};

Server Componentsでの使用

// app/settings/page.tsx
import { env } from '@/lib/env';

// Server Componentでは直接process.envにアクセス可能
async function getConfig() {
  const response = await fetch(env.NEXT_PUBLIC_API_URL + '/config', {
    headers: {
      Authorization: `Bearer ${env.STRIPE_SECRET_KEY}`,
    },
  });
  return response.json();
}

export default async function SettingsPage() {
  const config = await getConfig();

  return (
    <div>
      <h1>{env.NEXT_PUBLIC_APP_NAME}の設定</h1>
      {/* ... */}
    </div>
  );
}

Server Actionsでの使用

// actions/payment.ts
'use server';

import { env } from '@/lib/env';
import Stripe from 'stripe';

const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-06-20',
});

export async function createPaymentIntent(amount: number) {
  // Server Actionではサーバー専用の環境変数にアクセス可能
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: 'jpy',
  });

  return { clientSecret: paymentIntent.client_secret };
}

Route Handlersでの使用

// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { env } from '@/lib/env';

export async function POST(request: NextRequest) {
  const signature = request.headers.get('stripe-signature');

  // サーバー専用の環境変数を使用
  const webhookSecret = env.STRIPE_WEBHOOK_SECRET;

  // Webhook処理...

  return NextResponse.json({ received: true });
}

Vercelでの環境変数設定

環境ごとの設定

# Vercel CLI
vercel env add DATABASE_URL production
vercel env add DATABASE_URL preview
vercel env add DATABASE_URL development

# 一覧表示
vercel env ls

# 取得
vercel env pull .env.local

vercel.json設定

{
  "env": {
    "NEXT_PUBLIC_APP_NAME": "MyApp"
  },
  "build": {
    "env": {
      "NEXT_PUBLIC_BUILD_ID": "@build-id"
    }
  }
}

ランタイム環境変数

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // ビルド時に埋め込まれる環境変数
  env: {
    NEXT_PUBLIC_BUILD_TIME: new Date().toISOString(),
  },

  // 実験的機能: サーバーランタイムで読み込まれる環境変数
  experimental: {
    serverActions: {
      bodySizeLimit: '2mb',
    },
  },
};

module.exports = nextConfig;

セキュリティベストプラクティス

機密情報の保護

// ❌ 危険な例
const apiKey = process.env.NEXT_PUBLIC_API_KEY; // クライアントに露出

// ✅ 安全な例
// Server Component / Server Action / Route Handler内
const apiKey = process.env.API_KEY; // サーバーのみ

環境変数の露出チェック

// scripts/check-env-exposure.ts
const dangerousPatterns = [
  /NEXT_PUBLIC_.*SECRET/i,
  /NEXT_PUBLIC_.*KEY/i,
  /NEXT_PUBLIC_.*PASSWORD/i,
  /NEXT_PUBLIC_.*TOKEN/i,
];

function checkEnvExposure() {
  const exposed: string[] = [];

  for (const key of Object.keys(process.env)) {
    if (key.startsWith('NEXT_PUBLIC_')) {
      for (const pattern of dangerousPatterns) {
        if (pattern.test(key)) {
          exposed.push(key);
        }
      }
    }
  }

  if (exposed.length > 0) {
    console.error('⚠️ 機密情報が公開される可能性があります:');
    exposed.forEach((key) => console.error(`  - ${key}`));
    process.exit(1);
  }

  console.log('✅ 環境変数のセキュリティチェック完了');
}

checkEnvExposure();

CI/CDでの設定

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check env exposure
        run: npx tsx scripts/check-env-exposure.ts

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v20
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

開発環境のセットアップスクリプト

// scripts/setup-env.ts
import fs from 'fs';
import readline from 'readline';

const requiredEnvVars = [
  { key: 'DATABASE_URL', description: 'PostgreSQL接続URL' },
  { key: 'STRIPE_SECRET_KEY', description: 'Stripeシークレットキー' },
  { key: 'OPENAI_API_KEY', description: 'OpenAI APIキー' },
];

async function setupEnv() {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  const question = (prompt: string): Promise<string> => {
    return new Promise((resolve) => {
      rl.question(prompt, resolve);
    });
  };

  let envContent = '';

  for (const { key, description } of requiredEnvVars) {
    const value = await question(`${description} (${key}): `);
    if (value) {
      envContent += `${key}=${value}\n`;
    }
  }

  fs.writeFileSync('.env.local', envContent);
  console.log('✅ .env.local を作成しました');

  rl.close();
}

setupEnv();

デバッグ用ユーティリティ

// lib/debug-env.ts
export function logEnvStatus() {
  if (process.env.NODE_ENV !== 'development') return;

  const serverOnlyVars = [
    'DATABASE_URL',
    'STRIPE_SECRET_KEY',
    'OPENAI_API_KEY',
  ];

  const publicVars = Object.keys(process.env).filter((key) =>
    key.startsWith('NEXT_PUBLIC_')
  );

  console.log('📦 Server-only environment variables:');
  serverOnlyVars.forEach((key) => {
    const value = process.env[key];
    console.log(`  ${key}: ${value ? '✅ Set' : '❌ Missing'}`);
  });

  console.log('\n🌍 Public environment variables:');
  publicVars.forEach((key) => {
    console.log(`  ${key}: ${process.env[key]}`);
  });
}

まとめ

変数の種類用途保存場所
DATABASE_URLDB接続.env.local / Vercel
API_SECRET外部API.env.local / Vercel
NEXT_PUBLIC_*クライアント設定.env / Vercel

参考文献

円