Next.js環境変数を業務で使いこなす実装パターン完全ガイド

React / Next.js

Next.js環境変数を業務で使いこなす実装パターン完全ガイド

Next.jsを使った実務開発では、環境ごとに異なる設定値を管理する必要があります。本記事では、開発環境・ステージング環境・本番環境での環境変数の使い分けから、実際のAPI連携、認証情報の安全な管理まで、業務で即座に活用できるパターンを解説します。

1. Next.jsの環境変数とは

Next.jsでは、プロジェクトルートに.env.local.env.productionなどのファイルを配置することで、環境ごとに異なる値を設定できます。重要な点は、ブラウザに公開される変数と、サーバー側でのみ使用できる変数を区別する必要があることです。

ブラウザに公開する場合はNEXT_PUBLIC_というプレフィックスをつける必要があります。これを忘れると、APIキーなどの機密情報が意図せずクライアント側に露出してしまいます。

環境変数ファイルの種類

  • .env.local – 全環境で読み込まれる(gitignoreに登録)
  • .env.development – 開発環境のみ
  • .env.production – 本番環境のみ
  • .env.test – テスト実行時のみ
  • .env – デフォルト値(gitに登録可)

2. 業務でのユースケース

シナリオ1: 複数API連携システム

実務では、複数の外部APIを同時に利用することが一般的です。開発環境ではスタブAPIを使い、本番環境では本物のAPIを使うといったパターンが多いです。

シナリオ2: 認証情報の管理

OAuth、JWT、データベース接続文字列など、機密情報を安全に管理する必要があります。開発時は誰でも使える共通キーを、本番環境では個別の認証情報を設定することが重要です。

シナリオ3: ログレベルやデバッグモードの制御

本番環境ではエラーログのみ出力し、開発環境ではデバッグログも出力するといった使い分けが必要です。

3. 実装コード

基本的なセットアップ

まず、プロジェクトルートに環境変数ファイルを作成します。

touch .env.local .env.development .env.production

.env.developmentファイルの例:

NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
NEXT_PUBLIC_APP_ENV=development
API_SECRET_KEY=dev-secret-key-12345
DATABASE_URL=postgresql://localhost:5432/myapp_dev
NEXT_PUBLIC_GA_ID=G-DEV123456
DEBUG_MODE=true

.env.productionファイルの例:

NEXT_PUBLIC_API_BASE_URL=https://api.mycompany.com
NEXT_PUBLIC_APP_ENV=production
API_SECRET_KEY=prod-secret-key-xxxxx
DATABASE_URL=postgresql://prod-server:5432/myapp
NEXT_PUBLIC_GA_ID=G-PROD123456
DEBUG_MODE=false

環境変数の型安全な管理

TypeScriptを使用する場合、環境変数の型を定義することでミスを防ぐことができます。

// lib/config.ts
const requiredEnvVars = [
  'NEXT_PUBLIC_API_BASE_URL',
  'API_SECRET_KEY',
  'DATABASE_URL',
] as const;

type EnvVar = typeof requiredEnvVars[number];

function getEnvVar(key: EnvVar): string {
  const value = process.env[key];
  
  if (!value) {
    throw new Error(`環境変数 ${key} が設定されていません`);
  }
  
  return value;
}

export const config = {
  apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001',
  appEnv: (process.env.NEXT_PUBLIC_APP_ENV || 'development') as 'development' | 'staging' | 'production',
  apiSecretKey: getEnvVar('API_SECRET_KEY'),
  databaseUrl: getEnvVar('DATABASE_URL'),
  isProduction: process.env.NEXT_PUBLIC_APP_ENV === 'production',
  isDevelopment: process.env.NEXT_PUBLIC_APP_ENV === 'development',
  debugMode: process.env.DEBUG_MODE === 'true',
  gaId: process.env.NEXT_PUBLIC_GA_ID,
} as const;

export type Config = typeof config;

API呼び出しの実装

環境変数を使用してAPIクライアントを初期化する実装例です。

// lib/api-client.ts
import { config } from './config';

