fetch APIの基本
JavaScriptでAPIリクエストを送るためのfetch APIについて学んでいきましょう。前回Thunder Clientで体験したAPIリクエストを、今度はコードで実装してみます。
この記事で学べること
- fetch APIの基本構文と使い方
- PromiseとAsync/Awaitの関係
- GETリクエストの実装方法
- POSTリクエストの実装方法
- エラーハンドリングの基本パターン
- AbortControllerによるキャンセル処理
fetch APIとは
fetchは、ブラウザに標準で備わっているAPIリクエスト用の関数です。XMLHttpRequest(XHR)の後継として登場し、現在のWeb開発ではこちらが主流です。Node.js 18以降でもネイティブサポートされているので、サーバーサイドでも同じ書き方で使えます(便利ですね)。
最小構成
fetch("https://jsonplaceholder.typicode.com/posts/1");
たったこれだけでHTTPリクエストが送れます。ただし、このままでは結果を受け取れません。fetchはPromiseを返すので、適切に処理する必要があります。
Promiseとasync/await
fetchを使いこなすには、JavaScriptの非同期処理を理解しておく必要があります。
Promiseとは
Promiseは「将来の結果を約束するオブジェクト」です。APIリクエストのように時間がかかる処理の結果を扱うために使います。
// fetchはPromiseを返す
const promise = fetch("https://jsonplaceholder.typicode.com/posts/1");
console.log(promise); // Promise { <pending> }
.then()で結果を受け取る
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => {
console.log("レスポンスを受け取りました");
return response.json(); // これもPromiseを返す
})
.then((data) => {
console.log(data.title);
});
.then()をチェーンでつなげて、順番に処理を書けます。ただ、ネストが深くなると読みにくくなりがちです。
async/awaitで読みやすく
async/awaitを使うと、非同期コードを同期的な見た目で書けます。
async function getPost() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await response.json();
console.log(data.title);
}
getPost();
awaitは「Promiseの結果が返ってくるまで待つ」という意味です。awaitを使うには、関数にasyncキーワードを付ける必要があります。
Note: この記事では主に
async/await形式で解説します。現代のJavaScript開発ではこちらが主流です。
GETリクエスト
データを取得する基本パターンを見ていきましょう。
基本形
async function fetchUser(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
);
const user = await response.json();
return user;
}
// 使用例
const user = await fetchUser(1);
console.log(user.name); // "Leanne Graham"
ヘッダーを付ける
APIによっては、リクエストヘッダーの指定が必要です。
async function fetchWithHeaders(url) {
const response = await fetch(url, {
method: "GET", // GETの場合は省略可能
headers: {
Accept: "application/json",
"X-Custom-Header": "value",
},
});
return response.json();
}
クエリパラメータの扱い
URLにパラメータを付けるにはURLSearchParamsが便利です。
async function searchUsers(query, limit = 10) {
const params = new URLSearchParams({
q: query,
_limit: limit.toString(),
});
const url = `https://jsonplaceholder.typicode.com/users?${params}`;
const response = await fetch(url);
return response.json();
}
URLSearchParamsはエンコードも自動でやってくれます(日本語なども安全に扱えます)。
POSTリクエスト
データを送信する場合はPOSTメソッドを使います。
基本形
async function createPost(post) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(post),
});
const created = await response.json();
return created;
}
// 使用例
const newPost = await createPost({
title: "Hello World",
body: "This is my first post",
userId: 1,
});
console.log(newPost.id); // 101(サーバーが割り当てたID)
重要なポイント
method: "POST"を指定するContent-Type: application/jsonヘッダーを付けるJSON.stringify()でオブジェクトを文字列に変換する
JSON.stringify()を忘れると、サーバーには[object Object]という文字列が送られてしまいます(よくあるミスです)。
PUT/PATCH/DELETEも同様
// PUT: リソース全体を置き換え
async function updatePost(id, post) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(post),
},
);
return response.json();
}
// DELETE: リソースを削除
async function deletePost(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
{
method: "DELETE",
},
);
return response.ok; // 成功ならtrue
}
エラーハンドリング
fetchのエラー処理には注意点があります。
fetchは404でも例外を投げない
const response = await fetch("https://jsonplaceholder.typicode.com/posts/9999");
console.log(response.ok); // false
console.log(response.status); // 404
// でも、例外は発生していない!
fetchが例外を投げるのは、ネットワークエラー(サーバーに到達できない場合)だけです。HTTP 404や500などのエラーレスポンスは、正常なレスポンスとして返ってきます。
response.okを確認する
async function fetchPost(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
response.okは、ステータスコードが200-299の範囲ならtrueになります。
try-catchで包む
async function safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
// ネットワークエラー or 上で投げたエラー
console.error("Fetch failed:", error.message);
throw error; // 再度投げるか、代替値を返す
}
}
エラーレスポンスのボディを読む
APIによっては、エラー時にJSON形式で詳細を返すものもあります。
async function fetchWithErrorDetail(url) {
const response = await fetch(url);
if (!response.ok) {
// エラーレスポンスのボディを読む
let errorMessage = `HTTP ${response.status}`;
try {
const errorBody = await response.json();
errorMessage = errorBody.message || errorMessage;
} catch {
// JSONでない場合は無視
}
throw new Error(errorMessage);
}
return response.json();
}
AbortControllerによるキャンセル
リクエストを途中でキャンセルしたいケースがあります(画面遷移時など)。
基本的な使い方
const controller = new AbortController();
const { signal } = controller;
// 3秒後にキャンセル
setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1", {
signal,
});
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === "AbortError") {
console.log("リクエストがキャンセルされました");
} else {
throw error;
}
}
タイムアウトの実装
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
Note: Reactでは、コンポーネントのアンマウント時にリクエストをキャンセルするのがベストプラクティスです。次回の「useEffectによる非同期処理」で詳しく扱います。
TypeScriptでの型付け
TypeScriptを使う場合、レスポンスの型を定義しておくと安全です。
type User = {
id: number;
name: string;
email: string;
};
async function fetchUser(id: number): Promise<User> {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user: User = await response.json();
return user;
}
response.json()の戻り値はanyなので、型を明示的に付けることで、以降のコードで型チェックが効きます(タイプミスに気づけて助かります)。
やってみよう!
- ブラウザの開発者ツール(Console)で以下を実行してみましょう
// GETリクエスト
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await res.json();
console.log("ユーザー数:", users.length);
- POSTリクエストを試してみましょう
// POSTリクエスト
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "テスト", body: "本文", userId: 1 }),
});
const created = await res.json();
console.log("作成されたID:", created.id);
- エラーハンドリングを確認
// 存在しないエンドポイント
const res = await fetch("https://jsonplaceholder.typicode.com/invalid");
console.log("ok:", res.ok); // false
console.log("status:", res.status); // 404
- タイムアウトを体験
const controller = new AbortController();
setTimeout(() => controller.abort(), 100); // 100msでキャンセル
try {
await fetch("https://jsonplaceholder.typicode.com/posts", {
signal: controller.signal,
});
} catch (e) {
console.log("エラー種別:", e.name); // "AbortError"
}
ポイント(まとめ)
- fetch: ブラウザ標準のHTTPリクエスト関数。Promiseを返す
- async/await: 非同期処理を同期的な見た目で書ける構文
- response.json(): レスポンスボディをJSONとしてパース
- response.ok: ステータスコードが200-299ならtrue
- JSON.stringify(): POSTで送るデータをJSON文字列に変換
- AbortController: リクエストのキャンセルに使う
- fetchは404で例外を投げない: 必ず
response.okをチェックする
参考リンク
- MDN: fetch API https://developer.mozilla.org/ja/docs/Web/API/Fetch_API
- MDN: async/await https://developer.mozilla.org/ja/docs/Learn_web_development/Extensions/Async_JS/Promises
- MDN: AbortController https://developer.mozilla.org/ja/docs/Web/API/AbortController
- JSONPlaceholder https://jsonplaceholder.typicode.com/