Slide 1

Slide 1 text

ネイティブアプリとWeb フロントエンドの API 通信ラッパーにおける共通化の勘所 PHPerKaigi 2026 [ 2026.03.21 ] MOSH develops and operates a platform that supports independent creators in selling their services online. © MOSH, Inc.

Slide 2

Slide 2 text

© MOSH, Inc. 大木 優 MOSH 株式会社 エンジニア LaravelLiveJapan コアスタッフ React Router v7 (Web) React Native / Expo (Mobile) Hono (PHP がスタックから消えちゃった悲しい) @suguru_ohki    @SuguruOoki 発表に入りきらなかったところはBlog へ

Slide 3

Slide 3 text

突然の宣伝!!! © MOSH, Inc.

Slide 4

Slide 4 text

© MOSH, Inc.

Slide 5

Slide 5 text

前提 Web アプリケーションがすでにあって運用されている。後からNative アプリを追加する TypeScript のMonoRepo アプリケーション © MOSH, Inc.

Slide 6

Slide 6 text

今日持ち帰ってほしいこと 後からNative アプリが追加されたとき、何を考慮すべきか? 1. Web とNative の 違い を知り、API 設計で地雷を踏まない 2. 実際に踏んだ地雷と、その 具体的な痛み を追体験する 3. 痛みを解消した 共通化戦略とツール選定 のたたき台を得る © MOSH, Inc.

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

よくある悲劇 ─ 仕様変更コスト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.

Slide 9

Slide 9 text

「WebView でよくない?」── 不採用の理由 WebView で最初にぶつかる壁 Push 通知: WebView からネイティブのPush 通知を制御するには、結局ネイティブコードが必要 決済フロー: App Store/Google Play のガイドライン上、WebView 内課金はグレーゾーン。審査リジ ェクトのリスクが高い カメラ・生体認証: WebView からFaceID/TouchID やカメラへのアクセスは制限が多く、ブリッジが 複雑化する © MOSH, Inc.

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Web vs Native の違い 違いを正しく理解して、API クライアントの設計に反映する © MOSH, Inc.

Slide 12

Slide 12 text

前提知識: 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.

Slide 13

Slide 13 text

認証方式の違い ─ なぜ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.

Slide 14

Slide 14 text

なぜXMLHttpRequest なら動くのか fetch とXHR の内部実装の違い fetch (Hermes) XMLHttpRequest 実行場所 JS エンジン内部 OS のネイティブネットワーク層 iOS Hermes のバグの影響を受ける NSURLSession にブリッジ → 影響なし タイムアウト 機構がない → 止まったら永遠に止まる OS 側に機構あり → 確実にエラーが返る → apisauce (axios 系)はXHR ベースなので、Hermes のfetch バグを回避できる © MOSH, Inc.

Slide 15

Slide 15 text

実際に採用したハイブリッド認証 Web Native セッション ブラウザがCookie を自動保存・自動付与 apisauce (XHR) + withCredentials: true で回避 API 認証 AWS SigV4 署名 X-Id-Token ヘッダで手動送信 トークン保存 ブラウザ管理 メモリキャッシュ + SecureStore リフレッシュ サーバーがCookie 更新 Cognito 直接呼び出しで再取得 結局Web/Native で認証コードが完全に分岐する → customFetch で吸収するしかない © MOSH, Inc.

Slide 16

Slide 16 text

データ取得パターンの違い(最重要) Web (React Router v7) Native (React Native) データ取得 loader/action (フレームワーク管理) TanStack Query / SWR (自前管理) SSR あり なし キャッシュ フレームワーク管理 自分で設計 レスポンスサイズ 1 リクエスト=1 ページ分もOK 軽量・段階取得が必須 この違いがあるから、hooks 以上の共通化は慎重になるべき © MOSH, Inc.

Slide 17

Slide 17 text

loader の恩恵と、Native で再現できない理由 loader がもたらすもの Loading 不要: 画面を見た瞬間にデータ表示済み SEO: クローラーが完成したHTML を読める 初期表示速度: JS 実行前にHTML にデータ → FCP が速い データ一貫性: サーバーで1 回取得 → race condition が起きにくい Native で再現できない理由 loader の恩恵 Native で使えない理由 SSR 統合 Native に SSR という概念自体がない Loading 不要 遷移前取得 → 画面遷移がブロック される データ一貫性 復帰時の 再取得はhooks でしか対応できない hooks を共通化 = loader の恩恵を捨てることになる © MOSH, Inc.

