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 がなく未処理例外が発生 | ★★★ |
| 2 | CORS 設定ミス | ブラウザが OPTIONS リクエストで失敗 | ★★ |
| 3 | runtimeConfig の誤設定 | 環境変数が 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) のチェックが非常に重要です。このチェックがないと、apiSecret が undefined のまま 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();
}
このコードは、fetchUserById が async 関数なのに、呼び出し側で 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-HeadersにAuthorizationを明示的に含める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 つのいずれかです。
| # | 原因 | 確認方法 |
|---|---|---|
| 1 | nuxt.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.tsのruntimeConfigが環境変数を受け取る- サーバーサイドではすべての値に、クライアントサイドでは
public配下の値のみにアクセス可能
デバッグツールとテクニック
500 エラーの原因を特定する際に役立つツールとテクニックをご紹介します。
ブラウザ開発者ツールの活用
ブラウザの開発者ツール(F12 キー)を開き、以下をチェックしましょう。
- Console タブ: エラーメッセージと CORS 警告を確認
- Network タブ: リクエストのステータスコード、ヘッダー、レスポンスを確認
- Headers セクション: CORS ヘッダーの有無を確認
| # | 確認項目 | 見るべきポイント |
|---|---|---|
| 1 | Status Code | 500 だけでなく 404、401 も確認 |
| 2 | Request Headers | Authorization、Content-Type の有無 |
| 3 | Response Headers | Access-Control-Allow-* ヘッダーの有無 |
| 4 | Timing タブ | リクエストがどの段階で失敗したか |
サーバーログの詳細化
開発中は、ログ出力を詳細にすることで問題を早期発見できます。
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 Rejection | try-catch で囲み、createError で適切なエラーレスポンスを返す |
| 2 | Promise の await 忘れ | [object Promise] エラー | すべての非同期処理に await を付ける |
| 3 | CORS 設定不足 | OPTIONS リクエスト失敗 | setResponseHeaders で Access-Control-* ヘッダーを設定 |
| 4 | runtimeConfig 未定義 | undefined 参照エラー | nuxt.config.ts で定義し、.env ファイルに値を設定 |
| 5 | 環境変数の検証不足 | 本番環境でのみエラー | API ハンドラーで値の存在を確認してからアクセス |
500 エラーに遭遇したら、まず以下の手順で原因を切り分けましょう。
- ターミナルのサーバーログを確認し、エラーメッセージとスタックトレースを読む
- ブラウザの開発者ツールで Network タブを開き、CORS エラーがないか確認する
- try-catch で例外処理を追加し、すべての非同期処理に await が付いているか確認する
- runtimeConfig の値が正しく設定されているか、値の存在確認を行っているか確認する
これらの基本を押さえることで、500 エラーの多くは解決できるはずです。それでも解決しない場合は、詳細なログ出力を追加して、問題の発生箇所を特定していきましょう。
エラーと向き合うことは、時に辛い作業かもしれません。しかし、一つひとつのエラーを丁寧に解決していくことで、確実にスキルアップできます。この記事が、皆さんの問題解決の一助となれば幸いです。
関連リンク
articleNuxt 500 の犯人はどこ?server/api で起きる例外・CORS・runtimeConfig の切り分け
articleNuxt Nitro のしくみを図解で理解:サーバーレス実行とアダプタの舞台裏
articleNuxt 本番運用チェックリスト:セキュリティヘッダー・CSP・Cookie 設定を総点検
articleNuxt クリーンアーキテクチャ実践:UI・ドメイン・インフラを composable で分離
articleNuxt nuxi コマンド速見表:プロジェクト作成からモジュール公開まで
articleNuxt を macOS + yarn で最短構築:ESLint/Prettier/TS 設定まで一気通貫
articleWebLLM とは?ブラウザだけで動くローカル推論の全体像【2025 年版】
articleMistral とは? 軽量・高速・高品質を両立する次世代 LLM の全体像
articleOllama コマンドチートシート:`run`/`pull`/`list`/`ps`/`stop` の虎の巻
articletRPC とは?型安全なフルスタック通信を実現する仕組みとメリット【2025 年版】
articleJest の “Cannot use import statement outside a module” を根治する手順
articleObsidian プラグイン相性問題の切り分け:セーフモード/最小再現/ログの活用
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来