T-CREATOR

Nuxt 500 の犯人はどこ?server/api で起きる例外・CORS・runtimeConfig の切り分け

Nuxt 500 の犯人はどこ?server/api で起きる例外・CORS・runtimeConfig の切り分け

Nuxt 3 でサーバーサイド API を実装していると、突然の 500 Internal Server Error に遭遇することがあります。「さっきまで動いていたのに…」と悩む瞬間、誰もが経験しますよね。

この記事では、Nuxt の server​/​api ディレクトリで起きる 500 エラーについて、例外処理CORS 問題runtimeConfig の設定ミス という 3 つの主要な原因を徹底的に切り分ける方法をご紹介します。エラーログの読み方から、実践的なデバッグ手順まで、初心者の方でも安心して問題を解決できるよう、丁寧に解説していきましょう。

背景

Nuxt 3 における server/api の役割

Nuxt 3 では、server​/​api ディレクトリにファイルを配置するだけで、自動的に API エンドポイントが生成されます。この仕組みは Nitro という強力なサーバーエンジンによって実現されており、従来の Express や Koa のような複雑な設定を必要としません。

typescript// server/api/hello.ts の例
export default defineEventHandler((event) => {
  return {
    message: 'Hello from Nuxt API!',
  };
});

このシンプルさの裏側では、H3 というミニマルな HTTP フレームワークが動作しており、ルーティング・リクエスト処理・レスポンス生成を自動で行ってくれます。

しかし、このシンプルさゆえに、エラーが発生したときの原因特定が難しくなることもあるのです。

500 エラーが起きる典型的なシチュエーション

実際の開発現場では、以下のようなシチュエーションで 500 エラーに遭遇することが多いですね。

typescript// 外部 API を呼び出すエンドポイント
export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  const response = await fetch(config.apiEndpoint); // ← ここで問題が起きやすい
  return response.json();
});

上記のコードは一見問題なさそうに見えますが、実は複数の潜在的なエラー要因を抱えています。

次の図は、Nuxt の server/api でリクエストが処理される基本的なフローを示したものです。

mermaidflowchart TD
  client["クライアント<br/>(ブラウザ)"] -->|HTTP リクエスト| nitro["Nitroサーバー"]
  nitro -->|ルーティング| handler["EventHandler<br/>(server/api/*.ts)"]
  handler -->|処理実行| logic["ビジネスロジック"]
  logic -->|外部API呼び出し| external["外部サービス"]
  external -->|レスポンス/エラー| logic
  logic -->|例外発生?| error_check{エラーハンドリング}
  error_check -->|未処理例外| error_500["500エラー"]
  error_check -->|正常処理| response["JSONレスポンス"]
  error_500 -->|エラーログ| client
  response -->|成功レスポンス| client

このフローのどの段階でも、適切なエラーハンドリングが欠けていると 500 エラーに直結してしまうのです。

図で理解できる要点

  • Nitro サーバーがすべてのリクエストを受け取り、適切な EventHandler へルーティングする
  • 外部 API 呼び出しや DB アクセスなど、非同期処理で例外が発生しやすい
  • エラーハンドリングが不十分だと、未処理例外が 500 エラーとなってクライアントに返される

課題

エラー原因の特定が困難な理由

Nuxt 3 の server​/​api で 500 エラーが発生したとき、まず直面するのが「どこで何が起きているのかわからない」という問題です。

ブラウザのコンソールには単に 500 Internal Server Error と表示されるだけで、具体的なエラーメッセージやスタックトレースが見えないことがあります。

bash# ブラウザコンソールに表示される典型的なエラー
GET http://localhost:3000/api/users 500 (Internal Server Error)

これは、サーバーサイドで発生したエラーが、セキュリティ上の理由からクライアントに詳細を送信しないためです。

複数の原因が混在する複雑さ

500 エラーの原因は一つとは限りません。実際のプロジェクトでは、以下のような問題が同時に存在することもあります。

#原因カテゴリ典型的な症状発見の難しさ
1例外処理の欠如try-catch がなく未処理例外が発生★★★
2CORS 設定ミスブラウザが OPTIONS リクエストで失敗★★
3runtimeConfig の誤設定環境変数が undefined になる★★★
4非同期処理の待機忘れPromise を await せずに返す★★
5型エラーTypeScript の型不一致

これらの問題が複合的に絡み合うと、デバッグに数時間を費やすことも珍しくありません。

