Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ネイティブアプリとWebフロントエンドのAPI通信ラッパーにおける共通化の勘所

 ネイティブアプリとWebフロントエンドのAPI通信ラッパーにおける共通化の勘所

仕事において「Webフロントエンドでサービス開始されたが、あとからネイティブアプリも必要になった」という状況が起こり得ます。

そのときに起こりがちなのが、React Native製ネイティブアプリとReact Router v7以降(元Reimix)などのWebフロントエンドで、独自にAPIクライアントが生えてしまい、仕様変更のたびに2倍のコストで改修することになる問題です。
本セッションでは、このカオスを避けるために、API通信ラッパーをどこまで共通化するのかという現実的な戦略を共有します。

特に以下の3点を扱います。

ネイティブとWebフロントエンドの違い(ネットワーク品質・バックグラウンド実行・ストレージ・オフライン対応・認証方式・エラー表現など)を踏まえた、バックエンドのAPI設計において配慮すべきポイントと具体例
API通信ラッパー共通化のためのレイヤー構成(型付き共通クライアント層/トークン管理やナビゲーション連携などのプラットフォーム依存層)と、実行環境の差分を吸収する実装パターン
Orvalを用いたOpenAPI+型生成、HTTPクライアント、SWR系ライブラリを選定する際の判断基準と、「ネイティブだけ別実装にしてしまい後から辛くなった」「最初に共通ラッパーへ投資して変更が楽になった」といった実例
[持ち帰ってもらうこと]

ネイティブアプリ追加時にも破綻しないAPI設計・レスポンス設計のチェックリストを自チームで作成できるようになる
React Native/Webフロントエンドの両方から使い回せるAPIラッパー構造を、自プロダクト向けに設計し直す際のたたき台を持ち帰れる
フロントエンド/ネイティブアプリ/バックエンド間で「どこまで共通化し、どこから分けるか」を合意形成するための議論のフレームワークを得られる

Avatar for Suguru Ohki

Suguru Ohki

March 21, 2026
Tweet

More Decks by Suguru Ohki

Other Decks in Programming

