あなたのアプリケーションは準備ができています。あなたは、いくつかの魔法のことを行うバックエンドを持っており、その後、APIを通じていくつかのデータを露出します。あなたは、そのAPIを消費し、ユーザーにデータを表示するフロントエンドを持っています。あなたは、バックエンドへのリクエストを作成するためにFetch APIを使用しています、その後、応答を処理し、UIを更新します。シンプルでシンプルです。 さて、開発では、はい。その後、あなたはあなたのアプリを生産に展開します。そして奇妙なことが起こり始めます。ほとんどの場合、すべては順調に見えますが、時には、リクエストが失敗します。UIは壊れます。ユーザーは文句を言います。あなたは何が間違ったのか疑問に思います。 ネットワークは予測不可能で、あなたはそれに備えなければなりません。あなたはこれらの質問に答えを持っている方が良いでしょう:ネットワークが遅いまたは信頼性が低いときに何が起こりますか?バックエンドがダウンしているときに何が起こりますか、またはエラーを返しますか? あなたが外部のAPIを消費する場合、あなたがレート制限に到達してブロックされたときに何が起こりますか? あなたはこれらのシナリオを優雅に処理し、良いユーザー体験を提供しますか? 正直に言うと、バニラフェッチはこれらのシナリオを処理するのに十分ではありません。あなたは、エラー、リリース、タイムアウト、キャッシュなどを処理するために多くのボイラープレートコードを追加する必要があります。 この記事では、リハーサルリクエストをライブラリを使用して生産準備する方法について説明します。 わたしたちは: ffetch いくつかのエンドポイントでNode.jsとExpressでバックエンドを作成する Fetch API を使用して vanilla JavaScript でこれらのエンドポイントをアンケートするフロントエンドを構築する バックエンドをフラッキーにし、現実世界のシナリオをシミュレートする 検索リクエストがどのように失敗するか、およびこれらの失敗をどのように処理するかをご覧ください。 ffetch を導入して、fetch リクエストの処理を簡素化し、強化する The Boilerplate バックエンドは、RESTfulエンドポイントを露出し、ユーザーやタスクを作成、読み取り、更新し、削除し、ユーザーにタスクを割り当てます。 バックアップ バックエンドを構築するために、Node.js と Express を使用します. 私たちはまた、単純なメモリ内のデータストアを使用して物事をシンプルにします. Here are the user and task models: interface User { id: number; // Unique identifier name: string; // Full name email: string; // Email address } export interface Task { id: number; // Unique identifier title: string; // Short task title description?: string; // Optional detailed description priority: "low" | "medium" | "high"; // Task priority } 以下のエンドポイントを作成します。 GET /users: Get all users(ユーザIDのマレージを返します) POST /users: Create a new user (returns the created user's id) (新しいユーザを作成する) GET /users/:id:Get a user by id(ユーザーオブジェクトを返します) PUT /users/:id: ユーザーの更新 (成功メッセージを返します) DELETE /users/:id: ユーザを削除する (成功メッセージを返します) GET /tasks: Get all tasks (returns an array of task ids) (すべてのタスクを取得する) GET /tasks/:id: Get a task by id (タスクオブジェクトを返します) POST /tasks: Create a new task (returns the created task's id) (新しいタスクを作成する) PUT /tasks/:id: タスクを更新 (成功メッセージを返します) DELETE /tasks/:id: タスクを削除する GET /users/:userId/tasks: ユーザーに割り当てられたすべてのタスクを取得します(タスクIDのマレージを返します) POST /users/:userId/tasks/:taskId:タスクをユーザーに割り当てる(成功メッセージを返します) DELETE /users/:userId/tasks/:taskId: ユーザーからタスクを削除する (成功メッセージを返します) 前線 フレームワークは通常、独自の抽象とやり方を追加するので、物事をシンプルにし、フレームワーク以外のものにするためにヴァニラタイプスクリプトを使用します。我々は2つのビューを持つSPAを作成します:ユーザリストのための1つと特定のユーザのための1つ。ユーザリストはユーザの名前とそれに割り当てられたタスクの数を表示します。ユーザをクリックすると、ユーザの詳細とそのタスクを表示します。 To keep things simple, we will use polling to get the latest data from the backend. Every 3 seconds, we will make requests to the backend to get the latest data for the current view and update the UI accordingly. For the userlist view, we will make a request to すべてのユーザIDを取得するには、それぞれのユーザに対して、私たちはリクエストを行います。 詳細を調べるために、そして、 to calculate the number of tasks assigned to them. GET /users GET /users/:id GET /users/:id/tasks ユーザビューのために、我々は要請を提出します。 ユーザーの詳細を確認し、そして、 次に、それぞれのタスク ID に対して、我々 は要求を提出します。 タスクの詳細を検索する GET /users/:id GET /users/:id/tasks GET /tasks/:id The GitHub Repo You can find the complete code for this example in the accompanying . GitHub レポ ボイラープレートの量のために、完全なコードのためのレポを参照してください. 記事の各段階はレポの支店を参照します。 repo には、バックエンドとフロントエンドの両方のコードが含まれています。 folder, and the frontend is in the repo をクローンすると、Run 両方のフォルダに依存性をインストールします。その後、バックエンドを実行できます。 in the フロントエンドと、Frontend with IN THE タイトル: The frontend will be served at 後ろ向きは, . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 あなたがすべての仕事を完了し、あなたのバックエンドとフロントエンドの両方が実行されると、あなたはあなたのブラウザを開いて行くことができます。 このアプリをアクションで見るには: http://localhost:5173 開発中 もしあなたが航海するなら , you should see everything working just fine. If you add a new user with http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' ユーザーリストの表示にユーザーが表示されるのは3秒以内に表示されるべきです。アプリで遊ぶことと、より多くのユーザーやタスクを追加することを自由に感じてください。 Well, this is where we finally arrive at the point of this article. Our backend works just fine. Our frontend, despite the horrible boilerplate, also works just fine. But between the frontend and backend, there is the network. And the network is unreliable. So, let's see what happens if we add a bit of flakiness to our backend. Simulating Network Errors 私たちのバックエンドに、リクエストを20%の確率でランダムに失敗させるミドルウェアを追加し、最大1秒間のランダム遅延も追加します。 You can find the flaky middleware in the file. Here is the code: backend/src/middleware/flaky.ts import { Request, Response, NextFunction } from 'express'; export function flaky(req: Request, res: Response, next: NextFunction) { // Randomly fail requests with a 20% chance if (Math.random() < 0.2) { return res.status(500).json({ error: 'Random failure' }); } // Add random delay up to 2 seconds const delay = Math.random() * 2000; setTimeout(next, delay); } Then, we can use this middleware in our Express app. You can find the code in the file. Just import the middleware and use it before your routes. ただ、ミドルウェアをインポートし、あなたのルートの前に使用してください。 backend/src/index.ts ... import { flaky } from './middleware/flaky'; ... app.use(cors()); app.use(express.json()); app.use(flaky); // Use the flaky middleware このコードは、The repoの支店ですので、ご覧になれます。 . network-errors git checkout network-errors あなたがバックエンドを再起動し、フロントエンドをリフレッシュしたら、あなたはいくつかの奇妙なものを見始めるべきです コンソールはエラーでいっぱいになります UIのいくつかのフィールドは 物事は崩壊し始める、そしてこれは、あなたがすでに持っていない場合、あなたはこれらのエラーを優雅に扱う方法について考え始める必要があります。 undefined エラーシナリオ まず、何が間違っているのか、そしてどのように対処できるのかを特定しましょう。 Intermittent network failures: requests can fail randomly, so on certain errors, we need to retry them a few times before giving up. リクエストはランダムに失敗する可能性があります。 投票時には、1 つの要求だけではなく、複数の要求を同期的に送信します。 3 秒後には、別の要求のバッチを送信します。 次の要求が送信されたときに前回の要求がまだ待機している場合、後回の要求の後に早期の回答を得る可能性があります。 これは不一致な UI 状態につながります。 最新の回答のみが UI を更新するために使用されることを確認する必要がありますので、新しい投票サイクルが始まると、前回の要求をキャンセルする必要があります。 同様に、ユーザーが前回のビューからのリクエストがまだ待機している間に別のビューに移動する場合、以前のビューのリクエストがすでに移動した後にもリクエストが得られる可能性があります。これも不一致なUI状態につながります。 ある時点でリクエストが成功したが、次回のアンケートサイクルで失敗した場合、ユーザーにエラー状態を直ちに表示したくない。 私たちは、バックエンドで削除されたユーザーを見ている場合のシナリオを処理する必要があります 404 エラーを優雅に処理し、ユーザー リスト ビューに戻り、または少なくとも見つからないメッセージを表示する必要があります。 Also, we need to handle scenarios where the backend is completely down or unreachable. We need to show a global error message to the user and maybe retry the requests after some time. そして、特にUIがデータの作成、更新、または削除を許可している場合は、リストは続きますが、今では、読み取り操作やデータを取得するときにエラーを処理する方法に焦点を当てましょう。 バニラフェッチ(Vanilla Fetch) ここでは、JavaScript (またはTypeScript) で多くのものと同様に、これらのシナリオを処理するための 2 つのオプションがあります. You can write your own utility functions to wrap the Fetch API and add the necessary error handling logic, or you can choose a library that does this for you. まずは、自分自身でコードを設定することにしましょう。 repoの支店ですので、ご覧になれます。 . native-fetch git checkout native-fetch 何をしなければならないか すべての fetch 論理を poller.ts に集中する。 各アンケートでは、新しい AbortController を作成し、以前のアンケートをキャンセルします。 Wrap fetch calls in a retry-and-timeout function をダウンロードします。 成功すると、キャッシュを更新し、レンダリングに使用します。 失敗の場合、必要に応じて再起動し、タイムアウト/キャンセルを優雅に処理してください。 わたしたち 現在のファイルはこんな感じです: poller.ts // Cache for responses const cache: Record<string, any> = {}; // AbortController for cancelling requests let currentController: AbortController | undefined; // Helper: fetch with retries and timeout async function fetchWithRetry(url: string, options: RequestInit = {}, retries = 2, timeout = 3000): Promise<any> { for (let attempt = 0; attempt <= retries; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); return data; } catch (err) { clearTimeout(timer); if (attempt === retries) throw err; } } } // Cancel all previous requests function cancelRequests() { if (currentController) currentController.abort(); currentController = new AbortController(); } export async function fetchUserListData() { cancelRequests(); // Use cache if available if (cache.userList) return cache.userList; try { if (!currentController) throw new Error('AbortController not initialized'); const userIds = await fetchWithRetry('http://localhost:3000/users', { signal: currentController!.signal }); const users = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}`, { signal: currentController!.signal }))); const taskCounts = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}/tasks`, { signal: currentController!.signal }).then((tasks: any[]) => tasks.length))); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { // fallback to cache if available if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { if (!currentController) throw new Error('AbortController not initialized'); const user = await fetchWithRetry(`http://localhost:3000/users/${userId}`, { signal: currentController!.signal }); const taskIds = await fetchWithRetry(`http://localhost:3000/users/${userId}/tasks`, { signal: currentController!.signal }); const tasks = await Promise.all(taskIds.map((id: number) => fetchWithRetry(`http://localhost:3000/tasks/${id}`, { signal: currentController!.signal }))); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } 削除した All fetch logic is now in 私たちのシンプルな使用ケースでは、それは持続可能ですが、ここでさえ、エラー、リトリ、タイムアウト、キャンセル、キャッシュを処理するために多くのボイラープレートコードを追加する必要がありました。 api.ts poller.ts アプリを現在実行している場合は、より良い機能を有していることがわかります. UI はより安定しており、頻繁に壊れることはありません. あなたはまだコンソールでいくつかのエラーを見ることができますが、それらは優雅に処理され、ユーザー体験にあまり影響を与えません. このアプローチの欠点 More boilerplate code: We had to write a lot of code to handle errors, retries, timeouts, cancellations, and caching. This can quickly become messy and hard to maintain. 多くのコードを書かなければなりませんでした。 あまり再利用できない:コードは私たちの特定の使用例に密接に結びつき、他のプロジェクトやシナリオではあまり再利用できない。 Limited features: The code only handles basic error scenarios. More complex scenarios like exponential backoff, circuit breakers, or global error handling would require even more code. 利用 for Better Fetch Handling ffetch ffetch To address the downsides of our custom fetch handling, I wrote a library called . It is a small and lightweight library that wraps the Fetch API and provides a simple and declarative way to handle errors, retries, timeouts, cancellations, and some more features. ffetch Let's rewrite our fetch logic using . you can find the code on the repoの支店ですので、ご覧になれます。 . ffetch ffetch git checkout ffetch まず、インストール in the folder: ffetch frontend npm install @gkoos/ffetch わたしたちは、わたしたちの書き直しができる。 ファイル使用 : poller.ts ffetch import createClient from '@gkoos/ffetch'; // Cache for responses const cache: Record<string, any> = {}; // Create ffetch client const api = createClient({ timeout: 3000, retries: 2, }); function cancelRequests() { api.abortAll(); } export async function fetchUserListData() { cancelRequests(); if (cache.userList) return cache.userList; try { const userIds = await api('http://localhost:3000/users').then(r => r.json()); const users = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}`).then(r => r.json())) ); const taskCounts = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}/tasks`).then(r => r.json()).then((tasks: any[]) => tasks.length)) ); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { const user = await api(`http://localhost:3000/users/${userId}`).then(r => r.json()); const taskIds = await api(`http://localhost:3000/users/${userId}/tasks`).then(r => r.json()); const tasks = await Promise.all( taskIds.map((id: number) => api(`http://localhost:3000/tasks/${id}`).then(r => r.json())) ); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } コードははるかにクリーンで読みやすくなります. We don't have to worry about retries, timeouts, or cancellations anymore. takes care of that for us. We just create a client with the desired options and use it to make requests. ffetch 使用の他の利点 ffetch 回路ブレーカー: 繰り返しの故障後の自動エンドポイントクールドダウン Automatic exponential backoff for retries: increasing waiting times between retries (リトリオの自動指数バックオフ:リトリオの間の待機時間を増やす) グローバルエラー処理:ログ化、リクエスト/応答を変更するためのハック等 たとえば、ネットワークエラーや5xxサーバーエラーをリトリすることもできますが、4xxクライアントエラーをリトリすることもできません。 doesn't do anything magical you couldn't build yourself, but it saves you from writing, testing, and maintaining all that boilerplate. It's a convenience wrapper that bakes in production-grade patterns (like circuit breaker and backoff) so you can focus on your app, not your fetch logic. It also stops at the fetch layer, so you can still use your own caching, state management, and UI libraries as you see fit. ffetch 結論 この記事の主な取り出しは、あなたが使用するべきではないということです。 特に、あなたは生産準備アプリケーションのためにバニラフェッチに頼るべきではありません。ネットワークは信頼できない、そしてあなたはそれを準備する必要があります。あなたは、エラー、リチャージ、タイムアウト、キャンセル、およびキャッシュを優雅に処理する必要があります、良いユーザー体験を提供する。 ffetch What you exactly need to do depends on your specific use case and requirements, but you can't go to production handling the happy path only. Things can and will go wrong, and your app needs to handle at least the most common failure scenarios. And can help with that. ffetch