次の図は、500 エラーの主要な 3 つの原因とその関係性を示しています。

mermaidflowchart LR
  error_500["500 Internal<br/>Server Error"] --> cause1["原因1:<br/>例外処理の欠如"]
  error_500 --> cause2["原因2:<br/>CORS問題"]
  error_500 --> cause3["原因3:<br/>runtimeConfig誤設定"]

  cause1 --> symptom1["未処理のPromise<br/>Rejection"]
  cause1 --> symptom2["try-catchの<br/>スコープ外エラー"]

  cause2 --> symptom3["OPTIONSリクエスト<br/>失敗"]
  cause2 --> symptom4["Access-Control-Allow-<br/>Origin不足"]

  cause3 --> symptom5["undefined参照<br/>エラー"]
  cause3 --> symptom6["環境変数の<br/>読み込み失敗"]

図で理解できる要点

  • 500 エラーは単一原因ではなく、複数の要因が絡み合って発生する
  • 例外処理・CORS・runtimeConfig という 3 大要因が特に重要
  • それぞれの要因にさらに細かい症状があり、系統的な切り分けが必要

開発環境と本番環境での挙動の違い

開発中は問題なく動作していたのに、本番環境にデプロイすると 500 エラーが頻発する、というケースも多く見られます。

これは、以下の環境差異が原因です。

typescript// 開発環境では動作するが本番では失敗する例
export default defineEventHandler(async (event) => {
  // 開発環境では process.env が直接使える
  const apiKey = process.env.API_KEY; // ← 本番では undefined になる可能性

  const response = await fetch(
    `https://api.example.com/data`,
    {
      headers: { 'X-API-Key': apiKey }, // ← apiKey が undefined だとエラー
    }
  );

  return response.json();
});

このように、環境依存の問題は開発中に気づきにくく、リリース後に発覚することが多いのです。

解決策

基本方針:ログ出力による原因の可視化

500 エラーを解決する第一歩は、何が起きているかを正確に把握することです。Nuxt の開発サーバーを起動しているターミナルには、サーバーサイドのログが出力されます。

bash# 開発サーバーを起動してログを確認
yarn dev

エラーが発生したときは、必ずターミナルのログを確認しましょう。そこには詳細なスタックトレースとエラーメッセージが表示されているはずです。

bash# 典型的なエラーログの例
[nuxt] [request error] [unhandled] [500] Cannot read property 'data' of undefined
  at /path/to/server/api/users.ts:15:23
  at processTicksAndRejections (node:internal/process/task_queues:96:5)

このログから、ファイル名users.ts)と行番号(15 行目)を特定できます。

解決策 1:例外処理の徹底実装

最も基本的でありながら、最も重要なのが try-catch による例外処理 です。すべての非同期処理を try-catch で囲むことで、未処理例外を防ぎます。

基本的な try-catch パターン

typescript// server/api/users.ts
export default defineEventHandler(async (event) => {
  try {
    // 外部 API 呼び出しなどの非同期処理
    const response = await fetch(
      'https://api.example.com/users'
    );

    // レスポンスのステータスチェック
    if (!response.ok) {
      throw new Error(
        `API responded with status ${response.status}`
      );
    }

    const data = await response.json();
    return data;
  } catch (error) {
    // エラーを適切にログ出力
    console.error('Error fetching users:', error);

    // クライアントに返すエラーレスポンス
    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to fetch users',
    });
  }
});

上記のコードでは、try ブロック内で外部 API を呼び出し、何らかのエラーが発生した場合は catch ブロックでキャッチしています。

createError は Nuxt が提供するヘルパー関数で、適切な HTTP エラーレスポンスを生成してくれます。

エラーの型安全性を高める

TypeScript を使っている場合、エラーオブジェクトの型を明示的に扱うことで、より堅牢なコードになります。

typescript// server/api/safe-users.ts
import type { H3Error } from 'h3';

export default defineEventHandler(async (event) => {
  try {
    const response = await fetch(
      'https://api.example.com/users'
    );

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

    return await response.json();
  } catch (error) {
    // error の型を絞り込む
    if (error instanceof Error) {
      console.error('API Error:', error.message);
      console.error('Stack:', error.stack);
    } else {
      console.error('Unknown error:', error);
    }

    // 詳細なエラー情報を返す
    throw createError({
      statusCode: 500,
      statusMessage: 'Internal Server Error',
      data: {
        message:
          error instanceof Error
            ? error.message
            : 'Unknown error occurred',
      },
    });
  }
});