Transcript

  1. ネイティブアプリとWeb フロントエンドの API 通信ラッパーにおける共通化の勘所 PHPerKaigi 2026 [ 2026.03.21 ] MOSH

    develops and operates a platform that supports independent creators in selling their services online. © MOSH, Inc.
  2. © MOSH, Inc. 大木 優 MOSH 株式会社 エンジニア LaravelLiveJapan コアスタッフ

    React Router v7 (Web) React Native / Expo (Mobile) Hono (PHP がスタックから消えちゃった悲しい) @suguru_ohki    @SuguruOoki 発表に入りきらなかったところはBlog へ
  3. 今日持ち帰ってほしいこと 後からNative アプリが追加されたとき、何を考慮すべきか? 1. Web とNative の 違い を知り、API 設計で地雷を踏まない

    2. 実際に踏んだ地雷と、その 具体的な痛み を追体験する 3. 痛みを解消した 共通化戦略とツール選定 のたたき台を得る © MOSH, Inc.
  4. よくある悲劇 ─ 仕様変更コスト2 倍問題 Before: Web だけの世界 Web → apiClient.ts

    → Backend API 仕様変更 = バックエンド + フロント1 箇所 = コスト1 After: Native が加わった世界 Web → webApiClient.ts → Backend API Native → nativeApiClient.ts → Backend API 仕様変更 = フロント 2 箇所 = コスト2 、しかも 片方だけバグる © MOSH, Inc.
  5. 「WebView でよくない?」── 不採用の理由 WebView で最初にぶつかる壁 Push 通知: WebView からネイティブのPush 通知を制御するには、結局ネイティブコードが必要

    決済フロー: App Store/Google Play のガイドライン上、WebView 内課金はグレーゾーン。審査リジ ェクトのリスクが高い カメラ・生体認証: WebView からFaceID/TouchID やカメラへのアクセスは制限が多く、ブリッジが 複雑化する © MOSH, Inc.
  6. 「WebView でよくない?」── 運用フェーズで詰む壁 パフォーマンス: Web 前提のレンダリング + モバイル回線 + WebView

    のオーバーヘッドで体感速度 が悪化 UX 期待値: スワイプバック、画面遷移アニメーション、操作時の振動フィードバックが再現不可。 「アプリっぽくない」とユーザーから言われる デバッグ困難: WebView 内のJS エラーはブラウザエンジン内で処理され、Crashlytics などのネイティ ブクラッシュレポートに出ない。障害時の原因特定に時間がかかる App Store 審査: Apple の「Guideline 4.2 Minimum Functionality 」に抵触。ネイティブ機能を活用し ていないアプリは「Web サイトのラッパーにすぎない」としてリジェクトされる 結論: ネイティブで作る判断はよさそう。API の通信あたりをどうするか? © MOSH, Inc.
  7. 前提知識: Hermes エンジンとは React Native のデフォルトJS ランタイム Meta (旧Facebook )が開発したReact

    Native 専用のJavaScript エンジン 2019 年にオープンソース化、React Native 0.70 以降でデフォルト有効 V8 (Chrome )やJavaScriptCore (Safari )とは別の独自実装 なぜHermes が使われるのか 起動速度の高速化: JS をビルド時にバイトコードに事前コンパイル(AOT ) メモリ使用量の削減: モバイル端末のリソース制約に最適化 アプリサイズの縮小: 必要な機能だけを含む軽量設計 ただし、ブラウザエンジンではない fetch やXMLHttpRequest はHermes 独自の実装(ブラウザの仕様と挙動が異なる場合がある) この違いが、後に説明するCookie の問題につながる © MOSH, Inc.
  8. 認証方式の違い ─ なぜNative でCookie が壊れるのか ブラウザのfetch とReact Native のfetch は別物

    ブラウザのfetch React Native のfetch 実装 ブラウザエンジン(C++ ) Hermes エンジン上のpolyfill Cookie 管理 Cookie Jar で自動管理 統合されたCookie 管理がない 品質 何十年もの実績 Cookie 周りの品質がブラウザレベルに達していない 具体的に何が起きるか credentials: "include" を付けると レスポンスが返らず処理が止まる(iOS Hermes ) Debug ビルドでは正常なのにRelease ビルドでのみ再現するケースもある withCredentials: true のXHR が他のXHR のコールバックをブロックする問題も © MOSH, Inc.
  9. なぜXMLHttpRequest なら動くのか fetch とXHR の内部実装の違い fetch (Hermes) XMLHttpRequest 実行場所 JS

    エンジン内部 OS のネイティブネットワーク層 iOS Hermes のバグの影響を受ける NSURLSession にブリッジ → 影響なし タイムアウト 機構がない → 止まったら永遠に止まる OS 側に機構あり → 確実にエラーが返る → apisauce (axios 系)はXHR ベースなので、Hermes のfetch バグを回避できる © MOSH, Inc.
  10. 実際に採用したハイブリッド認証 Web Native セッション ブラウザがCookie を自動保存・自動付与 apisauce (XHR) + withCredentials:

    true で回避 API 認証 AWS SigV4 署名 X-Id-Token ヘッダで手動送信 トークン保存 ブラウザ管理 メモリキャッシュ + SecureStore リフレッシュ サーバーがCookie 更新 Cognito 直接呼び出しで再取得 結局Web/Native で認証コードが完全に分岐する → customFetch で吸収するしかない © MOSH, Inc.
  11. データ取得パターンの違い(最重要) Web (React Router v7) Native (React Native) データ取得 loader/action

    (フレームワーク管理) TanStack Query / SWR (自前管理) SSR あり なし キャッシュ フレームワーク管理 自分で設計 レスポンスサイズ 1 リクエスト=1 ページ分もOK 軽量・段階取得が必須 この違いがあるから、hooks 以上の共通化は慎重になるべき © MOSH, Inc.
  12. loader の恩恵と、Native で再現できない理由 loader がもたらすもの Loading 不要: 画面を見た瞬間にデータ表示済み SEO: クローラーが完成したHTML

    を読める 初期表示速度: JS 実行前にHTML にデータ → FCP が速い データ一貫性: サーバーで1 回取得 → race condition が起きにくい Native で再現できない理由 loader の恩恵 Native で使えない理由 SSR 統合 Native に SSR という概念自体がない Loading 不要 遷移前取得 → 画面遷移がブロック される データ一貫性 復帰時の 再取得はhooks でしか対応できない hooks を共通化 = loader の恩恵を捨てることになる © MOSH, Inc.
  13. ここまでのまとめ 論点 結論 WebView でいい? Push 通知・決済・審査リジェクト → ネイティブで作る判断は正しい 認証は共通化できる?

    Cookie/ トークン管理が根本的に違う → customFetch で差分を吸収 データ取得は共通化できる? loader (SSR ) vs hooks (CSR )→ hooks まで共通化するとloader の恩恵を失う © MOSH, Inc.
  14. 地雷1: Cookie 認証がNative で動かない Web 版の前提 httpOnly Cookie でセッションをバックエンドAPI サーバーのCookie

    として自動送信 AWS SigV4 署名でAPI リクエストを認証 フロントエンドは認証を意識しなくていい設計 Native で起きたこと React Native にはブラウザのようなCookie の自動保存・自動付与の仕組みがない SigV4 署名に必要なCognito 認証情報の取得方法がWeb と全く違う 「認証通らない」でNative 開発が止まった さらにバックエンドの認証方式が変更され、PoC アプリが突然ログインできなくなった バックエンドがCookie 前提で設計されていると、Native 追加時に認証基盤の改修が必要になる © MOSH, Inc.
  15. 地雷2: Web 向けAPI レスポンスがモバイル回線で激重 Web 版の前提 1 画面に必要なデータを 1 リクエストで全部返す

    設計 WiFi/ 有線前提でレスポンスサイズを気にしていなかった ページ単位のキャッシュでフレームワークが管理 Native で起きたこと 1 画面表示に大きなJSON を返すエンドポイントがそのまま使われた モバイル回線(3G/4G )では 表示が遅い 画面が頻発 オフセットベースのページネーション → 無限スクロールと相性最悪 Web 向けに設計されたAPI レスポンスは、Native のネットワーク環境では破綻する © MOSH, Inc.
  16. 地雷3: 「とりあえず別ファイル」が生んだカオス 時系列で何が起きたか 1. Web 版はリリース済み、Orval でOpenAPI をもとにClient 生成 2.

    Native 追加時、 「とりあえず」 nativeApi.ts を新規作成 3. 認証フローが微妙に違う(Web はCookie/SigV4 、Native はIdToken ) 4. API 仕様変更のたびに 2 ファイル修正 → 片方の修正漏れ 具体的な痛み 問題 影響 レスポンス型の定義が2 箇所 型の不整合でランタイムエラー エラーハンドリングが分散 401 処理の挙動がWeb/Native で違う テストも2 系統 メンテナンスコスト2 倍 「Web で直したけどNative で忘れた」 本番インシデント複数回 © MOSH, Inc.
  17. 地雷から学んだ3 つの教訓 教訓1: 認証はBearer Token も受け付けるようにする 理想: 最初からBearer Token 方式で設計

    現実: 既存Web のCookie 認証はそのまま、Native 用にBearer Token を追加で受け付ける 教訓2: Native 向けの軽量エンドポイントを用意する 理想: 全API をNative の回線品質前提で設計し直す 現実: 既存API はWeb で使い続け、重いエンドポイントだけNative 向けに軽量版を追加 教訓3: API クライアントの共通化に投資する 理想: 最初から共通ラッパーで設計 現実: 既存Web のコードは変えず、Orval + customFetch の層を上から被せる 既存Web を壊さずにNative を追加する ── それがLv.2 戦略の前提 © MOSH, Inc.
  18. 共通化の4 レベル Lv.1: 型定義のみ共通 型だけ共有。API 呼び出し・エラーハンドリング・キャッシュは完全に別。 Lv.2: 型 + Fetcher

    関数を共通 ★推奨 呼び出し関数は同じ。データ取得の仕組みはプラットフォームに最適化。 Lv.3: Fetcher + エラーハンドリング共通 DI (依存注入)パターン必須。PoC 段階ではオーバーエンジニアリング。 Lv.4: Hooks ・キャッシュ戦略まで共通 React Router v7 のloader/action を 諦める 必要あり。既存Web への影響大。 © MOSH, Inc.
  19. 4 レベル比較マトリクス Lv.1 Lv.2 Lv.3 Lv.4 型の共有 ◦ ◦ ◦

    ◦ fetcher 共有 × ◦ ◦ ◦ エラーHD 共有 × × ◦ ◦ hooks 共有 × × × ◦ 導入コスト 低 低〜中 中 高 メンテコスト 低 低 中 高 RRv7 loader 活用 ◦ ◦ ◦ × 「既存Web を壊さず、Native を速く立ち上げる」ならLv.2 が最適解 © MOSH, Inc.
  20. なぜ Lv.2 が最適解なのか ─ 共通化の範囲 Lv.2 で共通化 Web 独自 Native

    独自 型定義 User, Response, Error - - fetcher 関数 getUser(), createOrder() - - Zod バリデーション レスポンス検証 - - データ取得 - SWR hooks / loader TanStack Query 認証 - Cookie / SigV4 ID Token / リトライ キャッシュ - フレームワーク管理 MMKV / オフライン 型・fetcher ・Zod を共通化し、hooks ・認証・キャッシュはプラットフォームに任せる © MOSH, Inc.
  21. なぜ Lv.2 が最適解なのか ─ 他レベルとの比較 他レベルの問題 Lv.2 の優位性 Lv.1: 型だけ共有

    → fetcher 手書きで 型ズレが起きる fetcher 関数も自動生成で 型ズレが構造的に不可能 Lv.3: DI 設計が必要 → 既存Web の改修が大きい 既存Web のloader/SWR に 手を入れずに導入できる Lv.4: hooks 共通 → loader/SSR の恩恵を失う Web/Native それぞれの 最適なデータ取得を維持 Lv.2 = 「共通化の効果が最大で、既存Web への影響が最小」のスイートスポット © MOSH, Inc.
  22. API 通信ラッパーの3 層アーキテクチャ Layer 2: customFetch Layer 3: UIフック層 Backend

    API PHP Layer 1: 共通層 OpenAPI Orval 型+fetcher SWR Hooks / loader TanStack Query Cognito + SigV4 ID Token + リトライ © MOSH, Inc.
  23. この設計の背景にあるパターン 腐敗防止層(Anti-Corruption Layer ) 外部の依存要素の変更から自身のコード領域を守る層 customFetch = プラットフォームの認証差分が、API 型定義に漏れ出さないための層 アダプターパターン(Ports

    & Adapters ) Layer 1 (型+fetcher )= Port: Web/Native 共通のインターフェース Layer 2 (customFetch )= Adapter: プラットフォームごとの実装を差し替え 契約駆動設計(Contract-First ) OpenAPI スキーマ = バックエンドとフロントエンドの契約 契約を先に定義し、実装はそこから自動生成する © MOSH, Inc.
  24. 参考になる発表 腐敗防止層・インターフェース設計 岡田正平(@okashoi) 「設計の考え方 - インターフェースと腐敗防止層編」PHP カンファレンス福岡 2024 speakerdeck.com/okashoi OpenAPI

    スキーマ駆動開発 武田憲太郎(@KentarouTakeda) 「Laravel OpenAPI による " 辛くない" スキーマ駆動開発」 PHPerKaigi 2024 speakerdeck.com/kentaroutakeda 堅牢なコード設計 和田卓人(@t_wada) 「予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント」PHPerKaigi 2022 / PHP カンファレンス福岡2025 speakerdeck.com/twada © MOSH, Inc.
  25. バックエンドAPI 設計チェックリスト 地雷を踏まないための6 項目(既存Web 影響なしで追加可能) Bearer Token 認証を追加 ← Cookie

    認証はそのまま、Native からも認証できるようにする べき等性キー(Idempotency-Key )← 既存API に後付け可能、Web にも恩恵あり cursor-based ページネーション ← 新規エンドポイントから段階的に導入 フィールド選択(?fields=id,name )← 指定なしなら全フィールド返すので既存互換 machine-readable エラーコード ← 既存のmessage にcode を追加するだけ ISO 8601 UTC 日時 ← フォーマット変換のみ、ロジック変更なし 全てWeb の既存動作を変えずに追加・拡張できる © MOSH, Inc.
  26. Native 側のcustomFetch で吸収する方法 チェックリスト項目 バックエンド対応不可時のNative 側対応 Bearer Token 非対応 customFetch

    でCookie をXHR 経由で送信(apisauce ) べき等性キーなし クライアント側でリクエストID 管理 + 二重送信防止UI offset-based のみ Native 側でoffset 管理 + 重複排除ロジック エラーコードなし HTTP ステータスコード + message テキストで分岐 customFetch の層を厚くして吸収できるが、メンテコストは上がる → 中期的にはバックエンド側の対応を交渉すべき © MOSH, Inc.
  27. チェックリスト解説 ─ ネットワーク・認証編 項目 Web だけなら Native が加わると Bearer Token

    認 証 Cookie で十分。フロントは認証を 意識しない Cookie が使えない。認証基盤の改修が必要になる べき等性キー 通信安定。リトライはほぼ不要 トンネル・電車内で切断→リトライ。二重実行を防ぐ仕組 みが必須 ISO 8601 UTC 日時 サーバーでJST に変換して返せば いい 端末のタイムゾーンが様々。UTC で返してクライアント側 で変換しないとズレる Web だけなら「なくても動く」ものが、Native では「ないと壊れる」 © MOSH, Inc.
  28. チェックリスト解説 ─ レスポンス設計編 項目 Web だけなら Native が加わると cursor-based ページネーショ

    ン offset+limit で十分。ページ番号で 管理 無限スクロールが標準UX 。offset だとデータ重複・欠落が 発生する フィールド選択 WiFi 前提で全データ返しても問題 ない モバイル回線では致命的。必要なフィールドだけ返す設計 が必須 machine- readable エラーコード エラー画面に遷移すればいい Alert/Toast/ リトライ/ ログイン画面遷移をコードで出し分 けが必要 これらはすべて 後から対応すると既存Web にも影響が出る → 最初から入れておくのが最もコストが低い © MOSH, Inc.
  29. OpenAPI + Orval: 同じスキーマからプラットフォーム別に生成 ✅ 共通 📱 Mobile 🌐 Web

    📄 openapi.ym Orval: sw SWR Hooks Orval: fetc fetch関数 Zod MSW Moc © MOSH, Inc.
  30. PoC はモノレポ、正式版はリポジトリ分離にした話 PoC フェーズ: モノレポで高速に検証 openapi.yml / Web / Native

    が同一リポジトリ → 変更が即反映 API 共通化の仕組みを検証するには最適 正式版: リポジトリを分離した理由 React Native が対応するReact バージョンが、React Router v7 よりも遅れがち モノレポだとNative 側のReact バージョンがWeb のアップグレードの 足枷 になる © MOSH, Inc.
  31. リポジトリを分離しても共通化は維持できる OpenAPI スキーマは 別リポジトリでもSingle Source of Truth Orval 生成は各リポジトリで独立実行(入力のスキーマだけ共有) CI

    でスキーマバージョンの整合性をチェック 共通化 ≠ モノレポ必須。型の共通化はリポジトリ構成に依存しない © MOSH, Inc.
  32. customFetch: Web 版 ─ SigV4 署名 + Cookie 認証 //

    packages/frontend/app/custom-fetch.ts(実際のコード) export const customFetch = async <T>( url: string, options: CustomFetchOptions, ): Promise<T> => { // AWS SigV4で署名(リージョン: ap-northeast-1, サービス: execute-api) const signedHeaders = await getSignedHeaders(url, options.method, options.body); // Cognito JWT をヘッダに追加(ID Pool → User Pool のフォールバック) const cognitoJwt = await getCognitoUserPoolJwtToken(); if (cognitoJwt) { signedHeaders["X-Cognito-Jwt"] = cognitoJwt; } const response = await fetch(url, { ...options, headers: { ...options.headers, ...signedHeaders }, }); return getBody<T>(response); }; Orval 生成コードからはこのcustomFetch が呼ばれる → 認証を意識しない © MOSH, Inc.
  33. customFetch: Mobile 版 ─ ID Token + 401 リトライ //

    packages/mobile/src/clients/custom-fetch.ts(実際のコード) export const customFetch = async <T>( url: string, options: RequestInit = {}, ): Promise<T> => { const headers = new Headers(options.headers); headers.set("Content-Type", "application/json"); // Cognito Direct経由でID Tokenを取得(キャッシュ優先 → 期限切れなら自動リフレッシュ) const idToken = await getOrRefreshIdToken(); if (idToken) headers.set("X-Id-Token", idToken); let response = await fetch(url, { ...options, headers }); // 401 → トークンリフレッシュ → リトライ(1回のみ) if (response.status === 401 && authStore.getState().isAuthenticated) { const newToken = await getOrRefreshIdToken(); if (newToken) headers.set("X-Id-Token", newToken); response = await fetch(url, { ...options, headers }); if (response.status === 401) authStore.getState().signOut(); } return await response.json(); }; 同じシグネチャ、違う認証ロジック → Orval 生成コードは共通 © MOSH, Inc.
  34. Orval 設定: 同じスキーマ、違うclient 設定 Web 版(SWR hooks 生成) // packages/frontend/orval.config.ts

    output: { client: "swr", // SWR hooksを生成 mutator: { path: "app/custom-fetch.ts", name: "customFetch" }, } Mobile 版(素のfetch 関数生成) // packages/mobile/orval.config.ts output: { client: "fetch", // 素のfetch関数を生成 mutator: { path: "src/clients/custom-fetch.ts", name: "customFetch" }, } 入力のOpenAPI スキーマは同じ。client とmutator の設定だけが違う © MOSH, Inc.
  35. PHP バックエンド側でやるべきこと OpenAPI スキーマを「契約」として管理する // Laravel + scramble でOpenAPI自動生成 composer

    require dedoc/scramble // または l5-swagger composer require darkaonline/l5-swagger CI/CD に組み込む # .github/workflows/api-sync.yml - name: Generate API client run: npx orval - name: Type check run: npx tsc --noEmit © MOSH, Inc.
  36. CI/CD パイプライン ─ スキーマ変更を型で守る なし あり 📝 PHP変更 📄 スキーマ更新

    ⚙ Orval⽣成 🔍 型チェック 不整合? ✅ Pass ❌ Fail スキーマ変更 → Orval 再生成 → 型チェック → 不整合があればCI 失敗 PHP のコードを書くだけで、フロントとの契約が 自動的に守られる © MOSH, Inc.
  37. 共通化の効果 Before After (共通化後) API 変更時の修正箇所: 2+ 1 (スキーマ更新 →

    自動生成) 型不整合バグ: 月1-2 件 0 件 新エンドポイント追加: 2 時間 15 分(生成コマンド実行のみ) テスト: 2 系統 1 系統 「Native で直し忘れた」: 複数回 構造的に発生しない 少ない初期投資で、毎日の開発効率が上がり続ける © MOSH, Inc.
  38. まとめ ─ 後からNative が来ても破綻しないために バックエンドAPI 設計で先にやっておくこと Bearer Token 認証 /

    べき等性キー / cursor-based ページネーション machine-readable エラーコード / ISO 8601 UTC 日時 OpenAPI スキーマ管理 + CI 連携 共通化戦略 Lv.2 (型+fetcher 共通) から始める → 既存Web を壊さない customFetch で認証差分を吸収 © MOSH, Inc.
  39. 導入ステップ 1. バックエンドでOpenAPI スキーマを整備 2. Orval でプラットフォーム別にクライアント生成 3. customFetch で認証差分を吸収

    4. CI でスキーマ→型チェックのパイプラインを組む Native が来る 前 にステップ1 を済ませておくのが最もコストが低い © MOSH, Inc.