はじめに
Next.js 15のアプリケーションをVercelにデプロイする際、ローカルでは動作するのに本番環境で500エラーが発生して苦労した経験はありませんか?
今回、日本語学習者向けの速読練習アプリ「SuiReN」の開発中に遭遇した5つの落とし穴と、その解決方法を共有します。同じような問題で悩んでいる方の参考になれば幸いです。
プロジェクトの概要
- 技術スタック: Next.js 15 (App Router)、Prisma ORM、PostgreSQL、Vercel
- 主な機能: コンテンツ管理、レベル別フィルタリング、Excel一括アップロード
- 課題: 静的なレベル管理から動的なデータベース管理への移行
1. Prismaのマイグレーションで既存データが消える問題
問題
Vercelのビルドコマンドで prisma db push --accept-data-loss
を使用していたため、デプロイのたびに既存データが失われる可能性がありました。
// package.json (間違った例)
{
"scripts": {
"build": "prisma db push --accept-data-loss && next build"
}
}
解決法
prisma generate
のみを使用し、スキーマの変更は手動で管理するように変更しました。
// package.json (正しい例)
{
"scripts": {
"build": "prisma generate && next build"
}
}
学び
- 本番環境では
--accept-data-loss
は絶対に使わない - データベースのマイグレーションは CI/CD パイプラインから分離する
- 重要なデータがある場合は必ずバックアップを取る
2. 外部キー制約違反によるテーブル作成エラー
問題
既存のContentテーブルに無効なlevelCode
が存在し、Levelテーブルの作成時に外部キー制約違反が発生しました。
Invalid `prisma.level.create()` invocation:
Foreign key constraint failed on the field: `Content_levelCode_fkey (index)`
解決法
マイグレーションスクリプトを作成し、データのクリーンアップを自動化しました。
// scripts/migrate-levels.js
async function migrateExistingData() {
// 無効なlevelCodeを持つコンテンツを削除
await prisma.content.deleteMany({
where: {
levelCode: {
notIn: ['beginner', 'intermediate', 'advanced']
}
}
});
// Levelテーブルを作成
const levels = [
{ id: 'beginner', displayName: '中級前半', orderIndex: 0 },
{ id: 'intermediate', displayName: '中級レベル', orderIndex: 1 },
{ id: 'advanced', displayName: '上級レベル', orderIndex: 2 }
];
for (const level of levels) {
await prisma.level.upsert({
where: { id: level.id },
update: {},
create: level
});
}
}
学び
- 外部キー制約を追加する前に、既存データの整合性を確認する
- マイグレーションは段階的に実行し、各段階でデータを検証する
- データクリーンアップ用のスクリプトを用意しておく
3. Vercel環境でのファイルアップロードエラー
問題
ローカルでは動作するExcelアップロード機能が、Vercel環境で400/500エラーを返していました。
// 問題のあったコード
export async function POST(request) {
const formData = await request.formData();
const file = formData.get('file'); // Vercelで undefined になることがある
}
解決法
エラーハンドリングを強化し、FormDataの解析を確実に行うようにしました。
export async function POST(request) {
try {
const contentType = request.headers.get('content-type') || '';
if (!contentType.includes('multipart/form-data')) {
return NextResponse.json(
{ error: 'Invalid content type' },
{ status: 400 }
);
}
const formData = await request.formData();
const file = formData.get('file');
if (!file || typeof file.arrayBuffer !== 'function') {
return NextResponse.json(
{ error: 'File not found or invalid' },
{ status: 400 }
);
}
const buffer = await file.arrayBuffer();
// 処理を続行...
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}
学び
- Vercel環境とローカル環境でのFormData処理の違いを考慮する
- ファイルオブジェクトの検証を徹底する
- エラーログを詳細に出力して問題の特定を容易にする
4. Prismaクライアントのエクスポートエラー
問題
Vercelビルド時に「The requested module 'prisma/prisma.js' does not provide an export named 'default'」というエラーが発生しました。
解決法
Prismaクライアントのエクスポート方法を修正しました。
// lib/prisma.js
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis;
const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export default prisma; // 明示的にdefaultエクスポート
学び
- ES Modulesのエクスポート/インポート構文を正確に使用する
- Vercelのビルド環境ではNode.jsのバージョンや設定が異なる可能性がある
- グローバル変数を使用したシングルトンパターンは慎重に実装する
5. 動的パラメータの非同期処理エラー
問題
Next.js 15では動的ルートのパラメータが非同期になり、適切に処理しないとエラーが発生しました。
// 間違った例
export async function GET(request, { params }) {
const { id } = params; // Next.js 15ではエラー
}
解決法
パラメータを非同期で取得するように修正しました。
// 正しい例
export async function GET(request, { params }) {
const { id } = await params; // awaitを追加
}
学び
- Next.js 15の破壊的変更を確認する
- TypeScriptを使用して型エラーを早期に発見する
- 公式ドキュメントの移行ガイドを必ず確認する
まとめ
これらの問題を通じて学んだ重要なポイント:
- 本番環境とローカル環境の違いを常に意識する
- エラーハンドリングとログ出力を充実させる
- データベースの変更は慎重に、段階的に行う
- フレームワークのアップデート時は破壊的変更を確認する
- デプロイ前にステージング環境でテストする
特にVercelとPrismaの組み合わせでは、ビルドプロセスとデータベース管理を適切に分離することが重要です。
参考リンク
これらの経験が、同じような問題に直面している開発者の皆さんの助けになれば幸いです。質問やご意見があれば、コメントでお知らせください!