このコードでは、error instanceof Error でエラーオブジェクトの型を確認し、適切なプロパティにアクセスしています。

複数の例外パターンへの対応

実際のアプリケーションでは、複数の種類のエラーが発生する可能性があります。それぞれを適切に処理するには、エラーの種類ごとに分岐させます。

typescript// server/api/detailed-error-handling.ts
export default defineEventHandler(async (event) => {
  try {
    const response = await fetch(
      'https://api.example.com/users'
    );

    if (!response.ok) {
      // HTTP ステータスに応じてエラーを分類
      if (response.status === 404) {
        throw createError({
          statusCode: 404,
          statusMessage: 'Users not found',
        });
      }

      if (response.status === 401) {
        throw createError({
          statusCode: 401,
          statusMessage: 'Unauthorized access to API',
        });
      }

      // その他の HTTP エラー
      throw new Error(`API error: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    // H3Error(Nuxtのエラー)はそのまま再スロー
    if (
      error &&
      typeof error === 'object' &&
      'statusCode' in error
    ) {
      throw error;
    }

    // ネットワークエラーなどの一般的なエラー
    console.error('Unexpected error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Internal Server Error',
    });
  }
});

このパターンでは、404 や 401 などの特定の HTTP ステータスに対して、適切なエラーレスポンスを返しています。

解決策 2:CORS 問題の正確な診断と対処

CORS(Cross-Origin Resource Sharing)は、異なるオリジンからの API アクセスを制御するブラウザのセキュリティ機構です。Nuxt の server​/​api を外部ドメインのフロントエンドから呼び出す場合、CORS 設定が必要になります。

次の図は、CORS のリクエストフローを示したものです。

mermaidsequenceDiagram
  participant Browser as ブラウザ
  participant Nuxt as Nuxt Server<br/>(server/api)

  Note over Browser,Nuxt: 1. Preflightリクエスト
  Browser->>Nuxt: OPTIONS /api/users<br/>Origin: https://example.com

  alt CORS設定が正しい場合
    Nuxt->>Browser: 200 OK<br/>Access-Control-Allow-Origin: *<br/>Access-Control-Allow-Methods: GET, POST
    Note over Browser: Preflight成功

    Note over Browser,Nuxt: 2. 実際のリクエスト
    Browser->>Nuxt: GET /api/users
    Nuxt->>Browser: 200 OK + データ
  else CORS設定がない場合
    Nuxt->>Browser: 200 OK<br/>(CORSヘッダーなし)
    Note over Browser: ブラウザがブロック<br/>500エラー相当の扱い
  end

CORS エラーの見分け方

CORS 問題かどうかは、ブラウザのコンソールに特徴的なエラーメッセージが表示されるかで判断できます。

bash# 典型的な CORS エラーメッセージ(Chrome)
Access to fetch at 'http://localhost:3000/api/users' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

また、ブラウザの開発者ツールのネットワークタブを開くと、OPTIONS メソッドのリクエストが失敗しているのが確認できます。

nuxt.config.ts での CORS 設定

Nuxt 3 では、nuxt.config.ts で CORS ヘッダーを設定できます。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/api/**': {
        cors: true,
        headers: {
          'Access-Control-Allow-Methods':
            'GET, POST, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers':
            'Content-Type, Authorization',
        },
      },
    },
  },
});

この設定により、​/​api 配下のすべてのエンドポイントに対して CORS ヘッダーが自動的に付与されます。

Access-Control-Allow-Origin: * は、すべてのオリジンからのアクセスを許可する設定ですが、本番環境では特定のドメインに限定することをおすすめします。

個別エンドポイントでの CORS 設定

特定のエンドポイントだけに CORS を適用したい場合は、setResponseHeaders を使用します。

typescript// server/api/public-data.ts
export default defineEventHandler(async (event) => {
  // CORS ヘッダーを設定
  setResponseHeaders(event, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
  });

  // OPTIONS リクエスト(Preflight)への対応
  if (event.method === 'OPTIONS') {
    return 'OK';
  }

  // 実際のデータ処理
  return {
    data: 'Public data accessible from any origin',
  };
});

event.method === 'OPTIONS' の分岐は、ブラウザが送信する Preflight リクエストに応答するためのものです。この処理がないと、CORS を必要とするリクエストが失敗してしまいます。

図で理解できる要点

  • ブラウザは実際のリクエスト前に OPTIONS リクエスト(Preflight)を送信する
  • サーバーが適切な CORS ヘッダーを返さないと、ブラウザがリクエストをブロックする
  • Nuxt では nuxt.config.ts または個別の EventHandler で CORS を設定可能

解決策 3:runtimeConfig の正しい使い方

runtimeConfig は、Nuxt で環境変数を安全に扱うための仕組みです。しかし、設定ミスによって 500 エラーが発生することがよくあります。

runtimeConfig の基本構造

typescript// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // サーバーサイドでのみアクセス可能(秘密情報)
    apiSecret: process.env.API_SECRET || '',

    // public 配下はクライアントサイドでもアクセス可能
    public: {
      apiBase:
        process.env.API_BASE || 'https://api.example.com',
    },
  },
});

runtimeConfig の直下に定義した変数は、サーバーサイド(server​/​api 内)でのみアクセスできます。クライアントサイドでも使いたい値は、必ず public 配下に配置しましょう。

server/api での正しい使用方法

typescript// server/api/secure-endpoint.ts
export default defineEventHandler(async (event) => {
  // runtimeConfig を取得
  const config = useRuntimeConfig();

  // サーバーサイド専用の値を使用
  const apiSecret = config.apiSecret;

  // 値が設定されているか確認(重要!)
  if (!apiSecret) {
    console.error('API_SECRET is not configured');
    throw createError({
      statusCode: 500,
      statusMessage: 'Server configuration error',
    });
  }

  try {
    // 外部 API に秘密鍵を渡す
    const response = await fetch(
      'https://api.example.com/secure-data',
      {
        headers: {
          Authorization: `Bearer ${apiSecret}`,
        },
      }
    );

    return await response.json();
  } catch (error) {
    console.error('API call failed:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to fetch secure data',
    });
  }
});

if (!apiSecret) のチェックが非常に重要です。このチェックがないと、apiSecretundefined のまま API 呼び出しが行われ、予期しないエラーが発生します。

環境変数ファイルの設定

開発環境では、プロジェクトルートに .env ファイルを作成して環境変数を定義します。

bash# .env(開発環境用)
API_SECRET=your-secret-key-here
API_BASE=https://api.example.com

このファイルは Git にコミットしないよう、.gitignore に必ず追加してください。

bash# .gitignore
.env
.env.*
!.env.example

本番環境では、ホスティングサービスの環境変数設定画面から同じ変数名で設定します。

型安全性を高める runtimeConfig の定義

TypeScript で型安全に runtimeConfig を扱うには、型定義を追加します。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',
    databaseUrl: '',
    public: {
      apiBase: '',
    },
  },
});
typescript// types/runtime-config.d.ts
declare module 'nuxt/schema' {
  interface RuntimeConfig {
    apiSecret: string;
    databaseUrl: string;
  }
  interface PublicRuntimeConfig {
    apiBase: string;
  }
}

export {};

この型定義により、useRuntimeConfig() の戻り値が適切に型付けされ、存在しない設定にアクセスしようとするとコンパイルエラーになります。

具体例

ケーススタディ 1:未処理 Promise による 500 エラー

実際のプロジェクトで遭遇した、未処理 Promise が原因の 500 エラーを見てみましょう。

問題のコード

typescript// server/api/users/[id].ts(エラーが発生するコード)
export default defineEventHandler(async (event) => {
  const id = event.context.params?.id;

  // Promise を await せずに返している
  const user = fetchUserById(id);

  // user は Promise オブジェクトのまま返される
  return user;
});
typescript// server/utils/user.ts
async function fetchUserById(id: string) {
  const response = await fetch(
    `https://api.example.com/users/${id}`
  );
  return response.json();
}

このコードは、fetchUserByIdasync 関数なのに、呼び出し側で await を忘れています。その結果、Promise オブジェクトがそのまま返され、クライアントは期待したデータを受け取れません。

エラーログ

bash# ターミナルに表示されるエラー
[nuxt] [request error] [unhandled] [500] [object Promise]
  at /server/api/users/[id].ts:6:10

このエラーメッセージの [object Promise] という部分が、Promise の待機忘れを示す重要な手がかりです。

修正方法

typescript// server/api/users/[id].ts(修正版)
export default defineEventHandler(async (event) => {
  try {
    const id = event.context.params?.id;

    // ID が存在するか確認
    if (!id) {
      throw createError({
        statusCode: 400,
        statusMessage: 'User ID is required',
      });
    }

    // await を追加して Promise を解決
    const user = await fetchUserById(id);

    // ユーザーが見つからない場合の処理
    if (!user) {
      throw createError({
        statusCode: 404,
        statusMessage: 'User not found',
      });
    }

    return user;
  } catch (error) {
    // すでに createError で生成されたエラーはそのまま再スロー
    if (
      error &&
      typeof error === 'object' &&
      'statusCode' in error
    ) {
      throw error;
    }

    // その他のエラー
    console.error('Error fetching user:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to fetch user',
    });
  }
});

修正版では、以下の改善が施されています。

  • await を追加して Promise を正しく解決する
  • ID の存在チェックを追加し、不正なリクエストに 400 エラーで応答する
  • ユーザーが見つからない場合に 404 エラーを返す
  • try-catch で予期しないエラーをキャッチする

エラーハンドリングのフロー図

次の図は、修正版のコードにおけるエラーハンドリングの流れを示しています。

mermaidflowchart TD
  start["リクエスト受信"] --> check_id{IDパラメータ<br/>存在する?}
  check_id -->|No| error_400["400エラー:<br/>Bad Request"]
  check_id -->|Yes| fetch_user["fetchUserById<br/>を await で実行"]

  fetch_user --> check_result{ユーザーが<br/>見つかった?}
  check_result -->|No| error_404["404エラー:<br/>Not Found"]
  check_result -->|Yes| return_user["ユーザーデータ<br/>を返す"]

  fetch_user -->|例外発生| catch_block["catchブロック"]
  catch_block --> is_h3_error{H3Errorか?}
  is_h3_error -->|Yes| rethrow["エラーを<br/>そのまま再スロー"]
  is_h3_error -->|No| error_500["500エラー:<br/>Internal Server Error"]

  error_400 --> client["クライアントに<br/>レスポンス"]
  error_404 --> client
  error_500 --> client
  return_user --> client
  rethrow --> client

この図から、複数のエラーケースがそれぞれ適切に処理されていることがわかります。

ケーススタディ 2:CORS と認証の複合問題

外部フロントエンドから Nuxt API を呼び出す際に、CORS と認証ヘッダーの両方が必要なケースです。

問題の状況

SPA(Single Page Application)フロントエンドが https:​/​​/​app.example.com でホストされており、Nuxt のバックエンド API が https:​/​​/​api.example.com で動作しています。

フロントエンドから認証トークン付きで API を呼び出すと、500 エラーが発生します。

javascript// フロントエンド側のコード(Vue、React など)
const response = await fetch(
  'https://api.example.com/api/profile',
  {
    headers: {
      Authorization: 'Bearer token-here',
      'Content-Type': 'application/json',
    },
  }
);

// エラー: CORS policy blocking

エラーメッセージ

bash# ブラウザコンソールのエラー
Access to fetch at 'https://api.example.com/api/profile' from origin
'https://app.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

解決方法:認証付き CORS の設定

typescript// server/api/profile.ts
export default defineEventHandler(async (event) => {
  // CORS ヘッダーを設定(認証ヘッダーを許可)
  setResponseHeaders(event, {
    'Access-Control-Allow-Origin':
      'https://app.example.com', // 特定のオリジンに限定
    'Access-Control-Allow-Methods':
      'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers':
      'Content-Type, Authorization', // Authorization を明示
    'Access-Control-Allow-Credentials': 'true', // Cookie や認証情報を許可
  });

  // Preflight リクエストへの応答
  if (event.method === 'OPTIONS') {
    return '';
  }

  try {
    // Authorization ヘッダーを取得
    const authHeader = getHeader(event, 'authorization');

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw createError({
        statusCode: 401,
        statusMessage:
          'Unauthorized: Missing or invalid token',
      });
    }

    // トークンを抽出
    const token = authHeader.substring(7);

    // トークンを検証(実際はJWT検証など)
    const user = await validateToken(token);

    if (!user) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized: Invalid token',
      });
    }

    // ユーザープロファイルを返す
    return {
      id: user.id,
      name: user.name,
      email: user.email,
    };
  } catch (error) {
    if (
      error &&
      typeof error === 'object' &&
      'statusCode' in error
    ) {
      throw error;
    }

    console.error('Profile fetch error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to fetch profile',
    });
  }
});
typescript// server/utils/auth.ts(トークン検証ユーティリティ)
export async function validateToken(token: string) {
  try {
    // JWT の検証などを実装
    // ここでは簡略化した例を示す
    const response = await fetch(
      'https://auth.example.com/validate',
      {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );

    if (!response.ok) {
      return null;
    }

    return await response.json();
  } catch (error) {
    console.error('Token validation error:', error);
    return null;
  }
}

このコードのポイントは以下の通りです。

  • Access-Control-Allow-Origin を特定のドメインに限定(* ではなく)
  • Access-Control-Allow-HeadersAuthorization を明示的に含める
  • Access-Control-Allow-Credentials: true で認証情報の送信を許可
  • OPTIONS リクエストには空文字列を返して Preflight を成功させる
  • Authorization ヘッダーの形式を検証してから処理する

ケーススタディ 3:runtimeConfig の undefined エラー

環境変数の設定ミスによる典型的な 500 エラーです。

問題のコード

typescript// server/api/external-data.ts
export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();

  // config.externalApiKey が undefined の可能性を考慮していない
  const response = await fetch(
    'https://api.external.com/data',
    {
      headers: {
        'X-API-Key': config.externalApiKey, // ← undefined だとエラー
      },
    }
  );

  return response.json();
});

