fetch APIとは?JavaScriptでPOSTリクエストを送る基本技術
JavaScript fetch APIを使ったPOSTリクエストの基本的な送信方法から実践的な活用例まで詳しく解説。フォームデータ送信、ファイルアップロード、エラーハンドリングの実装方法を初心者向けに分かりやすく説明します。
fetch APIとは?JavaScriptでPOSTリクエストを送る基本技術
みなさん、JavaScriptでサーバーにデータを送信したいと思ったことありませんか?
「フォームに入力したデータをサーバーに保存したい」 「画像をアップロードする機能を作りたい」 「APIにデータを送って処理結果を受け取りたい」
こんな場面に遭遇することがあるかもしれませんね。
JavaScriptのfetch APIは、サーバーとのHTTP通信を行うための現代的で強力なツールです。 この記事では、fetch APIを使ったPOSTリクエストについて基本的な使い方から実践的な活用方法まで詳しく解説します。
フォームデータの送信、ファイルアップロード、エラーハンドリングの実装方法を、実際のコード例を交えて初心者向けに分かりやすく説明していきます。
fetch APIって何だろう?
簡単に言うとどんなもの?
fetch APIは、サーバーとデータをやり取りするためのJavaScript機能です。
簡単に言うと、Webページからサーバーに「こんなデータを送るよ」とか「あのデータをちょうだい」とやり取りするためのツールなんです。 従来のXMLHttpRequestという古い方法に代わって、もっと使いやすく設計された新しい機能です。
// fetch APIの基本的な形fetch('/api/endpoint', { method: 'POST', body: '送りたいデータ'});
このコードで、サーバーにデータを送ることができます。
POSTリクエストとは?
POSTリクエストは、サーバーにデータを送信するためのHTTPメソッドです。
HTTPには色々な「お願いの仕方」があります。 その中でも、POSTは「このデータを受け取って処理してください」という意味を持っています。
// POSTの基本的な使い道の例fetch('/api/users', { method: 'POST', // 「送信します」という意味 body: userData // 送りたいデータ});
フォーム送信、ファイルアップロード、チャット投稿など、様々な場面で使われます。
なぜfetch APIが選ばれるの?
fetch APIが人気な理由をいくつか見てみましょう。
// 従来の方法(XMLHttpRequest)- 複雑let xhr = new XMLHttpRequest();xhr.open('POST', '/api/data');xhr.setRequestHeader('Content-Type', 'application/json');xhr.onload = function() { if (xhr.status === 200) { console.log(xhr.responseText); }};xhr.send(JSON.stringify(data));
// 新しい方法(fetch API)- シンプルfetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)}).then(response => response.json()).then(data => console.log(data));
fetch APIの方が、ずっと読みやすくて理解しやすいですよね。
基本的なPOSTリクエストをマスターしよう
最もシンプルなPOSTリクエスト
まずは、一番基本的なPOSTリクエストから始めてみましょう。
// 基本的なPOSTリクエストの例async function sendBasicData() { try { let response = await fetch('/api/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'こんにちは!', sender: 'ユーザー1' }) }); let result = await response.json(); console.log('送信成功:', result); } catch (error) { console.error('送信エラー:', error); }}
このコードの各部分を詳しく見てみましょう。
まず、関数の宣言部分です。
async function sendBasicData() {
async
をつけることで、この関数の中でawait
が使えるようになります。
これにより、サーバーからの応答を待ちやすくなります。
次に、fetch関数の呼び出し部分です。
let response = await fetch('/api/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'こんにちは!', sender: 'ユーザー1' })});
fetch
の第一引数は送信先のURL、第二引数は送信の設定です。
method: 'POST'
でPOSTリクエストを指定し、body
に送信するデータを入れています。
fetchの設定オプションを理解しよう
fetchで使える主要な設定オプションを見てみましょう。
// fetch設定オプションの詳細例let options = { method: 'POST', // HTTPメソッド headers: { // リクエストヘッダー 'Content-Type': 'application/json', 'Authorization': 'Bearer token123', 'X-Custom-Header': '独自ヘッダー' }, body: JSON.stringify(data), // 送信データ credentials: 'same-origin', // 認証情報の扱い cache: 'no-cache', // キャッシュ制御 redirect: 'follow' // リダイレクト処理};
let response = await fetch('/api/endpoint', options);
それぞれの役割を説明しますね。
method
は送信方法を指定します(GET、POST、PUT、DELETEなど)。
headers
はサーバーに追加情報を伝えるためのものです。
body
は実際に送信するデータの内容です。
エラーハンドリングの基本
サーバーとの通信では、エラーが発生する可能性があります。 適切なエラー処理を組み込みましょう。
async function sendDataWithErrorHandling() { try { let response = await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '田中太郎', email: 'tanaka@example.com' }) }); // レスポンスの状態をチェック if (!response.ok) { throw new Error(`HTTPエラー: ${response.status}`); } let result = await response.json(); console.log('送信完了:', result); return result; } catch (error) { console.error('エラーが発生しました:', error); alert('データの送信に失敗しました'); throw error; }}
response.ok
でリクエストが成功したかチェックできます。
失敗した場合は、適切なエラーメッセージを表示して、ユーザーに状況を伝えましょう。
JSONデータを送信してみよう
オブジェクトをJSONで送信する
最も一般的なデータ送信方法は、JavaScriptオブジェクトをJSONに変換して送ることです。
async function sendUserProfile() { // 送信するユーザーデータ let userProfile = { name: '佐藤花子', age: 28, email: 'sato.hanako@example.com', hobbies: ['読書', '映画鑑賞', 'プログラミング'], settings: { language: 'ja', theme: 'dark', notifications: true } }; try { let response = await fetch('/api/users/profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userProfile) }); if (response.ok) { let result = await response.json(); console.log('プロフィール更新成功:', result); alert('プロフィールが更新されました!'); } else { console.error('更新失敗:', response.status); alert('プロフィールの更新に失敗しました'); } } catch (error) { console.error('ネットワークエラー:', error); alert('通信エラーが発生しました'); }}
JSON.stringify()
でJavaScriptオブジェクトを文字列に変換しています。
この変換により、サーバーが理解できる形式でデータを送信できます。
配列データの送信
複数のアイテムを一度に送信する場合の例です。
async function sendShoppingCart() { // ショッピングカートのデータ let cartItems = [ { id: 1, name: 'ノートPC', price: 100000, quantity: 1 }, { id: 2, name: 'マウス', price: 2000, quantity: 2 }, { id: 3, name: 'キーボード', price: 8000, quantity: 1 } ]; // 合計金額を計算 let totalAmount = cartItems.reduce((sum, item) => { return sum + (item.price * item.quantity); }, 0); let orderData = { items: cartItems, totalAmount: totalAmount, customerInfo: { id: 'user123', name: '山田太郎' }, orderDate: new Date().toISOString() }; try { let response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(orderData) }); let result = await response.json(); if (response.ok) { console.log('注文完了:', result); alert(`注文が完了しました!注文番号: ${result.orderId}`); } else { console.error('注文失敗:', result.message); alert('注文の処理に失敗しました'); } } catch (error) { console.error('注文エラー:', error); alert('注文の送信に失敗しました'); }}
配列データも、オブジェクトと同じようにJSON形式で送信できます。 サーバー側で処理しやすいよう、適切な構造でデータを整理することが大切です。
動的なデータ生成と送信
ユーザーの入力に基づいて動的にデータを作成し、送信する例です。
function createDynamicForm() { // フォームデータを動的に収集 function collectFormData() { let formData = {}; // テキスト入力の収集 let textInputs = document.querySelectorAll('input[type="text"]'); textInputs.forEach(input => { formData[input.name] = input.value; }); // チェックボックスの収集 let checkboxes = document.querySelectorAll('input[type="checkbox"]'); let selectedOptions = []; checkboxes.forEach(checkbox => { if (checkbox.checked) { selectedOptions.push(checkbox.value); } }); formData.selectedOptions = selectedOptions; // セレクトボックスの収集 let selects = document.querySelectorAll('select'); selects.forEach(select => { formData[select.name] = select.value; }); return formData; } // データを送信 async function submitDynamicData() { let collectedData = collectFormData(); // データの検証 if (!collectedData.name || collectedData.name.trim() === '') { alert('名前を入力してください'); return; } // タイムスタンプを追加 collectedData.submittedAt = new Date().toISOString(); collectedData.userAgent = navigator.userAgent; try { let response = await fetch('/api/dynamic-form', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(collectedData) }); let result = await response.json(); if (response.ok) { console.log('フォーム送信成功:', result); alert('フォームが正常に送信されました!'); // フォームをリセット document.querySelector('form').reset(); } else { console.error('送信失敗:', result); alert('フォームの送信に失敗しました'); } } catch (error) { console.error('送信エラー:', error); alert('通信エラーが発生しました'); } } return { collectFormData, submitDynamicData };}
// 使用例let dynamicForm = createDynamicForm();
// フォーム送信ボタンのイベント設定document.getElementById('submit-btn').addEventListener('click', function(event) { event.preventDefault(); dynamicForm.submitDynamicData();});
この方法なら、フォームの内容が変わっても柔軟に対応できます。
フォームデータを送信してみよう
HTMLフォームからのデータ送信
実際のHTMLフォームからデータを取得して送信する方法を見てみましょう。
<form id="contact-form"> <div class="form-group"> <label for="name">お名前:</label> <input type="text" id="name" name="name" required> </div> <div class="form-group"> <label for="email">メールアドレス:</label> <input type="email" id="email" name="email" required> </div> <div class="form-group"> <label for="subject">件名:</label> <input type="text" id="subject" name="subject" required> </div> <div class="form-group"> <label for="message">メッセージ:</label> <textarea id="message" name="message" rows="5" required></textarea> </div> <div class="form-group"> <label for="category">お問い合わせ種類:</label> <select id="category" name="category"> <option value="general">一般的な質問</option> <option value="support">サポート</option> <option value="business">営業について</option> </select> </div> <button type="submit">送信する</button></form>
このフォームに対応するJavaScriptです。
document.getElementById('contact-form').addEventListener('submit', async function(event) { event.preventDefault(); // FormDataオブジェクトを作成 let formData = new FormData(this); // FormDataをオブジェクトに変換 let formObject = {}; formData.forEach((value, key) => { formObject[key] = value; }); // 送信前のバリデーション if (!validateForm(formObject)) { return; } // 送信処理 try { showLoading(true); let response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formObject) }); let result = await response.json(); if (response.ok) { alert('お問い合わせを送信しました!'); this.reset(); // フォームをリセット console.log('送信成功:', result); } else { alert('送信に失敗しました: ' + result.message); } } catch (error) { console.error('送信エラー:', error); alert('ネットワークエラーが発生しました'); } finally { showLoading(false); }});
// バリデーション関数function validateForm(data) { if (!data.name || data.name.trim().length < 2) { alert('名前は2文字以上で入力してください'); return false; } if (!data.email || !isValidEmail(data.email)) { alert('正しいメールアドレスを入力してください'); return false; } if (!data.message || data.message.trim().length < 10) { alert('メッセージは10文字以上で入力してください'); return false; } return true;}
// メールアドレスの形式チェックfunction isValidEmail(email) { let emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailPattern.test(email);}
// ローディング表示の制御function showLoading(show) { let submitButton = document.querySelector('#contact-form button[type="submit"]'); if (show) { submitButton.disabled = true; submitButton.textContent = '送信中...'; } else { submitButton.disabled = false; submitButton.textContent = '送信する'; }}
フォームの送信時にevent.preventDefault()
を使って、通常のフォーム送信を止めています。
その後、JavaScriptでデータを収集し、fetch APIで送信しています。
FormDataオブジェクトの直接使用
FormDataオブジェクトをそのまま送信する方法もあります。
async function sendFormDataDirectly() { // FormDataを直接作成 let formData = new FormData(); formData.append('name', '鈴木一郎'); formData.append('email', 'suzuki@example.com'); formData.append('age', '35'); formData.append('department', '開発部'); formData.append('joinDate', '2020-04-01'); // 複数の値を同じキーで追加 formData.append('skills', 'JavaScript'); formData.append('skills', 'Python'); formData.append('skills', 'React'); try { let response = await fetch('/api/employees', { method: 'POST', // FormDataの場合、Content-Typeは自動設定される body: formData }); let result = await response.json(); if (response.ok) { console.log('社員登録成功:', result); alert('社員情報が登録されました'); } else { console.error('登録失敗:', result); alert('登録に失敗しました'); } } catch (error) { console.error('登録エラー:', error); alert('通信エラーが発生しました'); }}
FormDataを使う場合は、Content-Type
ヘッダーを手動で設定する必要がありません。
ブラウザが自動的に適切な形式で送信してくれます。
条件に応じたデータ送信
ユーザーの選択や状況に応じて、送信するデータを変える例です。
class ConditionalFormSubmitter { constructor() { this.userType = 'guest'; this.setupEventListeners(); } setupEventListeners() { // ユーザータイプの変更を監視 document.getElementById('user-type').addEventListener('change', (e) => { this.userType = e.target.value; this.updateFormFields(); }); // フォーム送信の処理 document.getElementById('dynamic-form').addEventListener('submit', (e) => { e.preventDefault(); this.submitForm(); }); } updateFormFields() { let additionalFields = document.getElementById('additional-fields'); // ユーザータイプに応じてフィールドを表示/非表示 if (this.userType === 'member') { additionalFields.innerHTML = ` <label for="member-id">会員ID:</label> <input type="text" id="member-id" name="memberId" required> <label for="member-level">会員レベル:</label> <select id="member-level" name="memberLevel"> <option value="bronze">ブロンズ</option> <option value="silver">シルバー</option> <option value="gold">ゴールド</option> </select> `; } else if (this.userType === 'business') { additionalFields.innerHTML = ` <label for="company-name">会社名:</label> <input type="text" id="company-name" name="companyName" required> <label for="department">部署:</label> <input type="text" id="department" name="department"> <label for="employee-count">従業員数:</label> <select id="employee-count" name="employeeCount"> <option value="1-10">1-10人</option> <option value="11-50">11-50人</option> <option value="51-200">51-200人</option> <option value="200+">200人以上</option> </select> `; } else { additionalFields.innerHTML = ''; } } async submitForm() { let formElement = document.getElementById('dynamic-form'); let formData = new FormData(formElement); // 共通データの追加 let submitData = { userType: this.userType, submittedAt: new Date().toISOString(), browserInfo: { userAgent: navigator.userAgent, language: navigator.language, platform: navigator.platform } }; // フォームデータを追加 formData.forEach((value, key) => { submitData[key] = value; }); // ユーザータイプ別の処理 let endpoint = this.getEndpointForUserType(); try { let response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(submitData) }); let result = await response.json(); if (response.ok) { this.handleSuccess(result); } else { this.handleError(result); } } catch (error) { console.error('送信エラー:', error); alert('通信エラーが発生しました'); } } getEndpointForUserType() { switch (this.userType) { case 'member': return '/api/members/register'; case 'business': return '/api/business/register'; default: return '/api/guests/register'; } } handleSuccess(result) { console.log('登録成功:', result); switch (this.userType) { case 'member': alert('会員登録が完了しました!'); window.location.href = '/member/dashboard'; break; case 'business': alert('法人登録が完了しました!'); window.location.href = '/business/setup'; break; default: alert('登録が完了しました!'); break; } } handleError(result) { console.error('登録失敗:', result); alert('登録に失敗しました: ' + (result.message || '不明なエラー')); }}
// 初期化document.addEventListener('DOMContentLoaded', function() { new ConditionalFormSubmitter();});
このように、動的にフォームの内容を変更し、適切なエンドポイントに送信できます。
ファイルアップロードを実装してみよう
画像ファイルのアップロード
ファイルをPOSTリクエストで送信する基本的な方法です。
<div class="upload-section"> <h3>画像アップロード</h3> <input type="file" id="image-input" accept="image/*"> <button onclick="uploadImage()">アップロード</button> <div id="upload-result"></div></div>
対応するJavaScriptです。
async function uploadImage() { let fileInput = document.getElementById('image-input'); let file = fileInput.files[0]; let resultDiv = document.getElementById('upload-result'); // ファイルが選択されているかチェック if (!file) { alert('ファイルを選択してください'); return; } // ファイルサイズのチェック(5MB以下) if (file.size > 5 * 1024 * 1024) { alert('ファイルサイズは5MB以下にしてください'); return; } // ファイルタイプのチェック if (!file.type.startsWith('image/')) { alert('画像ファイルを選択してください'); return; } // FormDataを作成 let formData = new FormData(); formData.append('image', file); formData.append('description', '画像アップロード'); formData.append('uploadedBy', 'ユーザー123'); formData.append('category', 'profile'); try { resultDiv.innerHTML = 'アップロード中...'; let response = await fetch('/api/upload/image', { method: 'POST', body: formData }); let result = await response.json(); if (response.ok) { console.log('アップロード成功:', result); resultDiv.innerHTML = ` <div style="color: green;"> ✓ アップロード完了!<br> ファイル名: ${result.filename}<br> URL: <a href="${result.url}" target="_blank">${result.url}</a> </div> `; // プレビュー表示 showImagePreview(result.url); } else { throw new Error(result.message || 'アップロードに失敗しました'); } } catch (error) { console.error('アップロードエラー:', error); resultDiv.innerHTML = ` <div style="color: red;"> ✗ エラー: ${error.message} </div> `; }}
// 画像プレビューの表示function showImagePreview(url) { let previewDiv = document.getElementById('image-preview') || document.createElement('div'); previewDiv.id = 'image-preview'; previewDiv.innerHTML = ` <h4>アップロード画像:</h4> <img src="${url}" alt="アップロード画像" style="max-width: 300px; height: auto;"> `; document.getElementById('upload-result').appendChild(previewDiv);}
ファイルアップロードでは、FormDataを使うのが一般的です。 適切なバリデーション(サイズ、タイプ)を行うことで、セキュリティも向上します。
複数ファイルの同時アップロード
複数のファイルを一度にアップロードする機能です。
<div class="multi-upload-section"> <h3>複数ファイルアップロード</h3> <input type="file" id="multiple-files" multiple accept="image/*"> <button onclick="uploadMultipleFiles()">一括アップロード</button> <div id="upload-progress"></div> <div id="upload-results"></div></div>
対応するJavaScriptです。
async function uploadMultipleFiles() { let fileInput = document.getElementById('multiple-files'); let files = Array.from(fileInput.files); let progressDiv = document.getElementById('upload-progress'); let resultsDiv = document.getElementById('upload-results'); if (files.length === 0) { alert('ファイルを選択してください'); return; } // ファイル数の制限(最大10個) if (files.length > 10) { alert('一度にアップロードできるのは10個までです'); return; } // 全ファイルのバリデーション let validationResult = validateFiles(files); if (!validationResult.isValid) { alert(validationResult.message); return; } // プログレス表示の初期化 progressDiv.innerHTML = ` <div>アップロード進行状況: <span id="progress-text">0/${files.length}</span></div> <div style="background: #f0f0f0; height: 20px; border-radius: 10px;"> <div id="progress-bar" style="background: #007bff; height: 100%; width: 0%; border-radius: 10px; transition: width 0.3s;"></div> </div> `; let results = []; let progressBar = document.getElementById('progress-bar'); let progressText = document.getElementById('progress-text'); // ファイルを一つずつアップロード for (let i = 0; i < files.length; i++) { let file = files[i]; try { let result = await uploadSingleFile(file, i); results.push({ success: true, file: file.name, result: result }); // プログレスを更新 let progress = ((i + 1) / files.length) * 100; progressBar.style.width = progress + '%'; progressText.textContent = `${i + 1}/${files.length}`; } catch (error) { console.error(`ファイル ${file.name} のアップロードエラー:`, error); results.push({ success: false, file: file.name, error: error.message }); } } // 結果の表示 displayUploadResults(results);}
// 単一ファイルのアップロードasync function uploadSingleFile(file, index) { let formData = new FormData(); formData.append('file', file); formData.append('index', index.toString()); formData.append('originalName', file.name); formData.append('uploadTime', new Date().toISOString()); let response = await fetch('/api/upload/multiple', { method: 'POST', body: formData }); if (!response.ok) { let errorData = await response.json(); throw new Error(errorData.message || 'アップロードに失敗しました'); } return await response.json();}
// ファイルのバリデーションfunction validateFiles(files) { let totalSize = 0; for (let file of files) { // ファイルタイプチェック if (!file.type.startsWith('image/')) { return { isValid: false, message: `${file.name} は画像ファイルではありません` }; } // 個別ファイルサイズチェック(5MB) if (file.size > 5 * 1024 * 1024) { return { isValid: false, message: `${file.name} のサイズが5MBを超えています` }; } totalSize += file.size; } // 総サイズチェック(20MB) if (totalSize > 20 * 1024 * 1024) { return { isValid: false, message: 'ファイルの総サイズが20MBを超えています' }; } return { isValid: true };}
// アップロード結果の表示function displayUploadResults(results) { let resultsDiv = document.getElementById('upload-results'); let successCount = results.filter(r => r.success).length; let failureCount = results.length - successCount; let html = ` <h4>アップロード結果</h4> <p>成功: ${successCount}個 / 失敗: ${failureCount}個</p> <div class="results-list"> `; results.forEach(result => { if (result.success) { html += ` <div style="color: green; margin: 5px 0;"> ✓ ${result.file} - アップロード完了 <br> URL: <a href="${result.result.url}" target="_blank">${result.result.url}</a> </div> `; } else { html += ` <div style="color: red; margin: 5px 0;"> ✗ ${result.file} - エラー: ${result.error} </div> `; } }); html += '</div>'; resultsDiv.innerHTML = html; // プログレス表示をクリア document.getElementById('upload-progress').innerHTML = '';}
複数ファイルのアップロードでは、進捗表示と個別のエラーハンドリングが重要です。 ユーザーにとって分かりやすいフィードバックを提供しましょう。
ドラッグ&ドロップによるファイルアップロード
より直感的なファイルアップロード機能を実装してみましょう。
<div id="drop-zone" class="drop-zone"> <div class="drop-message"> <p>ファイルをここにドラッグ&ドロップ</p> <p>または</p> <button onclick="document.getElementById('hidden-file-input').click()"> ファイルを選択 </button> </div> <input type="file" id="hidden-file-input" multiple accept="image/*" style="display: none;"></div>
<div id="file-list"></div><div id="upload-status"></div>
<style>.drop-zone { border: 2px dashed #ccc; border-radius: 10px; padding: 50px; text-align: center; margin: 20px 0; background-color: #f9f9f9; transition: all 0.3s ease;}
.drop-zone.dragover { border-color: #007bff; background-color: #e3f2fd;}
.file-item { display: flex; align-items: center; padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 5px; background: white;}
.file-preview { width: 50px; height: 50px; object-fit: cover; margin-right: 10px; border-radius: 5px;}</style>
対応するJavaScriptです。
class DragDropUploader { constructor() { this.dropZone = document.getElementById('drop-zone'); this.fileInput = document.getElementById('hidden-file-input'); this.fileList = document.getElementById('file-list'); this.statusDiv = document.getElementById('upload-status'); this.selectedFiles = []; this.setupEventListeners(); } setupEventListeners() { // ドラッグ&ドロップイベント this.dropZone.addEventListener('dragover', this.handleDragOver.bind(this)); this.dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this)); this.dropZone.addEventListener('drop', this.handleDrop.bind(this)); // ファイル選択イベント this.fileInput.addEventListener('change', this.handleFileSelect.bind(this)); // ドラッグ中のページ全体の処理を防ぐ document.addEventListener('dragover', (e) => e.preventDefault()); document.addEventListener('drop', (e) => e.preventDefault()); } handleDragOver(event) { event.preventDefault(); this.dropZone.classList.add('dragover'); } handleDragLeave(event) { event.preventDefault(); this.dropZone.classList.remove('dragover'); } handleDrop(event) { event.preventDefault(); this.dropZone.classList.remove('dragover'); let files = Array.from(event.dataTransfer.files); this.addFiles(files); } handleFileSelect(event) { let files = Array.from(event.target.files); this.addFiles(files); } addFiles(files) { // 画像ファイルのみフィルタリング let imageFiles = files.filter(file => file.type.startsWith('image/')); if (imageFiles.length !== files.length) { alert('画像ファイルのみアップロード可能です'); } // 既存のリストに追加 this.selectedFiles = [...this.selectedFiles, ...imageFiles]; this.displayFileList(); } displayFileList() { this.fileList.innerHTML = ''; if (this.selectedFiles.length === 0) { this.fileList.innerHTML = '<p>選択されたファイルはありません</p>'; return; } this.selectedFiles.forEach((file, index) => { let fileItem = document.createElement('div'); fileItem.className = 'file-item'; // ファイルのプレビュー作成 this.createFilePreview(file, (previewUrl) => { fileItem.innerHTML = ` <img src="${previewUrl}" alt="プレビュー" class="file-preview"> <div class="file-info"> <div><strong>${file.name}</strong></div> <div>サイズ: ${this.formatFileSize(file.size)}</div> <div>タイプ: ${file.type}</div> </div> <button onclick="uploader.removeFile(${index})" style="margin-left: auto;"> 削除 </button> `; }); this.fileList.appendChild(fileItem); }); // アップロードボタンを追加 let uploadButton = document.createElement('button'); uploadButton.textContent = `${this.selectedFiles.length}個のファイルをアップロード`; uploadButton.onclick = () => this.uploadAllFiles(); uploadButton.style.cssText = 'width: 100%; padding: 10px; margin-top: 10px; background: #007bff; color: white; border: none; border-radius: 5px;'; this.fileList.appendChild(uploadButton); } createFilePreview(file, callback) { let reader = new FileReader(); reader.onload = (e) => callback(e.target.result); reader.readAsDataURL(file); } formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; let k = 1024; let sizes = ['Bytes', 'KB', 'MB', 'GB']; let i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } removeFile(index) { this.selectedFiles.splice(index, 1); this.displayFileList(); } async uploadAllFiles() { if (this.selectedFiles.length === 0) { alert('アップロードするファイルがありません'); return; } this.statusDiv.innerHTML = ` <div>アップロード中... 0/${this.selectedFiles.length}</div> <div style="background: #f0f0f0; height: 20px; border-radius: 10px; margin-top: 10px;"> <div id="upload-progress-bar" style="background: #28a745; height: 100%; width: 0%; border-radius: 10px; transition: width 0.3s;"></div> </div> `; let results = []; let progressBar = document.getElementById('upload-progress-bar'); for (let i = 0; i < this.selectedFiles.length; i++) { try { let result = await this.uploadSingleFile(this.selectedFiles[i]); results.push({ success: true, file: this.selectedFiles[i].name, result }); // プログレス更新 let progress = ((i + 1) / this.selectedFiles.length) * 100; progressBar.style.width = progress + '%'; this.statusDiv.firstChild.textContent = `アップロード中... ${i + 1}/${this.selectedFiles.length}`; } catch (error) { results.push({ success: false, file: this.selectedFiles[i].name, error: error.message }); } } this.displayUploadResults(results); this.selectedFiles = []; this.displayFileList(); } async uploadSingleFile(file) { let formData = new FormData(); formData.append('file', file); formData.append('uploadMethod', 'dragdrop'); let response = await fetch('/api/upload/dragdrop', { method: 'POST', body: formData }); if (!response.ok) { let errorData = await response.json(); throw new Error(errorData.message || 'アップロードに失敗しました'); } return await response.json(); } displayUploadResults(results) { let successCount = results.filter(r => r.success).length; let html = `<h4>アップロード完了: ${successCount}/${results.length}</h4>`; results.forEach(result => { if (result.success) { html += `<div style="color: green;">✓ ${result.file}</div>`; } else { html += `<div style="color: red;">✗ ${result.file}: ${result.error}</div>`; } }); this.statusDiv.innerHTML = html; }}
// 初期化let uploader;document.addEventListener('DOMContentLoaded', function() { uploader = new DragDropUploader();});
ドラッグ&ドロップ機能により、ユーザーにとってより直感的なファイルアップロードが実現できます。
エラーハンドリングをマスターしよう
包括的なエラー処理
様々なエラーに対応した堅牢な実装を見てみましょう。
class RobustAPIClient { constructor(baseUrl = '/api') { this.baseUrl = baseUrl; this.defaultTimeout = 10000; // 10秒 } async post(endpoint, data, options = {}) { let url = `${this.baseUrl}${endpoint}`; let timeout = options.timeout || this.defaultTimeout; try { // タイムアウト付きfetch let response = await this.fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...options.headers }, body: JSON.stringify(data) }, timeout); // HTTPステータスの詳細チェック await this.handleResponseStatus(response); // レスポンスの解析 let result = await this.parseResponse(response); return result; } catch (error) { // エラーの分類と処理 let handledError = this.categorizeError(error); console.error('API呼び出しエラー:', handledError); throw handledError; } } // タイムアウト付きfetch fetchWithTimeout(url, options, timeout) { return Promise.race([ fetch(url, options), new Promise((_, reject) => { setTimeout(() => { reject(new Error(`リクエストがタイムアウトしました (${timeout}ms)`)); }, timeout); }) ]); } // レスポンスステータスの処理 async handleResponseStatus(response) { if (response.ok) { return; } let errorMessage = 'HTTPエラーが発生しました'; let errorData = null; try { errorData = await response.json(); errorMessage = errorData.message || errorMessage; } catch (parseError) { // JSONパースエラーは無視 } switch (response.status) { case 400: throw new Error(`リクエストが不正です: ${errorMessage}`); case 401: throw new Error('認証が必要です。ログインしてください。'); case 403: throw new Error('アクセスが禁止されています。'); case 404: throw new Error('リソースが見つかりません。'); case 409: throw new Error(`データの競合が発生しました: ${errorMessage}`); case 429: throw new Error('リクエストが多すぎます。しばらく待ってから再試行してください。'); case 500: throw new Error('サーバー内部エラーが発生しました。'); case 502: throw new Error('サーバーが一時的に利用できません。'); case 503: throw new Error('サービスが一時的に利用できません。'); default: throw new Error(`HTTPエラー ${response.status}: ${errorMessage}`); } } // レスポンスの解析 async parseResponse(response) { let contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { try { return await response.json(); } catch (error) { throw new Error('レスポンスの解析に失敗しました(JSON形式が不正)'); } } else { return await response.text(); } } // エラーの分類 categorizeError(error) { let categorizedError = { originalError: error, type: 'unknown', message: error.message, isRetryable: false, userMessage: '予期しないエラーが発生しました' }; if (error instanceof TypeError) { // ネットワークエラー categorizedError.type = 'network'; categorizedError.isRetryable = true; categorizedError.userMessage = 'ネットワーク接続を確認してください'; } else if (error.message.includes('タイムアウト')) { // タイムアウトエラー categorizedError.type = 'timeout'; categorizedError.isRetryable = true; categorizedError.userMessage = 'リクエストがタイムアウトしました。再試行してください。'; } else if (error.message.includes('認証')) { // 認証エラー categorizedError.type = 'authentication'; categorizedError.isRetryable = false; categorizedError.userMessage = 'ログインが必要です'; } else if (error.message.includes('禁止')) { // 認可エラー categorizedError.type = 'authorization'; categorizedError.isRetryable = false; categorizedError.userMessage = 'この操作を行う権限がありません'; } else if (error.message.includes('見つかりません')) { // リソース不存在エラー categorizedError.type = 'not_found'; categorizedError.isRetryable = false; categorizedError.userMessage = '要求されたデータが見つかりません'; } else if (error.message.includes('サーバー')) { // サーバーエラー categorizedError.type = 'server'; categorizedError.isRetryable = true; categorizedError.userMessage = 'サーバーエラーが発生しました。後でもう一度お試しください。'; } return categorizedError; }}
// 使用例:リトライ機能付きAPI呼び出しclass RetryableAPIClient extends RobustAPIClient { async postWithRetry(endpoint, data, options = {}) { let maxRetries = options.maxRetries || 3; let retryDelay = options.retryDelay || 1000; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { let result = await this.post(endpoint, data, options); return result; } catch (error) { console.log(`試行 ${attempt}/${maxRetries} 失敗:`, error.message); // 最後の試行、またはリトライ不可能なエラー if (attempt === maxRetries || !error.isRetryable) { throw error; } // 次の試行まで待機 await this.sleep(retryDelay * attempt); } } } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }}
// 実際の使用例async function createUserWithErrorHandling() { let apiClient = new RetryableAPIClient(); try { let userData = { name: '新規ユーザー', email: 'newuser@example.com', role: 'user' }; let result = await apiClient.postWithRetry('/users', userData, { maxRetries: 3, retryDelay: 1000 }); console.log('ユーザー作成成功:', result); alert('ユーザーが正常に作成されました!'); } catch (error) { console.error('ユーザー作成失敗:', error); // ユーザーにわかりやすいメッセージを表示 alert(error.userMessage || 'ユーザーの作成に失敗しました'); // 特定のエラータイプに応じた処理 if (error.type === 'authentication') { window.location.href = '/login'; } else if (error.type === 'network' && confirm('ネットワークエラーです。再試行しますか?')) { createUserWithErrorHandling(); // 再試行 } }}
このような包括的なエラーハンドリングにより、ユーザーにとって分かりやすく、開発者にとってデバッグしやすいアプリケーションが作れます。
ステータス表示とユーザーフィードバック
リクエストの状態をユーザーに適切に伝える機能を実装しましょう。
<div class="api-demo"> <h3>API通信デモ</h3> <button id="api-button" onclick="performAPICall()">データを送信</button> <div id="status-display"></div></div>
<style>.status-loading { color: #007bff; border-left: 4px solid #007bff; padding-left: 10px; background-color: #f8f9fa;}
.status-success { color: #28a745; border-left: 4px solid #28a745; padding-left: 10px; background-color: #d4edda;}
.status-error { color: #dc3545; border-left: 4px solid #dc3545; padding-left: 10px; background-color: #f8d7da;}</style>
対応するJavaScriptです。
class APIStatusManager { constructor() { this.statusDisplay = document.getElementById('status-display'); this.apiButton = document.getElementById('api-button'); } showLoading(message = 'データを送信中...') { this.apiButton.disabled = true; this.statusDisplay.className = 'status-loading'; this.statusDisplay.innerHTML = ` <div style="display: flex; align-items: center;"> <div class="spinner"></div> <span style="margin-left: 10px;">${message}</span> </div> `; // スピナーのCSS追加 this.addSpinnerCSS(); } showSuccess(message, data = null) { this.apiButton.disabled = false; this.statusDisplay.className = 'status-success'; let html = `<strong>✓ ${message}</strong>`; if (data) { html += `<div style="margin-top: 10px; font-family: monospace; background: white; padding: 10px; border-radius: 5px;">${JSON.stringify(data, null, 2)}</div>`; } this.statusDisplay.innerHTML = html; // 5秒後に表示をクリア setTimeout(() => this.clear(), 5000); } showError(message, details = null) { this.apiButton.disabled = false; this.statusDisplay.className = 'status-error'; let html = `<strong>✗ ${message}</strong>`; if (details) { html += `<div style="margin-top: 10px; font-size: 0.9em; opacity: 0.8;">${details}</div>`; } this.statusDisplay.innerHTML = html; } clear() { this.statusDisplay.className = ''; this.statusDisplay.innerHTML = ''; } addSpinnerCSS() { if (!document.getElementById('spinner-css')) { let style = document.createElement('style'); style.id = 'spinner-css'; style.textContent = ` .spinner { width: 20px; height: 20px; border: 2px solid #f3f3f3; border-top: 2px solid #007bff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } }}
// API呼び出しの実行async function performAPICall() { let statusManager = new APIStatusManager(); let apiClient = new RobustAPIClient(); try { statusManager.showLoading('ユーザーデータを送信中...'); let userData = { name: 'テストユーザー', email: 'test@example.com', preferences: { theme: 'dark', language: 'ja', notifications: true }, timestamp: new Date().toISOString() }; let result = await apiClient.post('/users/create', userData); statusManager.showSuccess('ユーザーが正常に作成されました', result); } catch (error) { let errorMessage = error.userMessage || '送信に失敗しました'; let errorDetails = error.type ? `エラータイプ: ${error.type}` : null; statusManager.showError(errorMessage, errorDetails); // ログにはより詳細な情報を記録 console.error('API呼び出しエラー:', { message: error.message, type: error.type, isRetryable: error.isRetryable, originalError: error.originalError }); }}
適切なステータス表示により、ユーザーは処理の進行状況を把握でき、エラーが発生した場合も適切に対応できます。
認証付きリクエストを実装してみよう
JWTトークンを使った認証
認証が必要なAPIへのリクエストを実装しましょう。
class AuthenticatedAPIClient { constructor(baseUrl = '/api') { this.baseUrl = baseUrl; this.tokenKey = 'authToken'; this.refreshTokenKey = 'refreshToken'; } // トークンの取得 getToken() { return localStorage.getItem(this.tokenKey); } // トークンの設定 setToken(token) { localStorage.setItem(this.tokenKey, token); } // トークンの削除 removeToken() { localStorage.removeItem(this.tokenKey); localStorage.removeItem(this.refreshTokenKey); } // 認証ヘッダーの生成 getAuthHeaders() { let token = this.getToken(); if (!token) { throw new Error('認証トークンがありません'); } return { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; } // 認証付きPOSTリクエスト async authenticatedPost(endpoint, data) { try { let headers = this.getAuthHeaders(); let response = await fetch(`${this.baseUrl}${endpoint}`, { method: 'POST', headers: headers, body: JSON.stringify(data) }); // トークンの期限切れチェック if (response.status === 401) { let refreshed = await this.tryRefreshToken(); if (refreshed) { // リフレッシュ成功、リクエストを再試行 headers = this.getAuthHeaders(); response = await fetch(`${this.baseUrl}${endpoint}`, { method: 'POST', headers: headers, body: JSON.stringify(data) }); } else { // リフレッシュ失敗、ログインページにリダイレクト this.handleAuthenticationFailure(); throw new Error('認証が無効です。再ログインしてください。'); } } if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('認証付きリクエストエラー:', error); throw error; } } // トークンのリフレッシュ async tryRefreshToken() { let refreshToken = localStorage.getItem(this.refreshTokenKey); if (!refreshToken) { return false; } try { let response = await fetch(`${this.baseUrl}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: refreshToken }) }); if (response.ok) { let result = await response.json(); this.setToken(result.accessToken); if (result.refreshToken) { localStorage.setItem(this.refreshTokenKey, result.refreshToken); } return true; } return false; } catch (error) { console.error('トークンリフレッシュエラー:', error); return false; } } // 認証失敗時の処理 handleAuthenticationFailure() { this.removeToken(); // 現在のページをログイン後のリダイレクト先として保存 let currentPath = window.location.pathname + window.location.search; localStorage.setItem('redirectAfterLogin', currentPath); // ログインページにリダイレクト window.location.href = '/login'; }}
// 使用例class UserProfileManager { constructor() { this.apiClient = new AuthenticatedAPIClient(); } async updateProfile(profileData) { try { let result = await this.apiClient.authenticatedPost('/profile/update', profileData); console.log('プロフィール更新成功:', result); alert('プロフィールが更新されました!'); return result; } catch (error) { console.error('プロフィール更新エラー:', error); if (error.message.includes('再ログイン')) { alert('セッションが期限切れです。ログインしてください。'); } else { alert('プロフィールの更新に失敗しました。'); } throw error; } } async createPost(postData) { try { let result = await this.apiClient.authenticatedPost('/posts/create', { ...postData, createdAt: new Date().toISOString() }); console.log('投稿作成成功:', result); alert('投稿が作成されました!'); return result; } catch (error) { console.error('投稿作成エラー:', error); alert('投稿の作成に失敗しました。'); throw error; } }}
// 実際の使用例async function updateUserProfile() { let profileManager = new UserProfileManager(); let profileData = { displayName: '更新された名前', bio: 'これは更新されたプロフィールです。', location: '東京', website: 'https://example.com' }; await profileManager.updateProfile(profileData);}
async function createNewPost() { let profileManager = new UserProfileManager(); let postData = { title: '新しい投稿のタイトル', content: 'これは新しい投稿の内容です。', tags: ['JavaScript', 'API', 'チュートリアル'], isPublic: true }; await profileManager.createPost(postData);}
JWT認証システムにより、セキュアなAPIコミュニケーションが実現できます。
APIキーを使った認証
API キーによる認証の実装例です。
class APIKeyClient { constructor(apiKey, baseUrl = '/api') { this.apiKey = apiKey; this.baseUrl = baseUrl; this.rateLimitInfo = { remaining: null, reset: null, limit: null }; } // APIキー認証ヘッダーの生成 getHeaders(additionalHeaders = {}) { return { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, 'User-Agent': 'MyApp/1.0', ...additionalHeaders }; } // レート制限情報の更新 updateRateLimitInfo(response) { this.rateLimitInfo.remaining = response.headers.get('X-RateLimit-Remaining'); this.rateLimitInfo.limit = response.headers.get('X-RateLimit-Limit'); this.rateLimitInfo.reset = response.headers.get('X-RateLimit-Reset'); console.log('レート制限情報:', this.rateLimitInfo); } // レート制限チェック checkRateLimit() { if (this.rateLimitInfo.remaining !== null && parseInt(this.rateLimitInfo.remaining) <= 0) { let resetTime = new Date(parseInt(this.rateLimitInfo.reset) * 1000); let waitTime = resetTime - new Date(); throw new Error(`レート制限に達しました。${Math.ceil(waitTime / 1000)}秒後に再試行してください。`); } } async post(endpoint, data, options = {}) { try { // レート制限チェック this.checkRateLimit(); let headers = this.getHeaders(options.headers); let response = await fetch(`${this.baseUrl}${endpoint}`, { method: 'POST', headers: headers, body: JSON.stringify(data) }); // レート制限情報の更新 this.updateRateLimitInfo(response); // API キーエラーのチェック if (response.status === 401) { throw new Error('APIキーが無効です'); } else if (response.status === 403) { throw new Error('このAPIキーには権限がありません'); } else if (response.status === 429) { throw new Error('レート制限に達しました'); } if (!response.ok) { let errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error('APIキー認証リクエストエラー:', error); throw error; } } // レート制限情報の取得 getRateLimitInfo() { return { ...this.rateLimitInfo }; }}
// 使用例:データ分析APIclass DataAnalyticsAPI { constructor(apiKey) { this.client = new APIKeyClient(apiKey, '/api/analytics'); } async submitAnalyticsData(eventData) { try { let result = await this.client.post('/events', { events: Array.isArray(eventData) ? eventData : [eventData], timestamp: new Date().toISOString(), source: 'web-app' }); console.log('分析データ送信成功:', result); return result; } catch (error) { if (error.message.includes('レート制限')) { console.warn('レート制限に達しました:', error.message); // レート制限の場合は、後で再試行するようにキューに追加 this.queueForRetry(eventData); } else { console.error('分析データ送信エラー:', error); } throw error; } } async batchSubmitEvents(events) { // イベントを100個ずつのバッチに分割 let batchSize = 100; let results = []; for (let i = 0; i < events.length; i += batchSize) { let batch = events.slice(i, i + batchSize); try { let result = await this.submitAnalyticsData(batch); results.push(result); // レート制限を考慮して少し待機 if (i + batchSize < events.length) { await this.sleep(100); } } catch (error) { console.error(`バッチ ${Math.floor(i / batchSize) + 1} の送信エラー:`, error); if (error.message.includes('レート制限')) { // レート制限の場合は少し長く待機 await this.sleep(5000); i -= batchSize; // このバッチを再試行 } else { throw error; } } } return results; } queueForRetry(eventData) { // 実際のアプリケーションでは、永続化されたキューを使用 if (!window.retryQueue) { window.retryQueue = []; } window.retryQueue.push({ data: eventData, timestamp: Date.now(), retries: 0 }); console.log('リトライキューに追加:', eventData); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }}
// 実際の使用例async function trackUserActions() { let analytics = new DataAnalyticsAPI('your-api-key-here'); // 単一イベントの送信 await analytics.submitAnalyticsData({ event: 'button_click', button_id: 'submit-form', user_id: 'user123', page: '/contact' }); // 複数イベントの一括送信 let events = [ { event: 'page_view', page: '/home', user_id: 'user123' }, { event: 'form_start', form_id: 'contact-form', user_id: 'user123' }, { event: 'form_complete', form_id: 'contact-form', user_id: 'user123' } ]; await analytics.batchSubmitEvents(events);}
APIキー認証により、外部サービスとの安全な連携が可能になります。
まとめ
JavaScriptのfetch APIを使ったPOSTリクエストについて詳しく解説しました。
重要なポイント
- 基本機能: サーバーにデータを送信するための現代的なAPI
- データ形式: JSON、フォームデータ、ファイルなど様々な形式に対応
- エラーハンドリング: 適切な例外処理とユーザーフィードバック
- 認証: JWT、APIキーなど様々な認証方式への対応
実践的な活用
- フォーム送信: 入力データの検証と送信
- ファイルアップロード: 画像や文書のアップロード機能
- リアルタイム通信: チャットやコメント機能の実装
- API連携: 外部サービスとのデータ交換
開発のベストプラクティス
- 適切なエラーハンドリング: ユーザーに分かりやすいエラーメッセージ
- セキュリティ: 認証とデータ検証の実装
- ユーザビリティ: ローディング表示と進捗管理
- 保守性: 再利用可能なAPIクライアントの作成
fetch APIを適切に活用することで、モダンで使いやすいWebアプリケーションが作れるようになります。
まずは基本的なPOSTリクエストから始めて、だんだんと高度な機能も取り入れてみましょう。 きっと、もっと便利で信頼性の高いWebアプリケーションが作れるようになりますよ。
ぜひ今日から、これらの知識を活用してサーバーとの効率的なデータ通信を実装してみませんか?