Next.jsは画面とAPIをどう分担するか
同じアプリの中でも、ブラウザで動く表示、サーバーで読む処理、金融更新の入口には違う責任がある。
本文の説明・図・掲載コードだけで読み進められます。出典リンクは、権限のある人向け(非公開GitHub)です。GitHubで閲覧権限がある方だけ開けます。
この記事でわかること
- サーバーで表示用データを読む経路と、ボタンからAPIへ送る経路がある
- ブラウザの操作制限だけでは金融データを守れない
- API、サービス、DBを分けると変更と調査の境界が分かる
この記事の目次
Next.jsという名前より、二つの実行場所を知る
Next.jsは、Webの画面と、その画面を支えるサーバー処理を組み立てるための枠組みです。MiniBankではReactの部品を使って表示を組み合わせ、TypeScriptでデータの形を記述しています。ただし初心者が最初に覚えるべきなのは道具の一覧ではありません。「この処理は利用者のブラウザで動くのか、サーバー側で動くのか」という違いです。
ブラウザはボタンを押したり、入力途中の値を持ったり、エラーを表示したりします。サーバー側は認証を確かめ、必要なデータを読み、処理の窓口を用意します。利用者の端末に渡った表示やコードは、利用者が操作できるものです。だから「画面に入力欄を置かなかった」「ボタンを隠した」だけを、残高や権限の最終防御にはできません。
口座ページを見ると、サーバー側のページが顧客・口座・利用者情報を集め、AccountExperienceという部品へ渡しています。そこで残高、口座状態、本人確認状態を別々に表示します。データを集める仕事と、意味が伝わるように並べる仕事を分けると、見た目の変更が金融計算へ入り込みにくくなります。
ページを開く経路と、操作する経路は同じではない
Webシステムを「必ず画面→API→サービス→DB」とだけ描くと、この実装の半分を見落とします。口座ページを開くときは、サーバー側のページがサービスを直接呼んで表示用データを集めます。自分自身のHTTP APIへ一度回り道しているわけではありません。一方、利用者が開設ボタンを押すと、ブラウザから口座APIへの通信が発生します。
この違いは、画面に数字が出たときにも効いてきます。ページを開いて残高を読んだことは、新しく仕訳を書いたことではありません。反対に、ボタンの色が変わったことは、DBで変更が確定した証拠ではありません。表示用の読取りと、保存される業務操作の間に線を引くと、どの場面で失敗を調べるべきかが分かります。
画面が持つ状態は、銀行の状態の代わりにならない
開設ボタンのファイル先頭にある「use client」は、この部品がブラウザ側の操作や状態を使うことを示します。部品はerrorとsubmittingを持ちます。errorはその場で見せる失敗メッセージ、submittingは依頼中かどうかです。これらは、bank_accountsに保存される口座のactiveやfrozenとは別の種類の状態です。
架空の山田さんが開設ボタンを押すと、submittingがtrueになり、表示は「口座を開設しています…」へ変わります。しかしこの時点ではまだ、銀行口座が作られたと確定していません。APIの成功応答を受けてページを読み直し、保存された口座を表示するまでを区別します。「操作中」という便利な表示を、「金融処理が完了した」という記録へ勝手に格上げしないのが大切です。
ボタンを無効にするのも、誤操作を減らすための配慮です。別タブや直接のAPI呼出しまで止める仕組みではないため、重複禁止はDBの制約でも守ります。また実装を読むときは、HTTPの失敗応答と、通信がまったく戻らない状態を分けて見ます。このボタンの存在だけから、あらゆる通信中断の復旧が完成していると判断することはできません。
APIは、外から来る依頼の形を狭くする
APIは、アプリの外から来る入力を受け取る境界です。口座開設では「普通預金か」「通貨は円か」をZodという入力検査の仕組みで確かめます。次は実際の定義です。optionalは省略可能、literalはこの値だけ、strictは余分な項目を認めない、という意味です。
export const createAccountSchema = z
.object({
account_type: z.literal("ordinary").optional(),
currency: z.literal("JPY").optional(),
})
.strict();ここで文字列なら何でもよいとすると、未対応の外貨や、予想外の状態指定が下流へ流れます。許す値を狭くすると、DBに到着する前に意図の違う依頼を拒否できます。ただし、形が正しい依頼でも、依頼者が正しい持ち主とは限りません。入力検査と認証・認可は別の仕事です。
口座APIではrequireAuthContextが認証情報を検証し、本人として動くDBクライアントを作ります。DBクライアントは、確認済みのアクセストークンを付けた接続であり、一般の依頼を管理者へ格上げする仕組みではありません。その先のRLSや業務関数にも守りを持たせ、画面やAPIだけを通らない経路を考慮しています。
サービスに金融処理を並べすぎない理由
サービスは、APIやページがやりたいことを、DBの読取りや業務関数へつなぐ場所です。AccountServiceは、公開する口座データの形を揃え、DB側のエラーをアプリで扱う形へ変えます。TransactionServiceは、明細を次のページへ読み進められる形へ整えます。つまり「DBの行をどう利用者へ渡すか」に責任を持ちます。
一方、お金を動かすときに「送り手を更新してから、受け手を更新する」という複数回の独立した通信を並べると、途中失敗の境界が増えます。MiniBankの重要な金融更新は、DBの業務関数と元帳の計上機構へまとめる方針です。画面やAPIに同じ残高計算を複製するほど、直すべき場所が増え、片方だけ古い計算を使う危険が生まれます。
ただし「サービスはすべて同じ経路」とも言えません。固定ソースのTransferServiceには、既存のSupabase RPC経路とJavaサービス経路の選択があります。設定を省略した場合のコード上の既定はlegacyです。これは実行中の環境がどちらを使うかを調査した結果ではありません。構成図を読むときは、実装されている候補、設定上の既定、実際の稼働状態を分けてください。
根拠:口座の公開形式とDBへの委譲(非公開GitHub・要権限)
薄い入口は、失敗を説明しやすくする
役割が分かれていると、不具合の調べ方も変わります。金額が崩れて見えるなら表示の整形を、入力が受付で落ちるなら検査の定義を、認証が通らないなら認証の入口を、仕訳が拒否されるならDBの業務条件を見ます。どの問題もとりあえず画面側で握りつぶすと、本当の拒否理由が見えなくなります。
APIの共通応答にはdataまたはerrorと、meta.request_idがあります。request_idは一回の通信を追う番号で、同じ業務依頼の二重計上を防ぐ冪等キーとは用途が違います。通信をやり直すと受付番号は変わり得ますが、同じ業務を一回に保つためのキーは同じ意図へ結び付けます。名前が似た識別子を「どれもID」として扱わないことが、調査の精度を上げます。
この章はMiniBankのコードの読み方であり、Next.jsやSupabaseの全機能の解説ではありません。APIがあることだけで安全、サーバー側で動くことだけで秘密、型が付いていることだけで正しい、と考えるのは早計です。どの境界で何を検証し、どこに書込みの権限があり、最後の保存時にどの条件が守られるかを、具体的な経路で確かめるのが設計を読む姿勢です。
出典と、確認できた範囲
以下のリンクは非公開GitHubへの出典で、閲覧権限が必要です。本文はリンク先を開かなくても読める構成です。固定コード・設計資料を参照していますが、今回、この読み物のために銀行の取引やDB操作、本番試験は実行していません。実装や資料にある検査と、こちらで実施した検証を混同しないようにしています。
- 口座ページ ↗(非公開GitHub・要権限)
サーバー側の読取りと表示部品への受渡し - 操作部品 ↗(非公開GitHub・要権限)
ブラウザの一時状態とAPI通信 - 入力検査 ↗(非公開GitHub・要権限)
型と許可値、未知の項目の拒否 - 本人としてのDB接続 ↗(非公開GitHub・要権限)
認証情報、RLS、受付番号の役割 - バックエンド選択 ↗(非公開GitHub・要権限)
既定はlegacy。稼働中の値とは区別 - 共通エラー ↗(非公開GitHub・要権限)
安全な応答と調査番号
MiniBankは学習・検証用の銀行アプリです。架空の金額や人物は、仕組みを説明するための例です。
つながる読み物
ここまで読んだ記録を、ひとつ。
自分のペースで。あとから取り消すこともできます。