TypeScript - TECH PLAY - TECH PLAY

TECH PLAY

TypeScript

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

はじめに こんにちは、エブリーでECの開発をしている山舗です。 この度、弊社で運営している EC サイトにて、既存の SaaS のカートに代わる自社実装を新規に開発し、全面リプレイスを行いました。扱うデータには、会員の個人情報や、定期購入の課金情報などがあります。期間は 3 か月弱、コードは約 61 万行、コミットは約 7,500 件に及ぶ開発となりました。この規模の開発を短期間で行うには、Claude Code を中心に、半自律的に開発を進めさせる必要がありました。 一方で、世の中では個人情報の漏えいのニュースが数多く流れています。2025 年に上場企業とその子会社が公表した分だけで、3,063 万人分に及びます( 東京商工リサーチの調査 )。 この記事では、こうした状況の中で、Claude に書かせる前提で開発しながら個人情報を守るために、どんな技術を選び、どう使ったかをご紹介します。 1. Claude に書かせる前提の技術選定 新規開発にあたって、まず技術選定から始めました。開発の中心を Claude に任せるので、Claude が書いたコードに読み違いや書き忘れがあっても、実行する前に機械で見つけられることを基準にしました。選んだ構成は次のとおりです。 役割 選んだ技術 言語 TypeScript(フロント / バックエンド) API Hono DB と ORM PostgreSQL、Drizzle フロント(storefront) Next.js、Tailwind フロント(admin) Vite、Tailwind まず、言語は TypeScript に統一しました。バックエンドを Go、フロントを TypeScript のように分けると、2 つの言語の間で型は自動ではつながりません。API の形を OpenAPI などの中間の定義に書き、そこからフロントの型とクライアントを生成する工程が要ります。例えば、「定期購入の配送サイクルは、決まった日数のどれか」のような値の決まりも、バックエンドの検証と画面の検証の 2 か所に書くことになります。片方だけ直すと、もう片方は古いまま動き、ずれに気付くのは実行したときです。 AI は、API だけを直して画面を直し忘れるといった、片方だけの変更をよく書きます。それが実行時まで見つからない作りは避けたかったので、API から画面までを 1 つの言語にして、型をつなげることにしました。フロントとバックエンドでドメインのロジックを共有でき、lint とコーディングの決まりを 1 言語に絞れるのも理由です。 その型を API から画面へ届けるために、API のフレームワークには Hono を選びました。Hono RPC の型推論を使うと、API の定義からフロントの呼び出しまで、型がそのままつながります。Express などより軽く、Web 以外から使う API も今は無いので、これで足りました。NestJS は DI の構造が複雑で、問題に起動時にしか気付けません。構造を静的に追えるほうが AI に向くと考えました。 DB の側も同じ考え方で、ORM には Drizzle を選びました。スキーマを TypeScript で定義し、型をそこから導くので、独自の言語やコード生成の工程がありません。Prisma は独自の言語でスキーマを書き、クライアントの生成が要ります。行ロックと入れ子のトランザクションを API で書けることも理由で、これは後で例を挙げます。管理画面の見た目には Tailwindを使っています。Tailwind は任意の値を text-[#e91e63] のように角括弧で書くので、AI が決まったスタイルから外れたことを機械で検知しやすくなります。 // 決めた色のトークンを使った書き方 <button className= "bg-primary" >申し込む</ button > // 決まりから外れた書き方。角括弧があるので、検索で見つかる <button className= "bg-[#0b794d]" >申し込む</ button > 色は決めたトークンだけを使う決まりにしていて、 -[# で検索すれば、外れた箇所を一覧にできます。 型を一貫させたときに起きること この EC サイトでは、配送サイクルの決まりは 1 か所にしかありません。 // packages/domain-values/src/subscription.ts(抜粋) export const cycleDaysSchema = z.union( [ z.literal( 7 ), z.literal( 14 ), z.literal( 21 ), z.literal( 28 ) ] ); export type CycleDays = z.infer < typeof cycleDaysSchema >; この 1 つの定義を、次の全部が参照します。 DB から読んだ値の検査(repository) API の要求と応答の契約(contracts) 画面のコード(ストアフロントと管理画面) API のルートの型は、Hono RPC でフロントのクライアント( hc<AppType> )にそのまま届きます。そのため、API の応答の項目を変えると、それを使う画面のコードが型の検査で落ちます。直し忘れは、実行する前に CI で止まります。実際にあった例を 2 つ紹介します。 例 1: 型を変えた後の直し忘れが、8 つのファイルで見つかった 顧客のメールアドレスは、最初はただの string でした。これを、前後の空白を取り、形式を確かめ、小文字にそろえた値だけが入る型に変えました。 // packages/domain-values/src/email.ts(抜粋) export const emailAddressSchema = z .string() . trim () .pipe(z.email()) .transform(( value ) => value. toLowerCase ()) .brand< "EmailAddress" >(); export type EmailAddress = z.infer < typeof emailAddressSchema >; .brand を付けた型には、このスキーマを通した値しか入りません。ただの文字列を渡すと、型の検査で落ちます。 この変更を進めている間に、main ではメールアドレスを文字列のまま渡すテストが足されていました。main を取り込んで型の検査を流すと、API・画面・ドメインの 3 つのパッケージに散った 7 つのテストが落ちました。直し方はどれも同じです。 // 直す前: 型の検査で落ちる customerContact= { { email : "member@example.com" , phone : null } } // 直した後 customerContact= { { email : emailAddressSchema.parse( "member@example.com" ), phone : null } } 型が無ければ、検証を通していない値が渡っていることに気付く手がかりがありません。型の検査は、直す場所をファイルと行で全部挙げます。 例 2: 画面に届く応答の型が広がっていたのを直し、型だけのテストで固定した 管理画面の API には、URL の番号の形式が合わないときに 404 を返す共通の部品があります。この部品が応答を { error: string } で受けていたため、エラーのコードの型が string に広がっていました。Hono RPC は応答の型をそのまま画面に届けるので、画面の側では、どのコードが返るのかが型から分からなくなっていました。 // apps/admin-api/src/middleware/validation.ts(抜粋) // 直す前 export function codeParamValidator < Schema extends ZodType >( schema : Schema , notFound : { error : string } , ) { /* ... */ } // 直した後: 渡されたコードをリテラル型のまま保つ export function codeParamValidator < Schema extends ZodType , const NotFound extends { error : string } , >( schema : Schema , notFound : NotFound ) { /* ... */ } 同じことがまた起きたら型の検査で落ちるように、画面の側に、型だけを確かめるテストを足しました。実行時には何もしないテストです。 // apps/admin/src/lib/api-client.test.ts(抜粋) expectTypeOf< InferResponseType <( typeof apiClient.customers )[ ":code" ][ "withdrawal" ][ "$post" ], 404> >().toEqualTypeOf< { readonly error : "customer_not_found" } >(); API の側でこの応答の型が変わると、画面の側のこのテストが型の検査で落ちます。 行ロックと入れ子のトランザクションを使う場面 ORM に Drizzle を選んだ理由の 1 つは、行ロックと入れ子のトランザクション(savepoint)を API で書けることです。EC では、同じデータに 2 つの操作が重なる場面が多くあります。 たとえば、会員がクーポンコードを入れた瞬間に、管理者がそのクーポンを停止することがあります。「使えるか」を確かめてから会員の保有を作るまでの間に停止が入ると、停止されたクーポンが会員の手元に残ります。そこで、クーポンの行をロックして読み、判定と保有の作成を同じトランザクションで済ませます。 // packages/repositories/src/coupons/coupon-code-entry.ts(抜粋) const query = executor .select( { id : coupons. id , grantOnly : coupons.grantOnly } ) .from(coupons) .where(sql ` ${ coupons.code } COLLATE "C" = ${ code } ` ) .limit( 1 ); const [ row ] = await (options?.forUpdate === true ? query.for( "share" ) : query); ロックはクエリの最後に .for("share") を付けるだけで、 row の型は select に書いた列から決まります。一方、Prisma のクエリの API には行ロックの指定がありません( 要望の issue は 2021 年から開いたままです)。同じことをするには、生の SQL を書くことになります。 // Prisma で同じ読み取りを書く場合の例(実際のコードではありません) const rows = await tx.$queryRaw< CouponRow []> ` SELECT id, grant_only FROM coupons WHERE code = ${ code } FOR SHARE ` ; CouponRow は自分で書いた型で、SQL が返す列と合っているかは検査されません。列の名前を変えても型の検査では落ちず、実行して初めて失敗します。行ロックを取る読み取りはリポジトリの中に 50 か所ほどあるので、そこで型が途切れないことを重視しました。 この上に、機械で確かめる決まりを重ねています。規約には「ルールは機械で守れるものから機械化する。散文の規約は機械化できないものに限る」と書いてあります。 パッケージの依存の向きは、Turborepo の boundaries が CI で確かめる。顧客向けの API と社内向けの API は、互いを import できない id は、どの表の id かを型で区別する(Zod の brand)。注文の id を顧客の id として渡すと型エラーになる 外部に触れる部品には、手書きのフェイクと本物の両方に同じテスト(契約テスト)を流し、挙動のずれを見つける 2. その型で個人情報を守る 個人情報も、「気を付ける」ではなく「書き忘れたら通らない」形に寄せました。 列を足したら、分析基盤に出すかを決めないと通らない 全部の表の全部の列を、全社のデータ基盤(Databricks)に「出す」か「出さない」かに分けた定義を、コードに置いています。分類の型を Drizzle の表の定義から導いているので、列を足して分類を書き忘れると型の検査で落ちます。表を足して書き忘れるとテストで落ちます。 // packages/db/src/analytics-exposure.ts(抜粋) export type Exposure = "analytics" | "restricted"; type ColumnExposure<T extends PgTable> = { [K in keyof T["_"]["columns"]]: Exposure }; デプロイのたびに、マイグレーションの後で、データ基盤用の DB のユーザーの権限を全部外し、「出す」列だけに列単位で付け直します。分類と実際の権限が食い違ったまま残りません。個人情報は DB の権限で守られるので、取り込みのコードを通さずに DB を読んでも取れません。 この仕組みは、入れた PR の中でさっそく働きました。main を取り込んだところ、別の PR が足した 3 つの列の分類が無く、型の検査が落ちました。一方で、型では止まらない誤りもレビューで見つかっています。サポート担当が自由記述する列(返金の理由など)が「出す」側に分類されていました。自由記述には顧客の名前や電話でのやり取りが書かれうるので、書く人が誰でも「出さない」にする決まりを足しました。 監査ログに、個人情報の写しを増やさない 管理画面の変更履歴の元になる監査ログも、同じ作りです。全部の列を「前後の値を書く」「書かない」「値を記録せず、変わったことだけ書く」などに分類し、漏れは型とテストで止まります。カードの下 4 桁と有効期限は、値を {"unrecorded": true} に差し替えて記録します。アプリの DB ユーザーは、監査ログに追記しかできません。書き換えと削除の権限を、デプロイのたびに外しています。 エラーの監視には、送る前に止める Sentry の SDK は、何も指定しないと、利用者の情報・Cookie・要求の本文・DB の引数・ローカル変数を集めます。これを止める設定を 1 か所に置き、どのアプリの初期化もそれを使います。届いてから Sentry の側で消す案は、個人情報が一度届いてしまうので採りませんでした。 ログでも同じ考え方を取っています。倉庫のシステムの API が JSON として読めない応答を返したときに、本文の先頭 500 文字を例外のメッセージに入れていたことがありました。受注や出荷の伝票の応答が壊れた形で返ると、宛先の氏名や住所がログに出ます。Claude に差分を分析させたレビューで見つかり、例外に入れるのを状態コード・種類・本文の長さだけに直しました。 id から、他人のデータを順にたどれないようにする 表の id は連番でなく UUIDv7 にしています。連番の id が URL や API に出ると、番号を 1 つずつ変えて他人の注文やお届け先を順に試せます。番号の大きさから、会員や注文の件数も読めます。UUIDv7 なら、この列挙と件数の推測が難しくなります。 ただし、id を認可の代わりにはしていません。誰のデータかの確認は別に必ず行い、他の会員のデータを指す要求には 403 でなく 404 を返して、その id があるかどうかも分からないようにしています。Claude に読ませる規約にも「外に出る識別子に、連番を新しく使わない」と書いてあります。UUIDv7 は作った時刻を含むので、時刻を知られて困るデータには別の形式を検討する決まりです。 そもそも持たないもの カード番号とセキュリティコードの入力欄は、Stripe の Payment Element をそのまま使っています。入力された値は Stripe に直接送られるので、自社のコード・ログ・DB を通りません。会員のパスワードはCognito に渡し、自社の DB には持ちません。この考え方は「 個人情報を自社のDBに持たない設計は、どこまでできるのか - every Tech Blog 」に詳しく書かれています。 3. そのほかにやったこと 型とテストで確かめられるのは、形にできた決まりだけです。そのほかに、次のようなこともしています。 Claude Code が読む規約(AGENTS.md)には、個人情報の扱いで守ることを書いています。個人情報をログや監視に出さない、渡す相手は一覧にある相手だけにする、他の会員のデータを指す要求には 403 でなく 404 を返す、外に出る識別子に連番を使わない、といった内容です。Claude Code のセキュリティレビューのプラグインも入れていて、「境界は会員個人にある」といった、このサービス固有の観点を渡しています。認証・認可・決済の境界に触る変更は、人のレビューを必ず通します。 規約そのものが実態とずれていたこともあります。以前の規約は「個人情報をログ・監視・外部 SaaS に平文で出さない」でした。しかし実際には、倉庫には宛先の氏名・住所・電話番号を、決済代行には請求先の氏名を渡しています。そこで「外部に渡すのは業務に要る相手だけ」として渡してよい相手の一覧を書き、相手を足すときはその行を直してレビューを受ける形にしました。 まとめ 今回は、EC サイトの新規開発で、Claude を中心に開発を進めるために選んだ技術と、その上で個人情報を守るためにしたことを紹介しました。 言語を TypeScript に統一し、Hono と Drizzle で DB から画面まで型をつなげたことで、Claude が書いたコードの直し忘れを、動かす前に型の検査で見つけられるようになりました。個人情報についても、分析基盤に出す列や監査ログに残す値を型とテストで確かめる形にして、書き忘れたら通らないようにしています。型で確かめられない部分は、Claude に読ませる規約とセキュリティレビュー、人のレビューで補っています。 私自身、新卒エンジニアとして入社半年でここまでの仕事を任せていただけるとは夢にも思っていませんでしたが、手でコードを書いていたときとは違った角度で技術そのものの強みや使いどころについて理解を深められ、良い機会になりました。型やテスト、DB の権限のように、AI に書かせる今だからこそ効いてくる仕組みのありがたさを、実際に作りながら感じました。 AI を中心に開発を進めようとしている方や、個人情報を扱うサービスを作っている方の参考になれば幸いです。 最後までお読みいただきありがとうございました!
はじめに こんにちは。スタメンでエンジニアをしております、 Ki と申します! 実はこの度、2026年10月に開催されたHonoConfのUser Trackにて、30分間発表させていただきました!ざっくり内容は以下の通りです。 speakerdeck.com TUNAGはRails・React・Next.jsと技術基盤を拡張する中で、機能変更が複数のリポジトリや担当領域にまたがり、調整や学習オーバーヘッドが増えていた 新Honoサブアプリでは、React/Viteの画面とHonoのバックエンドを専用リポジトリへ切り出し、既存Railsの認証・組織情報を活用しながら、画面からAPI・業務処理・保存までをTypeScriptで一気通貫に開発できる構成で実装を進めた Honoのミドルウェアと型付きContextで、既存TUNAGへの認証委譲とユーザー情報の受け渡しを実装した。認証・ログなどの共通処理をまとめ、各ルートではフォーム固有の処理に集中できるようにした app.request() でHTTPサーバーを起動せずにAPIをテストし、同じHonoアプリをローカルのNodeと本番のAWS Lambdaで利用している。本番で回答一覧がレスポンスサイズ上限に達した際も、Lambda向けアダプターと配信設定を変更してストリーミングに対応した 機能全体を単一リポジトリ・同一言語で追える構成は、少人数でのAI駆動開発と相性が良く、導入以前の開発手法よりも20倍効率よく実装することができた 今回は時間の都合上盛り込めなかったネタをこちらで書いていきます🙏 AI駆動開発(AI-DLC)と今回の開発手法の相性について 今回のHonoサブアプリは AI-DLCというAI駆動開発のフレームワーク を用いて開発を進めました。具体的に相性が良かったと感じた点は以下になります。 機能の実装に必要なコンテキストを、一つのリポジトリにまとめられたこと フロントエンドからバックエンドまで、TypeScriptで変更を追えたこと 型チェックとテストを使い、AIの実装結果を検証しやすかったこと 仕様や設計判断を文書に残し、人間とAIで前提を共有できたこと まず大きかったのは、 AIエージェントが参照する範囲を、開発する機能の単位でまとめられた ことです。 従来の構成では、一つの機能を変更するにも、共通フロントエンドと別リポジトリのバックエンドを行き来する必要がありました。今回のHonoサブアプリでは、画面・API・業務処理・保存までが同じリポジトリにあります。例えば、あるフォームの入力項目を追加する場合も、画面だけでなく、APIの入力検証や保存処理まで関連するコードを追いやすい構成です。 もちろん、同じリポジトリに置けばAIがすべて理解してくれるわけではありません。それでも、「今回の変更に関係するコードがどこにあるか」を示しやすくなったことは、AIと開発を進めるうえで大きな利点でした。 また、フロントエンドとバックエンドをTypeScriptで統一したことで、私自身が AIの変更内容を確認しやすく なりました。画面からバックエンドまで同じ言語で読めることは、AIに実装を依頼する場合のみならず、自分が処理の流れを追い、変更の意図を確認しやすいという点でも大きな助けとなっています! 実装後の確認では、型チェックとテストも役立ちました。型の不整合を検出できれば、エラーを手掛かりに修正を進められますし、Honoの app.request() を使えば、わざわざHTTPサーバーを起動せずにAPIの振る舞いを検証できます。AI駆動開発では、「コードを生成して終わり」ではなく、 変更→検証→結果を基に修正 というイテレーションを回し続けることが重要です。このイテレーションを高速で回すという点でHonoの機能が大いに役立ちました。 そして、コードと同じくらい重要なのが、 仕様や設計判断の共有 です。AI-DLCは、AIとの開発を構造化し、検証可能な流れとして進めるためのフレームワークです。Honoサブアプリでも、期待する振る舞いは仕様として、設計判断の理由はADRとして参照できるようにしています。コードだけでは分からない「なぜこの構成なのか」「何を守る必要があるのか」を残すことは、AIに適切なコンテキストを渡せることに加えて、私以外のメンバーがコードベースを見た際の理解速度の向上にも繋がります。 人間が機能全体を理解し、AIにも必要な前提を渡し、実装結果を検証できる状態を作ることが、AI駆動開発を進めるうえで大切です。今回のHonoサブアプリの構成はこの状態を構築する上でかなり相性が良かったのではないかと考えております! まとめ 実はHonoConf当日、私は風邪をひいてしまっており懇親会まで参加することができなかったのですが、それでもいくつか他の方の発表を目にする機会がありました!中でも、他の登壇者の方が挙げられていたHonoとAI駆動開発の相性の良いポイントが私の考えていたこととほぼほぼ一致しており、嬉しかったです。 実は、私はちょうど1年前のHonoConf 2025にも聴衆として参加していたのですが、その時は自分がまだ営業からエンジニアにコンバートしたてということもあり、よくわからない呪文をずっと聞いているという感覚でした。それが今回、登壇まで出来たのは自分を支えてくれた同僚や上司の存在が大きかったように思います! 最後に スタメンでは、ビジネスサイドからエンジニアへのコンバートを支援する活動を行なっています!また、一緒に働くエンジニアも募集しています!興味を持っていただけた方は、ぜひ下記からご覧ください。 herp.careers 最後まで読んでいただきありがとうございました!
AI開発時代のWeb技術スタックを考えてみた   PSSLの佐々木です。 これまで業務アプリは、深く考えずに「とりあえずReact + FastAPI」のようなSPA+APIの構成で実装してきました。 ただ最近はClaude CodeなどのAIがコードの大半を書いてくれます。Reactを選んでいた理由は「JSを書くのがつらい」でしたが、AIが書くなら技術者側の理由で技術選定をするのではなく、作るものに応じた最適なものを選定すべきだろうと思いました。 この記事では、AI開発時代のWeb技術スタックについて海外・国内の記事やベンチマークを調べ、「どんな時に何を選ぶべきか」を私の意見も含めて整理します。 ReactやNextを選択していた理由 まずは振り返りです。昔はDjangoやPlay、Springなどのサーバー側レンダリング + jQueryで画面を作っていましたが、これがとにかくつらかったです。 サーバー側レンダリング + jQueryのつらさ 状態がDOMの中にしかない(「いま何が選択されているか」をDOMから読む) HTMLとJSの結びつきがセレクタ頼み( $('.btn') が別の画面の同名クラスにも当たる) Ajaxで差し替えた要素にイベントが付かない、または二重に付く 変数も関数もグローバルで、どのページで何が動くか追えない 初期表示はテンプレート、更新後はJSの文字列組み立て、という二重実装 型もモジュールもなく、壊れたことに実行するまで気づけない 「予期せぬところでJSが発火する」の正体はこれです。つらかったのは書く量ではなく、 構造を与える仕組みがなかった からですね。 Reactが解決してくれたこと 宣言的UI(状態を変えれば表示が追従する) コンポーネントという境界(影響範囲が読める) 初期表示と更新が同じコード モジュールとTypeScriptの型 豊富なUIライブラリ jQueryのつらさへの直接の回答だったので、移行したのは合理的だったと思います。 React / Next + API分離のつらさ ただ、Reactに移行してすべて解決したかというとそうでもなく、 サーバーとクライアントを分断した代償 が生まれました。 真実が2か所になる(本物はDBなのに、クライアントがコピーを持つ) サーバーがブラウザに指示できない(リダイレクト、認証切れ、完了後の遷移をクライアントで自前実装) あらゆる通信に「読み込み中・エラー・空」の3状態がつく(画面が一瞬白くなる、ちらつく) バリデーション、型、権限チェックをフロントとバックの両方に持つ useEffectの依存配列、サーバー/クライアントコンポーネントの境界、キャッシュなどフレームワーク固有の難しさ デプロイが2系統になり、CORSやトークンの受け渡しが必要 まとめるとこうです。 構成 解決したこと 代償 サーバー側レンダリング + jQuery サーバーが真実で、制御が単純 ブラウザ側のコードに構造がない React / Next + API ブラウザ側のコードに構造がある 真実が分断され、同期と制御が複雑 とほほ。どちらも一長一短です。 仮説:AI時代ならサーバー側レンダリングに戻せるはず どちらもつらいなら、AI時代はサーバー側に戻すのが正解だと考えました。 Reactを選んだ理由は「JSを書くのがつらい」だった。AIがそれを解消するなら、DjangoやRailsのサーバー側レンダリングに戻し、ロジックをAPIとして再利用できる形にしておいたほうが、ブラウザの制御がしやすくて合理的なはずだ。 この仮説がどこまで正しいのか、海外・国内のブログ、ベンチマーク、各社のテックブログを調べてみました。 調査結果 結論から言うと、仮説は「方向」は合っていて、「理由」が間違っていました。 先に注意点です。同じ業務アプリを「サーバー駆動」と「SPA + API」で作り、AIの成功率を比べた中立の実測は見つかりませんでした。以下は間接的な実測と体験談からの判断です。 合っていたこと サーバー側に寄せるとブラウザ制御は単純になる htmxは HX-Redirect などのレスポンスヘッダで、サーバーからブラウザに直接指示できる( htmx reference ) InertiaやTurboも、サーバーのリダイレクトにそのまま従う( Inertia: The Protocol ) 境界を減らすとAIの成功率が上がる 変更が1ファイルなら成功率17.7%、4ファイルなら6.9%( SWE-PolyBench 、Claude 3.5 Sonnet時代の数字) LayerXはフロントとバックのリポジトリを統合し、API追加が1つのPRで完結するようになったと報告( LayerXの登壇資料 ) ロジックは再利用できる形にしておく ただし「APIを先に作る」ではなく「サービス層に集める」( HackSoft Django Styleguide ) HTMLビューもJSON APIもMCPツールも、サービス層を呼ぶ薄い皮として後から足せる 間違っていたこと 「AIがJSを書いてくれるから戻せる」は理由にならない AIが解消するのは書く手間だけで、構造は与えてくれない AIが最も無難に書けるのは依然React(AIアプリビルダーの既定もReact + Tailwind) htmxの hx-target もファイルをまたぐ文字列の約束事で、jQueryのセレクタ問題と同じ落とし穴がある テンプレートは検証が弱い Djangoテンプレートは、変数が存在するかどうかを静的に検査できない( django-language-server ) 型チェックを編集のたびに回すとAIの成功率が約80%から84%に上がるという報告があり、テンプレートはこの恩恵を受けにくい( Pyrefly ) 白フラッシュは素のMPAに戻すとむしろ増える ページ間のView TransitionsはChrome 126以降とSafari 18.2以降で使えるが、Firefoxは未対応( web-features explorer ) hx-boost やTurbo Driveのような仕組みが別途必要 つまり最適解は「サーバー側レンダリングに戻る」ではなく、 サーバーを唯一の真実にして、1機能がまたぐ境界(リポジトリ・言語・スキーマ層)を減らす ことでした。 主要な構成の種類 調べた結果、業務アプリの構成は大きく4つに分けられます。 1. サーバー駆動(Django / Rails / Laravel + htmx / Hotwire / Livewire) 例: Django + htmx、Rails + Hotwire、Laravel + Livewire 特徴: サーバーがHTMLを返し、振る舞いはHTML属性に宣言的に書く リダイレクトや認証切れをサーバーが直接指示できる クライアントのステート管理がほぼ不要 1リポジトリ1言語で、CORSも別ビルドも不要 Railsのベンチマークでは最良モデルが21タスクの92%を解決( Rails Foundation ) こんな時に選ぶべき: 一覧・検索・フォーム・承認フローが中心の業務アプリ リッチな画面が数枚で済む 少人数でフルスタックに開発したい 注意点: ドラッグ&ドロップ、楽観的更新、条件分岐の多い動的フォーム、オフライン、共同編集は苦手 テンプレートは型検査が効かないので、レンダリングテストとE2Eで補う htmx 4.0は2026年8月公開で破壊的変更あり。AIの知識は2.x中心と考えられるので、当面は2.x固定が無難( htmx 4.0.0 ) AIがフレームワークの既存APIを使った割合は8〜35%どまり。規約はリンタとレビューで強制する 2. Inertia型モノリス 例: Laravel + Inertia + React 特徴: ルーティング・認証・検証はサーバー、描画だけReact API層もGraphQLもクライアント側のデータ取得層も消える Reactのコンポーネント資産と型は残せる v3(2026年3月)で楽観的更新に対応( Laravel News ) こんな時に選ぶべき: リッチな画面が多いが、API分離のつらさは解消したい チームがすでにReactに慣れている バックエンドにLaravelを選べる 注意点: 一級の統合を確認できたのはLaravelだけ Django向けの類似ライブラリ(django-bridge)は0.x系で、採用実績を確認できず 非公式アダプタではAIが動くコードを出せなかったという体験談あり 3. フルスタックTypeScript 例: Next.js(App Router)、TanStack Start 特徴: 言語が1つで、エンドツーエンドの型が効く 学習データが最も多く、UIの見栄えも良い サーバー/クライアントの境界は消えず、コンポーネント境界とキャッシュに移るだけ こんな時に選ぶべき: 公開サイトでSEOや初期表示速度が重要 UIの質が競争力になるプロダクト チーム全員がTypeScript 注意点: 変化が速い。Next.js 16のAPIではAIの合格率が53%で、ドキュメント索引を渡して100%( Vercel ) 2025年12月にRSCでCVSS 10.0の脆弱性(React2Shell)が公開された( UpGuard ) 認証の内側だけで動く業務アプリにSSRはほぼ不要 4. SPA + 型付きAPI 例: Vite + React + FastAPI(OpenAPIからクライアント生成) 特徴: リッチな画面を制約なく作れる OpenAPIからクライアントを生成すれば、契約のずれがビルドエラーになる( FastAPI docs ) 同じAPIをモバイルアプリからも使える こんな時に選ぶべき: グリッド編集やドラッグ&ドロップが主要機能を占める オフライン動作やリアルタイム共同編集が要件にある ネイティブアプリとAPIを共有する 注意点: リダイレクトや認証切れをクライアントで扱い続ける構造は残る フロントとバックは1つのリポジトリにまとめる GraphQLは「DBモデル・スキーマ・TypeScript型」の3層を揃える負担があり、社内アプリでは維持する根拠が弱い(私の判断です) 実用的な選択フローチャート ステップ1: 「最も難しい画面」を確認 グリッド編集、ドラッグ&ドロップ、項目同士が複雑に連動するフォームがどのくらいあるかを数えます。ここが最大の分岐点です。 ほぼない、または数枚 → サーバー駆動(難しい画面だけReactの島にする) 主要機能を占める → Inertia型 または SPA + 型付きAPI ステップ2: 要件を確認 オフライン動作、リアルタイム共同編集、ネイティブアプリとのAPI共有がある → SPA + 型付きAPI 公開サイトでSEOが重要 → フルスタックTypeScript どれもない → サーバー駆動 または Inertia型 ステップ3: チームの資産を確認 Pythonの資産を活かしたい → Django + htmx Reactの資産を活かしたく、Laravelに移れる → Laravel + Inertia + React ReactもPythonも捨てたくない → Vite + React + FastAPI(Next.jsは外す) 私の場合はPythonバックエンドで、フォームと一覧が中心の業務アプリが多いです。なので新規は Django + サービス層 + htmx 2.x、難しい画面だけReactの島 が第一候補になります。 既存のNext.js + FastAPIを一括で書き直す必要はありません。次の順で段階的に進めるのが現実的です。 フロントとバックを1つのリポジトリにまとめる ロジックをサービス関数に寄せる 新規エンドポイントからGraphQLをやめ、OpenAPIのクライアント生成に切り替える リダイレクトや認証切れで困っているCRUD画面から、1画面ずつサーバー描画に置き換える まとめ AIはコードを書くコストを下げましたが、理解と検証のコストは下げていません。なので構成を選ぶ基準は「書きやすさ」から、「1つの変更が何か所に波及し、その正しさを機械がどこまで確かめられるか」に移ります。 各構成の選択基準: サーバー駆動(Django + htmx など): フォームと一覧が中心で、境界を最小にしたい時 Inertia型モノリス: リッチな画面が多く、API分離のつらさも解消したい時 フルスタックTypeScript: 公開サイトやUIの質が競争力になる時 SPA + 型付きAPI: リッチな画面が主役、またはオフラインやネイティブアプリ対応が必要な時 本記事を執筆したことで実装するプロダクトに最適化した技術スタックを選べるようになったかと思います。 ではまた ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post とりあえずReact/Next.jsを選ぶのをやめたい|AI時代のWebアプリ技術選定 first appeared on SIOS Tech Lab .

動画

書籍