エラーログ

bash# ターミナルのエラー
[nuxt] [request error] [unhandled] [500] Cannot read property 'externalApiKey' of undefined
  at /server/api/external-data.ts:6:23

# または
[nuxt] [request error] [unhandled] [500] Invalid header value

Invalid header value というエラーは、ヘッダーに undefined が渡されたことを示しています。

原因分析

問題の原因は以下の 3 つのいずれかです。

#原因確認方法
1nuxt.config.ts で runtimeConfig を定義していないnuxt.config.ts を確認
2環境変数ファイル(.env)に値が設定されていない.env ファイルを確認
3環境変数名のスペルミスnuxt.config.ts と .env の両方を確認

修正方法:完全な設定手順

まず、nuxt.config.ts で runtimeConfig を定義します。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // サーバーサイド専用(秘密鍵など)
    externalApiKey: process.env.EXTERNAL_API_KEY || '',
    databaseUrl: process.env.DATABASE_URL || '',

    public: {
      // クライアントサイドでも使用可能
      appName: process.env.APP_NAME || 'My App',
    },
  },
});

デフォルト値として空文字列('')を設定しておくことで、環境変数が未設定の場合でも undefined にならないようにします。

次に、.env ファイルに実際の値を設定します。

bash# .env
EXTERNAL_API_KEY=your-api-key-here
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
APP_NAME=Production App