Slide 18

Slide 18 text

ここまでのまとめ 論点 結論 WebView でいい? Push 通知・決済・審査リジェクト → ネイティブで作る判断は正しい 認証は共通化できる? Cookie/ トークン管理が根本的に違う → customFetch で差分を吸収 データ取得は共通化できる? loader (SSR ) vs hooks (CSR )→ hooks まで共通化するとloader の恩恵を失う © MOSH, Inc.

Slide 19

Slide 19 text

実際に踏んだ地雷 違いを理解せず実装を始めた結果 © MOSH, Inc.

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

地雷2: Web 向けAPI レスポンスがモバイル回線で激重 Web 版の前提 1 画面に必要なデータを 1 リクエストで全部返す 設計 WiFi/ 有線前提でレスポンスサイズを気にしていなかった ページ単位のキャッシュでフレームワークが管理 Native で起きたこと 1 画面表示に大きなJSON を返すエンドポイントがそのまま使われた モバイル回線(3G/4G )では 表示が遅い 画面が頻発 オフセットベースのページネーション → 無限スクロールと相性最悪 Web 向けに設計されたAPI レスポンスは、Native のネットワーク環境では破綻する © MOSH, Inc.

Slide 22

Slide 22 text

地雷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.

Slide 23

Slide 23 text

地雷から学んだ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.

Slide 24

Slide 24 text

共通化戦略 どこまで揃えて、どこから分けるか © MOSH, Inc.

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

4 レベル比較マトリクス Lv.1 Lv.2 Lv.3 Lv.4 型の共有 ○ ○ ○ ○ fetcher 共有 × ○ ○ ○ エラーHD 共有 × × ○ ○ hooks 共有 × × × ○ 導入コスト 低 低〜中 中 高 メンテコスト 低 低 中 高 RRv7 loader 活用 ○ ○ ○ × 「既存Web を壊さず、Native を速く立ち上げる」ならLv.2 が最適解 © MOSH, Inc.

Slide 27

Slide 27 text

なぜ 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.

Slide 28

Slide 28 text

なぜ 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.

Slide 29

Slide 29 text

実践: 地雷を踏まないAPI 設計 3 層アーキテクチャとOrval による自動生成 © MOSH, Inc.

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

この設計の背景にあるパターン 腐敗防止層(Anti-Corruption Layer ) 外部の依存要素の変更から自身のコード領域を守る層 customFetch = プラットフォームの認証差分が、API 型定義に漏れ出さないための層 アダプターパターン(Ports & Adapters ) Layer 1 (型+fetcher )= Port: Web/Native 共通のインターフェース Layer 2 (customFetch )= Adapter: プラットフォームごとの実装を差し替え 契約駆動設計(Contract-First ) OpenAPI スキーマ = バックエンドとフロントエンドの契約 契約を先に定義し、実装はそこから自動生成する © MOSH, Inc.

Slide 32

Slide 32 text