interface ApiRequestOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  headers?: Record;
  body?: unknown;
  timeout?: number;
}

class ApiClient {
  private baseUrl: string;
  private secretKey: string;
  private timeout: number = 10000;

  constructor() {
    this.baseUrl = config.apiBaseUrl;
    this.secretKey = config.apiSecretKey;
  }

  private buildHeaders(additionalHeaders?: Record): Record {
    const headers: Record = {
      'Content-Type': 'application/json',
      'X-API-Key': this.secretKey,
      'User-Agent': `MyApp/${config.appEnv}`,
    };

    if (additionalHeaders) {
      Object.assign(headers, additionalHeaders);
    }

    return headers;
  }

  async request(
    endpoint: string,
    options: ApiRequestOptions = {}
  ): Promise {
    const url = `${this.baseUrl}${endpoint}`;
    const method = options.method || 'GET';
    const timeout = options.timeout || this.timeout;

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(url, {
        method,
        headers: this.buildHeaders(options.headers),
        body: options.body ? JSON.stringify(options.body) : undefined,
        signal: controller.signal,
      });

      if (!response.ok) {
        throw new Error(`API Error: ${response.status} ${response.statusText}`);
      }

      return await response.json() as T;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  async get(endpoint: string, options?: ApiRequestOptions): Promise {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  async post(endpoint: string, body: unknown, options?: ApiRequestOptions): Promise {
    return this.request(endpoint, { ...options, method: 'POST', body });
  }

  async put(endpoint: string, body: unknown, options?: ApiRequestOptions): Promise {
    return this.request(endpoint, { ...options, method: 'PUT', body });
  }

  async delete(endpoint: string, options?: ApiRequestOptions): Promise {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

export const apiClient = new ApiClient();

サーバーサイドでの環境変数使用

API Routeやサーバーコンポーネントでは、機密情報を含む環境変数を安全に使用できます。

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { config } from '../../lib/config';

interface User {
  id: number;
  name: string;
  email: string;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // 本番環境でのみ実行
  if (!config.isProduction && !('Authorization' in req.headers)) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  try {
    // サーバー側でのみアクセス可能な秘密キーを使用
    const response = await fetch(`${config.apiBaseUrl}/api/users`, {
      headers: {
        'X-API-Secret': config.apiSecretKey,
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      throw new Error(`Upstream API error: ${response.statusText}`);
    }

    const users: User[] = await response.json();
    res.status(200).json(users);
  } catch (error) {
    if (config.debugMode) {
      console.error('API Error:', error);
    }
    res.status(500).json({ error: 'Internal Server Error' });
  }
}

クライアントサイドでの使用

クライアントコンポーネントから環境変数を使用する場合は、NEXT_PUBLIC_プレフィックスの変数のみがアクセス可能です。

// components/UserList.tsx
'use client';

import { useEffect, useState } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

export function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        // /api/users は同一オリジンのため、相対パスで呼び出し
        const response = await fetch('/api/users', {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
          },
        });

        if (!response.ok) {
          throw new Error('Failed to fetch users');
        }

        const data = await response.json();
        setUsers(data);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
        setUsers([]);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return 
読み込み中...
; if (error) return
エラー: {error}
; return (

ユーザー一覧

    {users.map((user) => (
  • {user.name} ({user.email})
  • ))}
); }

複数環境での動的設定

ステージング環境など、複数の環境を管理する場合の実装例です。

// lib/env-config.ts
type Environment = 'development' | 'staging' | 'production';

interface EnvironmentConfig {
  apiBaseUrl: string;
  apiSecretKey: string;
  databaseUrl: string;
  logLevel: 'debug' | 'info' | 'warn' | 'error';
  enableAnalytics: boolean;
  sessionTimeout: number;
  maxRetries: number;
}

const environmentConfigs: Record = {
  development: {
    apiBaseUrl: 'http://localhost:3001',
    apiSecretKey: process.env.API_SECRET_KEY || 'dev-key',
    databaseUrl: 'postgresql://localhost:5432/myapp_dev',
    logLevel: 'debug',
    enableAnalytics: false,
    sessionTimeout: 3600000,
    maxRetries: 3,
  },
  staging: {
    apiBaseUrl: 'https://staging-api.mycompany.com',
    apiSecretKey: process.env.API_SECRET_KEY || '',
    databaseUrl: process.env.DATABASE_URL || '',
    logLevel: 'info',
    enableAnalytics: true,
    sessionTimeout: 1800000,
    maxRetries: 2,
  },
  production: {
    apiBaseUrl: 'https://api.mycompany.com',
    apiSecretKey: process.env.API_SECRET_KEY || '',
    databaseUrl: process.env.DATABASE_URL || '',
    logLevel: 'warn',
    enableAnalytics: true,
    sessionTimeout: 900000,
    maxRetries: 1,
  },
};

function getCurrentEnvironment(): Environment {
  const env = process.env.NEXT_PUBLIC_APP_ENV as Environment;
  return ['development', 'staging', 'production'].includes(env) ? env : 'development';
}

export const envConfig = environmentConfigs[getCurrentEnvironment()];

4. よくある応用パターン

パターン1: .envファイルの検証

アプリケーション起動時に必須の環境変数が設定されているかを確認することは重要です。

// lib/validate-env.ts
export function validateEnvironmentVariables(): void {
  const required = [
    'NEXT_PUBLIC_API_BASE_URL',
    'API_SECRET_KEY',
    'DATABASE_URL',
  ];

  const missing = required.filter((varName) => !process.env[varName]);

  if (missing.length > 0) {
    throw new Error(
      `必須の環境変数が不足しています: ${missing.join(', ')}`
    );
  }

  // 本番環境での追加チェック
  if (process.env.NEXT_PUBLIC_APP_ENV === 'production') {
    if (!process.env.DATABASE_URL) {
      throw new Error('本番環境ではDATABASE_URLが必須です');
    }
  }
}

// next.config.js内などで起動時に実行
validateEnvironmentVariables();

パターン2: キャッシュレイヤーを含む

頻繁にアクセスされるAPI設定をメモリにキャッシュする実装です。

// lib/api-config-cache.ts
import { config } from './config';

interface CachedApiConfig {
  endpoint: string;
  timeout: number;
  retries: number;
  cacheTime: number;
}

class ApiConfigCache {
  private cache = new Map();
  private readonly cacheDuration = 5 * 60 * 1000; // 5分

  getConfig(service: string): CachedApiConfig {
    const cached = this.cache.get(service);

    if (cached && Date.now() - cached.timestamp < this.cacheDuration) {
      return cached.data;
    }

    // キャッシュが無い場合は設定値から構築
    const newConfig: CachedApiConfig = {
      endpoint: `${config.apiBaseUrl}/api/${service}`,
      timeout: 10000,
      retries: config.isProduction ? 1 : 3,
      cacheTime: this.cacheDuration,
    };

    this.cache.set(service, {
      data: newConfig,
      timestamp: Date.now(),
    });

    return newConfig;
  }

  clear(): void {
    this.cache.clear();
  }
}

export const apiConfigCache = new ApiConfigCache();

パターン3: ログシステムの統合

環境に応じたログレベルの制御です。

// lib/logger.ts
import { config } from './config';

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

const logLevelMap: Record = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3,
};

const currentLogLevel = config.debugMode 
  ? logLevelMap.debug 
  : logLevelMap.info;

class Logger {
  private isEnabled(level: LogLevel): boolean {
    return logLevelMap[level] >= currentLogLevel;
  }

  debug(message: string, data?: unknown): void {
    if (this.isEnabled('debug')) {
      console.log(`[DEBUG] ${message}`, data || '');
    }
  }

  info(message: string, data?: unknown): void {
    if (this.isEnabled('info')) {
      console.log(`[INFO] ${message}`, data || '');
    }
  }

  warn(message: string, data?: unknown): void {
    if (this.isEnabled('warn')) {
      console.warn(`[WARN] ${message}`, data || '');
    }
  }

  error(message: string, error?: unknown): void {
    if (this.isEnabled('error')) {
      console.error(`[ERROR] ${message}`, error || '');
    }
  }
}

export const logger = new Logger();

パターン4: 環境に応じた条件付きレンダリング

Reactコンポーネントで環境に応じた表示を切り分ける例です。

// components/DebugPanel.tsx
'use client';

// NEXT_PUBLIC_付きのため、クライアント側でアクセス可能
const isDevelopment = process.env.NEXT_PUBLIC_APP_ENV === 'development';

export function DebugPanel() {
  if (!isDevelopment) {
    return null;
  }

  return (
    

環境: 開発

API Base: {process.env.NEXT_PUBLIC_API_BASE_URL}

); }

5. 注意点と落とし穴

セキュリティ上の注意

重要: APIキーやデータベース接続文字列などの秘密情報には、絶対にNEXT_PUBLIC_をつけてはいけません。これらはサーバー側でのみ使用すべき情報です。

// ❌ 危険: クライアント側に公開される
NEXT_PUBLIC_DATABASE_PASSWORD=secret123

// ✅ 正しい: サーバー側のみ
DATABASE_PASSWORD=secret123

.envファイルのgitignore設定

.env.localファイルは必ずgitignoreに登録して、リポジトリにコミットしないようにしましょう。

// .gitignore に追加
.env.local
.env.*.local
.env.production.local

環境変数の型定義の重要性

環境変数は文字列型で取得されるため、数値や真偽値として扱う場合は明示的に変換する必要があります。

// ❌ 間違い: 文字列「false」は true として評価される
const isDev = process.env.DEBUG_MODE === 'true'; // 正しい

// ✅ 正しい
const isDev = process.env.DEBUG_MODE === 'true';
const port = parseInt(process.env.PORT || '3000', 10);

本番環境へのデプロイ時の確認

Vercelなどのホスティング環境では、環境変数を管理画面で設定する必要があります。

デプロイ前に必ず以下を確認しましょう:

  • すべての必須環境変数が設定されているか
  • 機密情報が誤ってクライアント側に露出していないか
  • 環境ごとに異なる値が正しく設定されているか
  • .env.localが本番環境に含まれていないか

ホットリロード時の動作

Next.jsの開発サーバーでは、.envファイルを変更した場合、サーバーを再起動する必要があります。クライアント側の変数(NEXT_PUBLIC_プレフィックス)は自動でホットリロードされますが、サーバー側の変数は再起動が必要です。

6. 実務のワークフロー例

実際のプロジェクトでの運用フローを想定した例です。

// lib/setup.ts - アプリケーション初期化時に実行
import { validateEnvironmentVariables } from './validate-env';
import { config } from './config';
import { logger } from './logger';

export function setupApplication(): void {
  // 1. 環境変数の検証
  validateEnvironmentVariables();

  // 2. 初期ログ出力
  logger.info('Application starting', {
    environment: config.appEnv,
    nodeEnv: process.env.NODE_ENV,
  });

  // 3. 本番環境の特別な初期化
  if (config.isProduction) {
    // Sentry、DatadogなどのMonitoringツールの初期化
    logger.info('Production mode detected - initializing monitoring');
  }

  logger.info('Application setup completed');
}

このセットアップ関数をNext.jsのカスタム_appやlayout内で呼び出します。

まとめ

Next.jsの環境変数管理は、プロジェクトの規模が大きくなるほど重要になります。本記事で紹介した実装パターンを参考に、以下の点を意識して運用しましょう:

  • セキュリティ: 機密情報にはNEXT_PUBLIC_をつけない
  • 型安全性: TypeScriptを使用して環境変数を型定義する
  • 検証: 起動時に必須の環境変数をチェックする
  • ドキュメント: チーム内で必要な環境変数を明記しておく
  • 本番環境: デプロイサービスの管理画面で環境変数を設定する

環境変数の適切な管理は、チーム開発の効率性とアプリケーションの安全性を大きく向上させます。ぜひプロジェクトに適用してください。

タイトルとURLをコピーしました