最後に、API ハンドラーで値の存在を確認してから使用します。

typescript// server/api/external-data.ts(修正版)
export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();

  // 環境変数が設定されているか確認
  if (!config.externalApiKey) {
    console.error(
      'EXTERNAL_API_KEY is not configured in runtimeConfig'
    );
    throw createError({
      statusCode: 500,
      statusMessage:
        'Server configuration error: Missing API key',
    });
  }

  try {
    const response = await fetch(
      'https://api.external.com/data',
      {
        headers: {
          'X-API-Key': config.externalApiKey,
        },
      }
    );

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

    return await response.json();
  } catch (error) {
    console.error('External API error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to fetch external data',
    });
  }
});

開発環境と本番環境の設定の違い

本番環境にデプロイする際は、ホスティングサービスの管理画面から環境変数を設定します。

bash# Vercel の場合(コマンドライン)
vercel env add EXTERNAL_API_KEY

# Netlify の場合(環境変数設定画面)
Site settings > Environment variables > Add variable
EXTERNAL_API_KEY = your-production-key

次の図は、runtimeConfig が読み込まれる流れを示しています。

mermaidflowchart LR
  env_file[".envファイル"] -->|読み込み| process_env["process.env"]
  hosting["ホスティング<br/>環境変数"] -->|デプロイ時| process_env

  process_env --> nuxt_config["nuxt.config.ts<br/>runtimeConfig"]

  nuxt_config -->|サーバーサイド| server_api["server/api/*.ts<br/>useRuntimeConfig()"]
  nuxt_config -->|public のみ| client["クライアント<br/>コンポーネント"]

  server_api --> validation{値が存在する?}
  validation -->|No| error_500["500エラー:<br/>設定ミス"]
  validation -->|Yes| api_call["外部APIへ<br/>アクセス"]