参考になる発表 腐敗防止層・インターフェース設計 岡田正平(@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.

Slide 33

Slide 33 text

バックエンド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.

Slide 34

Slide 34 text

チェックリストを実行できない場合の対応 バックエンドに手を入れられない状況もある レガシーAPI で改修コストが高い / 別チームが管理している リリーススケジュールの都合でバックエンド改修が間に合わない そもそもAPI の仕様変更権限がない(外部API 、マイクロサービス境界) その場合はNative 側のcustomFetch で吸収する © MOSH, Inc.

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

チェックリスト解説 ─ レスポンス設計編 項目 Web だけなら Native が加わると cursor-based ページネーショ ン offset+limit で十分。ページ番号で 管理 無限スクロールが標準UX 。offset だとデータ重複・欠落が 発生する フィールド選択 WiFi 前提で全データ返しても問題 ない モバイル回線では致命的。必要なフィールドだけ返す設計 が必須 machine- readable エラーコード エラー画面に遷移すればいい Alert/Toast/ リトライ/ ログイン画面遷移をコードで出し分 けが必要 これらはすべて 後から対応すると既存Web にも影響が出る → 最初から入れておくのが最もコストが低い © MOSH, Inc.

Slide 38

Slide 38 text

OpenAPI + Orval: 同じスキーマからプラットフォーム別に生成 ✅ 共通 📱 Mobile 🌐 Web 📄 openapi.ym Orval: sw SWR Hooks Orval: fetc fetch関数 Zod MSW Moc © MOSH, Inc.

Slide 39

Slide 39 text

PoC はモノレポ、正式版はリポジトリ分離にした話 PoC フェーズ: モノレポで高速に検証 openapi.yml / Web / Native が同一リポジトリ → 変更が即反映 API 共通化の仕組みを検証するには最適 正式版: リポジトリを分離した理由 React Native が対応するReact バージョンが、React Router v7 よりも遅れがち モノレポだとNative 側のReact バージョンがWeb のアップグレードの 足枷 になる © MOSH, Inc.

Slide 40

Slide 40 text

リポジトリを分離しても共通化は維持できる OpenAPI スキーマは 別リポジトリでもSingle Source of Truth Orval 生成は各リポジトリで独立実行(入力のスキーマだけ共有) CI でスキーマバージョンの整合性をチェック 共通化 ≠ モノレポ必須。型の共通化はリポジトリ構成に依存しない © MOSH, Inc.

Slide 41

Slide 41 text

customFetch: Web 版 ─ SigV4 署名 + Cookie 認証 // packages/frontend/app/custom-fetch.ts(実際のコード) export const customFetch = async ( url: string, options: CustomFetchOptions, ): Promise => { // 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(response); }; Orval 生成コードからはこのcustomFetch が呼ばれる → 認証を意識しない © MOSH, Inc.

Slide 42

Slide 42 text

customFetch: Mobile 版 ─ ID Token + 401 リトライ // packages/mobile/src/clients/custom-fetch.ts(実際のコード) export const customFetch = async ( url: string, options: RequestInit = {}, ): Promise => { 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.

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

CI/CD パイプライン ─ スキーマ変更を型で守る なし あり 📝 PHP変更 📄 スキーマ更新 ⚙ Orval⽣成 🔍 型チェック 不整合? ✅ Pass ❌ Fail スキーマ変更 → Orval 再生成 → 型チェック → 不整合があればCI 失敗 PHP のコードを書くだけで、フロントとの契約が 自動的に守られる © MOSH, Inc.

Slide 46

Slide 46 text

Before / After 共通化への投資で何が変わったか © MOSH, Inc.

Slide 47

Slide 47 text

共通化の効果 Before After (共通化後) API 変更時の修正箇所: 2+ 1 (スキーマ更新 → 自動生成) 型不整合バグ: 月1-2 件 0 件 新エンドポイント追加: 2 時間 15 分(生成コマンド実行のみ) テスト: 2 系統 1 系統 「Native で直し忘れた」: 複数回 構造的に発生しない 少ない初期投資で、毎日の開発効率が上がり続ける © MOSH, Inc.

Slide 48

Slide 48 text

まとめ ─ 後からNative が来ても破綻しないために バックエンドAPI 設計で先にやっておくこと Bearer Token 認証 / べき等性キー / cursor-based ページネーション machine-readable エラーコード / ISO 8601 UTC 日時 OpenAPI スキーマ管理 + CI 連携 共通化戦略 Lv.2 (型+fetcher 共通) から始める → 既存Web を壊さない customFetch で認証差分を吸収 © MOSH, Inc.

Slide 49

Slide 49 text

導入ステップ 1. バックエンドでOpenAPI スキーマを整備 2. Orval でプラットフォーム別にクライアント生成 3. customFetch で認証差分を吸収 4. CI でスキーマ→型チェックのパイプラインを組む Native が来る 前 にステップ1 を済ませておくのが最もコストが低い © MOSH, Inc.

Slide 50

Slide 50 text

Thank You 変更コスト2 倍の世界から1 倍の世界へ OpenAPI を契約として、Orval で型を共通化し、 プラットフォーム差分は customFetch で吸収する。 大木 優 / @SuguruOoki MOSH 株式会社 © MOSH, Inc.