💡この記事で分かること
- Next.js 14 App Routerの動的ルーティングで
paramsが取得できない問題の解決法 - Prismaのスキーマ変更が反映されない「無限ループ」からの脱出方法
- 開発初心者がAI開発ツール(Cursor)を使いこなすために私自身が考えたこと
対象読者: Next.js 14 × Prismaで開発中の方、CursorなどのAI開発ツールを使っている方、「コードは合ってるはずなのにエラーが出る...」と悩んでいる方
はじめに
株式会社Dirbatoの社内技術支援横断組織「Backbeat」で、最新技術の調査や社内ツールの保守開発を担当している山田です!
私はCursorで共同旅行計画アプリ「PonChord」を、技術キャッチアップを兼ねて個人開発しています。
今回は、そのアプリの開発中に特に頭を抱えた バックエンド周りの「2大ハマりポイント」 について、初心者ならではのAI開発での苦悩とその解決方法を書きたいと思います。
ハマったポイントは以下の2つ:
- Next.js 14 App Routerで
paramsが取得できずにエラーになる問題 - Prismaのスキーマを変更しても、なぜか反映されずにエラーになる問題
これらはエラーの原因をなかなか見つけることができず、上手くCursorに改善点を伝えられずに、開発を進める中でかなり悩んだところでした。
どちらも「コードが間違っている」というよりは、「ツールのルール(作法)」の問題なので、初心者には原因が特定しにくく、非常にハマりやすいポイントだと思います。同じ構成で開発している方の助けになれば幸いです!
▼「PonChord」については前回記事で紹介しています
前提知識:Next.js 14とPrismaって何?
まず、今回のエラーに関わる技術について、私が理解したことを書きます。(勉強中なので、間違いがあればご指摘ください...!)
① Next.js 14 (App Router)
Reactという人気のライブラリをベースにした、Webサイトを作るためのフレームワークです。
「バージョン14」の「App Router」というのは、ファイルの置き方に関する新しいルールのことです。
何ができる? フォルダを作ると、それがそのままURLになります。
- 例:
app/about/page.tsxというファイルを作るとwww.example.com/aboutというページができる。
動的ルーティング(Dynamic Routing) これが今回のキーポイントです!
- フォルダ名を
[id]のように[]で囲むと、そこは 「何が入ってもいい場所」 になります。 - 例:
app/api/resource/[id]/route.tsと作ると、/api/resource/123や/api/resource/abcなど、どんなIDでも受け取れるようになります。この123やabcという値をプログラムの中で受け取る変数がparamsです。
② Prisma
プログラム(Next.js)とデータベース(PostgreSQLなど)の仲介役(ORM) です。
役割: 本来、データベースを操作するには「SQL」という難しい言語が必要ですが、Prismaを使うとJavaScript/TypeScriptの書き方でデータベースを操作できます。何それ美味しいの?状態から使い始めましたが、めちゃくちゃ便利でした。
schema.prisma: 「設計図」ファイルです。「ユーザーには名前とメールアドレスがある」といった定義をここに書きます。
型定義(Type Safety): Prismaの最大の特徴です。設計図を元に、「ユーザーデータには必ず名前が入っているはずだ」というルール(型)を自動で作ってくれます。これにより、コードを書いている最中に「あ、名前の綴りが間違ってるよ」と教えてくれるようになります。これがあるおかげで、タイポによるバグが激減しました!
1. App Routerの洗礼:params は Promise である
Next.js 14のApp Routerで動的ルーティングを使用したAPI(例: /api/resource/[id]/route.ts)で、IDを取得しようとすると不可解な挙動に遭遇しました。
❌ エラーの状況
params.id が undefined になったり、404/500エラーが発生したりしました。
発生したエラーログ(例):
GET app/api/resource/[id]/route.ts 500 (Internal Server Error)
TypeError: Cannot read properties of undefined (reading 'id')
修正前のコード:
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const journeyId = params.id // ⚠️ ここが undefined になることがある
// ...処理
}
「え、なんで?ちゃんと [id] って書いてるのに!」と頭を抱えていました。Cursorに「なぜこのエラーが出るのか」と聞いても、コードはあってるよ、と的確な答えが返ってこず、公式ドキュメントを見ても知識不足で次々と疑問が湧き、エラーログを行ったり来たり……。完全に迷子でした。
そんな中、ChatGPTに「わかりやすく教えて、、!!」と泣きついたところ、(私的には)分かりやすい例えをしてくれました。この例えのお陰でこのエラーの根本的な原因を理解することが出来ました!
🍽️ わかりやすい例え:レストランの注文
昔(Next.js 13以前など): 店員さんに「注文いいですか?」と言うと、その場ですぐに注文を聞いてくれました。
(コード:
params.idと書けばすぐにIDが取れた)今(Next.js 14の一部): 店員さんに声をかけると、「後で注文を聞きに行くから待っててね」という整理券(Promise)を渡されるようになりました。
それなのに、私は整理券そのものを料理だと思って食べようとして、「これ料理じゃないよ!(エラー)」 となっていたのです。
なるほど!!! この例えで、ようやく腑に落ちました。
わたし、Next.js 14 つかってるぢゃん!!!!
✅ 解決策:動的ルーティングの引数の型はPromise型で、値はPromise.resolve で解決する
Next.js 14以降は、動的ルートの params は Promise として扱われると判明。
どうやらCursorくんは、引き数の型をそもそもPromise型ではない書き方で書いてしまい、コード上エラーが無いように見え、詰まってしまっていた模様でした。
そのため、普通に通されるのではなく、「まず整理券(Promise)をもらう」必要がある。そして「整理券(Promise)」をもらったら、「呼び出されるまで待つ(await)」という処理が必要でした。
修正後のコード:
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> | { id: string } }
) {
try {
const resolvedParams = await Promise.resolve(params)
const journeyId = resolvedParams.id
if (!journeyId) {
return NextResponse.json({ error: 'IDが必要です' }, { status: 400 })
}
// ...以降、journeyId を使用してDB検索などを行う
} catch (error) {
// エラーハンドリング
}
}
この対応は、[id] だけでなく、[itemId] や [token] など、すべての動的APIルートに適用する必要がありました。
原因を理解してからは、Cursorの.cursor/rules配下のファイルで「すべての動的ルートで params を Promise として扱い、await Promise.resolve(params) で解決するパターンを適用して」と具体的に指示しておくことで、同じような動的ルーティングを使用したAPIの実装を爆速で完了できました!やったー!
ポイント: Promise.resolve() は、引数がPromiseならそのまま解決し、Promiseでなければそのまま返してくれる便利な関数です。防御的コーディングとして非常に有効!
2. Prismaスキーマ変更の「反映されない」無限ループ
❌ エラーの状況:スキーマを変えたのに...
データベースにPostgreSQL、ORMにPrismaを使用していますが、「行きたいところリスト」にタグ機能を実装するため、tag フィールドを追加した時です。
schema.prisma を書き換えて保存したのに、いざAPIを叩くと以下のようなエラーが出ました。
エラーログ:
// API実行時
Unknown field `tag` for include statement on model WishlistItem.
または
// TypeScriptのコンパイルエラー
Property 'tag' does not exist on type 'WishlistItem'.
「え、ちゃんとスキーマに追加したのに!?」と何度もスキーマファイルを見直しましたが、問題はそこではありませんでした……。Cursorに「スキーマ変更したのにエラーが出るたすけて」と泣きつきました。。
このエラーも私の知識不足によりCursorに上手く修正してほしい点を伝えられず、修正に時間がかかってしまいました。結局このエラーに関してはCursorに壁打ちしながら修正することが出来ました。(Cursorとの対話を繰り返すうちに、ようやく原因に辿り着けた感じです)
💭 ここで痛感したこと
Cursorは非常に優秀なツールですが、やはりしっかりとエラーの原因を理解したうえで改善するためのプロンプトを書かないと、時間がかかってしまう場合があると私は思います。
「スキーマを変更したのにエラーが出る」という漠然とした相談では、Cursorも「コードのどこが間違っているのか」を探す方向に進んでしまい、本質的な 「手順の抜け漏れ」 には気づきにくいんですね。
この優秀なCursorを使いこなすためには、もっともっと私自身が成長する必要があるなと強く思いました。 AIは強力なパートナーですが、「何が問題なのか」を理解し、「どう解決したいのか」を言語化できるのは、結局のところ人間である私たち自身なんですよね。
✅ 解決策:変更時の「鉄の掟」ルーティン
Prismaの場合、schema.prisma ファイルを書き換えるだけでは変更は反映されません。型定義ファイル(Prisma Client)の再生成が必須です。
Prismaでは、変更を適用するために以下の手順が必須です。
手順1. スキーマの変更
model WishlistItem {
id String @id @default(cuid())
// ...
tag String? // 追加!
}
手順2. DBへの反映
npx prisma db push --accept-data-loss
手順3. クライアントの再生成 ← 今回ここが抜けていた!
ここを忘れがちですが、これをやらないとTypeScriptが新しいフィールド tag を認識しません。
npx prisma generate
特に3番目の generate は、「TypeScriptの型定義」を作り直す作業です。これをやらないと、エディタやNext.jsは新しい項目の存在を知ることができません。ここが盲点でした...!
手順4. 開発サーバーの再起動
# 一度 Ctrl+C で止めてから
npm run dev
この手順を理解してからは、Cursorに「Prismaスキーマを変更したから、db push と generate を実行して」と具体的に指示することで、ターミナルコマンドまで提案してもらえるようになりました! やはり、「何をすべきか」を理解していることが、Cursorへの的確な指示につながるんですね。
Tips: この4ステップを「push → generate → restart」のリズムで覚えると、もう迷いません!Cursorの.cursor/rules配下のファイルに「Prisma変更時の手順」として書いておくと、次回からこの実装手順を反映することが出来て便利です。
まとめ:ハマりポイントを乗り越えて
今回の開発で得た教訓は以下の3点です。
-
Next.js App Routerの
paramsは非同期前提で扱う- Next.js 14の動的URLを使うときは、「データは後から来る(非同期)」と思って待ち受けるコードを書くこと。
-
Prisma変更時は push → generate → restart
このリズムを崩すと、謎のエラーで時間を溶かすことになります。(実体験)- データベースの定義を変えたら、「DBの更新」と「プログラム側の更新(generate)」の両方を必ずセットで行うこと。
-
Cursorを使いこなすには「理解」が必要
- エラーの原因を理解し、的確なプロンプトを書けるかどうかが開発速度を大きく左右する
どちらも「コードが間違っている」というよりは、「ツールのルール(作法)」の問題なので、初心者には原因が特定しにくく、非常にハマりやすいポイントです。
だからこそ、こうした「ハマりポイント」を言語化して共有することが大切だと感じています。
同じところで躓いている方の助けになれば嬉しいです! もし「私もここでハマった!」という経験があれば、ぜひコメントで教えてください。一緒に乗り越えましょう!