図で理解できる要点

  • 開発環境では .env ファイル、本番環境ではホスティングサービスの環境変数が process.env に読み込まれる
  • nuxt.config.tsruntimeConfig が環境変数を受け取る
  • サーバーサイドではすべての値に、クライアントサイドでは public 配下の値のみにアクセス可能

デバッグツールとテクニック

500 エラーの原因を特定する際に役立つツールとテクニックをご紹介します。

ブラウザ開発者ツールの活用

ブラウザの開発者ツール(F12 キー)を開き、以下をチェックしましょう。

  • Console タブ: エラーメッセージと CORS 警告を確認
  • Network タブ: リクエストのステータスコード、ヘッダー、レスポンスを確認
  • Headers セクション: CORS ヘッダーの有無を確認
#確認項目見るべきポイント
1Status Code500 だけでなく 404、401 も確認
2Request HeadersAuthorization、Content-Type の有無
3Response HeadersAccess-Control-Allow-* ヘッダーの有無
4Timing タブリクエストがどの段階で失敗したか

サーバーログの詳細化

開発中は、ログ出力を詳細にすることで問題を早期発見できます。

typescript// server/api/debug-endpoint.ts
export default defineEventHandler(async (event) => {
  // リクエスト情報をログ出力
  console.log('=== Request Debug Info ===');
  console.log('Method:', event.method);
  console.log('URL:', event.path);
  console.log('Headers:', getHeaders(event));
  console.log('Query:', getQuery(event));

  try {
    const config = useRuntimeConfig();

    // 環境変数の状態をログ出力(秘密情報はマスクする)
    console.log('Config check:');
    console.log(
      '- externalApiKey:',
      config.externalApiKey ? '[SET]' : '[NOT SET]'
    );

    // ここから実際の処理
    const result = await someOperation();

    console.log('Operation successful:', result);
    return result;
  } catch (error) {
    // エラーの詳細をログ出力
    console.error('=== Error Details ===');
    console.error(
      'Type:',
      error instanceof Error
        ? error.constructor.name
        : typeof error
    );
    console.error(
      'Message:',
      error instanceof Error ? error.message : error
    );
    console.error(
      'Stack:',
      error instanceof Error
        ? error.stack
        : 'No stack trace'
    );

    throw createError({
      statusCode: 500,
      statusMessage: 'Operation failed',
    });
  }
});

このような詳細なログ出力により、問題の発生箇所を素早く特定できます。

まとめ

Nuxt 3 の server​/​api で発生する 500 エラーは、主に 例外処理の欠如CORS 設定の不備runtimeConfig の誤設定 の 3 つに起因します。

それぞれの問題に対する基本的な対処法を表でまとめます。

#原因症状対処法
1例外処理の欠如未処理の Promise Rejectiontry-catch で囲み、createError で適切なエラーレスポンスを返す
2Promise の await 忘れ[object Promise] エラーすべての非同期処理に await を付ける
3CORS 設定不足OPTIONS リクエスト失敗setResponseHeaders で Access-Control-* ヘッダーを設定
4runtimeConfig 未定義undefined 参照エラーnuxt.config.ts で定義し、.env ファイルに値を設定
5環境変数の検証不足本番環境でのみエラーAPI ハンドラーで値の存在を確認してからアクセス

500 エラーに遭遇したら、まず以下の手順で原因を切り分けましょう。

  1. ターミナルのサーバーログを確認し、エラーメッセージとスタックトレースを読む
  2. ブラウザの開発者ツールで Network タブを開き、CORS エラーがないか確認する
  3. try-catch で例外処理を追加し、すべての非同期処理に await が付いているか確認する
  4. runtimeConfig の値が正しく設定されているか、値の存在確認を行っているか確認する

これらの基本を押さえることで、500 エラーの多くは解決できるはずです。それでも解決しない場合は、詳細なログ出力を追加して、問題の発生箇所を特定していきましょう。

エラーと向き合うことは、時に辛い作業かもしれません。しかし、一つひとつのエラーを丁寧に解決していくことで、確実にスキルアップできます。この記事が、皆さんの問題解決の一助となれば幸いです。

関連リンク