HTML & JS Topic
JavaScript Fetch API & AJAX
1. Fetch API Basics
Basic GET Request
javascript
// Simple GET request
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// With async/await
async function fetchData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
Fetch Response Methods
javascript
fetch('https://api.example.com/data')
.then(response => {
// Check if request was successful
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Different ways to parse response
// For JSON data
return response.json();
// For text data
// return response.text();
// For binary data (images, files)
// return response.blob();
// For form data
// return response.formData();
// For array buffer
// return response.arrayBuffer();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Fetch with Headers & Options
javascript
// Custom headers and options
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here',
'X-Custom-Header': 'custom-value'
},
mode: 'cors', // 'cors', 'no-cors', 'same-origin'
cache: 'no-cache', // 'default', 'no-cache', 'reload', 'force-cache'
credentials: 'same-origin', // 'omit', 'same-origin', 'include'
redirect: 'follow' // 'manual', 'follow', 'error'
};
fetch('https://api.example.com/data', options)
.then(response => response.json())
.then(data => console.log(data));
2. HTTP Methods (GET, POST, PUT, DELETE)
GET - Retrieve Data
javascript
// Basic GET
async function getPosts() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const posts = await response.json();
return posts;
} catch (error) {
console.error('Failed to fetch posts:', error);
throw error;
}
}
// GET with query parameters
async function searchUsers(query) {
const url = new URL('https://api.example.com/users');
url.searchParams.append('q', query);
url.searchParams.append('limit', '10');
url.searchParams.append('page', '1');
const response = await fetch(url);
return await response.json();
}
// GET with headers (API key)
async function getWeather(city) {
const API_KEY = 'your-api-key';
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
return await response.json();
}
POST - Create Data
javascript
// Basic POST with JSON
async function createPost(postData) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData)
});
if (!response.ok) {
throw new Error(`Failed to create post: ${response.status}`);
}
return await response.json();
}
// Usage
const newPost = {
title: 'My New Post',
body: 'This is the content of my post',
userId: 1
};
createPost(newPost)
.then(data => console.log('Created post:', data))
.catch(error => console.error('Error:', error));
// POST with FormData (for file uploads)
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My uploaded file');
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData
// Note: Don't set Content-Type header for FormData
// Fetch will set it automatically with boundary
});
return await response.json();
}
// File upload example
document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadFile(file);
console.log('Upload successful:', result);
} catch (error) {
console.error('Upload failed:', error);
}
});
PUT - Update Data
javascript
// PUT to update entire resource
async function updatePost(postId, updatedData) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedData)
});
if (!response.ok) {
throw new Error(`Failed to update post: ${response.status}`);
}
return await response.json();
}
// Usage
const updatedPost = {
id: 1,
title: 'Updated Title',
body: 'Updated content',
userId: 1
};
updatePost(1, updatedPost)
.then(data => console.log('Updated post:', data))
.catch(error => console.error('Error:', error));
// PATCH for partial updates
async function partialUpdate(postId, partialData) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(partialData)
});
return await response.json();
}
// Usage: Only update the title
partialUpdate(1, { title: 'Only title updated' })
.then(data => console.log('Partially updated:', data));
DELETE - Remove Data
javascript
// DELETE request
async function deletePost(postId) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Failed to delete post: ${response.status}`);
}
// DELETE usually returns 204 No Content or 200 with success message
if (response.status === 204) {
return { success: true, message: 'Post deleted successfully' };
}
return await response.json();
}
// Usage with confirmation
async function deleteWithConfirmation(postId) {
if (!confirm('Are you sure you want to delete this post?')) {
return;
}
try {
const result = await deletePost(postId);
console.log('Delete successful:', result);
// Refresh the data or remove from UI
} catch (error) {
console.error('Delete failed:', error);
alert('Failed to delete post. Please try again.');
}
}
3. Handling JSON
Sending and Receiving JSON
javascript
// Complete JSON handling example
class ApiClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
...this.defaultHeaders,
...options.headers
},
...options
};
// Add body if present (stringify if it's not FormData)
if (config.body && !(config.body instanceof FormData)) {
config.body = JSON.stringify(config.body);
}
const response = await fetch(url, config);
// Handle different response types
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else if (contentType && contentType.includes('text/')) {
data = await response.text();
} else {
data = await response.blob();
}
// Check for errors
if (!response.ok) {
const error = new Error(data.message || `HTTP ${response.status}`);
error.status = response.status;
error.data = data;
throw error;
}
return {
data,
status: response.status,
headers: response.headers,
ok: response.ok
};
}
// Convenience methods
get(endpoint, options = {}) {
return this.request(endpoint, { method: 'GET', ...options });
}
post(endpoint, body, options = {}) {
return this.request(endpoint, { method: 'POST', body, ...options });
}
put(endpoint, body, options = {}) {
return this.request(endpoint, { method: 'PUT', body, ...options });
}
patch(endpoint, body, options = {}) {
return this.request(endpoint, { method: 'PATCH', body, ...options });
}
delete(endpoint, options = {}) {
return this.request(endpoint, { method: 'DELETE', ...options });
}
}
// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');
// Get all posts
api.get('/posts')
.then(({ data }) => console.log('Posts:', data));
// Create new post
api.post('/posts', {
title: 'New Post',
body: 'Content here',
userId: 1
}).then(({ data }) => console.log('Created:', data));
JSON Schema Validation
javascript
// Simple JSON validation
function validateJSON(schema, data) {
const errors = [];
// Check required fields
if (schema.required) {
schema.required.forEach(field => {
if (data[field] === undefined || data[field] === null) {
errors.push(`Missing required field: ${field}`);
}
});
}
// Check field types
if (schema.properties) {
Object.entries(schema.properties).forEach(([field, rules]) => {
if (data[field] !== undefined) {
if (rules.type && typeof data[field] !== rules.type) {
errors.push(`${field} should be ${rules.type}, got ${typeof data[field]}`);
}
if (rules.minLength && data[field].length < rules.minLength) {
errors.push(`${field} should be at least ${rules.minLength} characters`);
}
if (rules.maxLength && data[field].length > rules.maxLength) {
errors.push(`${field} should be at most ${rules.maxLength} characters`);
}
}
});
}
return {
valid: errors.length === 0,
errors
};
}
// Schema definition
const postSchema = {
required: ['title', 'body', 'userId'],
properties: {
title: { type: 'string', minLength: 3, maxLength: 100 },
body: { type: 'string', minLength: 10 },
userId: { type: 'number' }
}
};
// Usage in API call
async function createPostWithValidation(postData) {
const validation = validateJSON(postSchema, postData);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
return await api.post('/posts', postData);
}
4. API Error Handling
Comprehensive Error Handling
javascript
// Custom error classes
class ApiError extends Error {
constructor(message, status, data = null) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
this.timestamp = new Date().toISOString();
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
data: this.data,
timestamp: this.timestamp,
stack: this.stack
};
}
}
class NetworkError extends Error {
constructor(message, originalError = null) {
super(message);
this.name = 'NetworkError';
this.originalError = originalError;
}
}
class ValidationError extends Error {
constructor(message, errors = []) {
super(message);
this.name = 'ValidationError';
this.errors = errors;
}
}
// Enhanced fetch with error handling
async function safeFetch(url, options = {}) {
let response;
try {
response = await fetch(url, options);
} catch (error) {
// Network errors (no internet, CORS, etc.)
throw new NetworkError(
`Network request failed: ${error.message}`,
error
);
}
// Parse response
let data;
const contentType = response.headers.get('content-type');
try {
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
} catch (error) {
// Response parsing error
throw new ApiError(
`Failed to parse response: ${error.message}`,
response.status,
null
);
}
// Check HTTP status
if (!response.ok) {
// Handle specific status codes
switch (response.status) {
case 400:
throw new ValidationError('Bad request', data.errors || data);
case 401:
throw new ApiError('Unauthorized - please login', 401, data);
case 403:
throw new ApiError('Forbidden - insufficient permissions', 403, data);
case 404:
throw new ApiError('Resource not found', 404, data);
case 429:
throw new ApiError('Too many requests - rate limited', 429, data);
case 500:
throw new ApiError('Server error', 500, data);
case 503:
throw new ApiError('Service unavailable', 503, data);
default:
throw new ApiError(
data.message || `HTTP ${response.status}`,
response.status,
data
);
}
}
return {
data,
status: response.status,
headers: response.headers,
ok: response.ok
};
}
// Usage with comprehensive error handling
async function fetchUserData(userId) {
try {
const result = await safeFetch(`https://api.example.com/users/${userId}`);
return result.data;
} catch (error) {
// Handle different error types
switch (error.name) {
case 'NetworkError':
console.error('Network issue:', error.message);
// Show offline message to user
showErrorMessage('Please check your internet connection');
break;
case 'ValidationError':
console.error('Validation failed:', error.errors);
// Show validation errors to user
showValidationErrors(error.errors);
break;
case 'ApiError':
console.error(`API Error ${error.status}:`, error.message);
// Handle specific API errors
if (error.status === 401) {
// Redirect to login
window.location.href = '/login';
} else if (error.status === 429) {
// Show rate limit message
showErrorMessage('Too many requests. Please wait a moment.');
} else {
// General API error
showErrorMessage(`API Error: ${error.message}`);
}
break;
default:
console.error('Unknown error:', error);
showErrorMessage('An unexpected error occurred');
}
// Re-throw for higher level handling if needed
throw error;
}
}
Retry Logic with Exponential Backoff
javascript
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
// Don't retry client errors (4xx) except 429
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw new Error(`Client error: ${response.status}`);
}
// Throw to trigger retry for server errors
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
break; // No more retries
}
// Calculate delay with exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage
fetchWithRetry('https://api.example.com/data', {}, 3)
.then(data => console.log('Success after retries:', data))
.catch(error => console.error('All retries failed:', error));
5. Practical Examples
Complete Blog API Client
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog API Client</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
border-radius: 10px;
margin-bottom: 2rem;
text-align: center;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 2rem;
}
.card {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
.card h3 {
color: #667eea;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f0f0f0;
}
button {
background: #667eea;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background 0.3s ease;
width: 100%;
margin-top: 10px;
}
button:hover {
background: #5a67d8;
}
button.danger {
background: #e53e3e;
}
button.danger:hover {
background: #c53030;
}
input, textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 5px;
font-size: 1rem;
margin-bottom: 1rem;
transition: border-color 0.3s ease;
}
input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
min-height: 120px;
resize: vertical;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 2rem;
}
.post-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
}
.post-card h4 {
color: #2d3748;
margin-bottom: 0.5rem;
font-size: 1.25rem;
}
.post-card p {
color: #4a5568;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.post-actions {
display: flex;
gap: 10px;
margin-top: 1rem;
}
.post-actions button {
flex: 1;
padding: 0.5rem;
font-size: 0.9rem;
margin: 0;
}
.loading {
text-align: center;
padding: 2rem;
color: #667eea;
font-size: 1.2rem;
}
.error {
background: #fed7d7;
color: #9b2c2c;
padding: 1rem;
border-radius: 5px;
margin: 1rem 0;
border-left: 4px solid #e53e3e;
}
.success {
background: #c6f6d5;
color: #22543d;
padding: 1rem;
border-radius: 5px;
margin: 1rem 0;
border-left: 4px solid #38a169;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 10px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.status-indicator {
padding: 0.5rem;
border-radius: 5px;
font-weight: 600;
text-align: center;
margin-bottom: 1rem;
}
.status-indicator.loading {
background: #bee3f8;
color: #2c5282;
}
.status-indicator.success {
background: #c6f6d5;
color: #22543d;
}
.status-indicator.error {
background: #fed7d7;
color: #9b2c2c;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>📝 Blog API Client</h1>
<p>CRUD Operations with JSONPlaceholder API</p>
</header>
<div class="controls">
<div class="card">
<h3>📬 Get All Posts</h3>
<p>Fetch all posts from the API</p>
<button onclick="getAllPosts()">Get All Posts</button>
</div>
<div class="card">
<h3>🔍 Get Single Post</h3>
<input type="number" id="post-id" placeholder="Enter Post ID" min="1">
<button onclick="getPostById()">Get Post</button>
</div>
<div class="card">
<h3>➕ Create New Post</h3>
<input type="text" id="post-title" placeholder="Post Title">
<textarea id="post-body" placeholder="Post Content"></textarea>
<button onclick="createNewPost()">Create Post</button>
</div>
<div class="card">
<h3>🗑️ Delete Post</h3>
<input type="number" id="delete-id" placeholder="Enter Post ID to delete">
<button class="danger" onclick="deletePost()">Delete Post</button>
</div>
</div>
<div id="status"></div>
<div class="posts-grid" id="posts-container">
<!-- Posts will be loaded here -->
</div>
</div>
<!-- Modal for editing posts -->
<div class="modal" id="edit-modal">
<div class="modal-content">
<h3>✏️ Edit Post</h3>
<input type="hidden" id="edit-id">
<input type="text" id="edit-title" placeholder="Post Title">
<textarea id="edit-body" placeholder="Post Content"></textarea>
<button onclick="updatePost()">Update Post</button>
<button onclick="closeModal()" style="background: #718096; margin-top: 10px;">Cancel</button>
</div>
</div>
<script>
const API_URL = 'https://jsonplaceholder.typicode.com/posts';
const postsContainer = document.getElementById('posts-container');
const statusDiv = document.getElementById('status');
const editModal = document.getElementById('edit-modal');
// Utility functions
function showStatus(message, type = 'info') {
statusDiv.innerHTML = `
<div class="status-indicator ${type}">
${message}
</div>
`;
// Auto-hide success messages after 3 seconds
if (type === 'success') {
setTimeout(() => {
statusDiv.innerHTML = '';
}, 3000);
}
}
function showError(message) {
showStatus(`${message}`, 'error');
}
function showLoading(message) {
showStatus(` ${message}...`, 'loading');
}
function showSuccess(message) {
showStatus(` ${message}`, 'success');
}
function closeModal() {
editModal.classList.remove('active');
}
// API Functions
async function fetchWithErrorHandling(url, options = {}) {
try {
showLoading('Loading data');
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
showSuccess('Data loaded successfully');
return data;
} catch (error) {
showError(`Failed: ${error.message}`);
console.error('API Error:', error);
throw error;
}
}
// CRUD Operations
async function getAllPosts() {
try {
const posts = await fetchWithErrorHandling(API_URL);
renderPosts(posts);
} catch (error) {
// Error already shown by fetchWithErrorHandling
}
}
async function getPostById() {
const postId = document.getElementById('post-id').value;
if (!postId || postId < 1) {
showError('Please enter a valid Post ID (>= 1)');
return;
}
try {
const post = await fetchWithErrorHandling(`${API_URL}/${postId}`);
renderPosts([post]); // Render as array
} catch (error) {
// Error already shown
}
}
async function createNewPost() {
const title = document.getElementById('post-title').value.trim();
const body = document.getElementById('post-body').value.trim();
if (!title || !body) {
showError('Please fill in both title and body');
return;
}
const newPost = {
title,
body,
userId: 1 // Hardcoded for demo
};
try {
const createdPost = await fetchWithErrorHandling(API_URL, {
method: 'POST',
body: JSON.stringify(newPost)
});
showSuccess(`Post created with ID: ${createdPost.id}`);
document.getElementById('post-title').value = '';
document.getElementById('post-body').value = '';
// Refresh posts
getAllPosts();
} catch (error) {
// Error already shown
}
}
async function updatePost() {
const id = document.getElementById('edit-id').value;
const title = document.getElementById('edit-title').value.trim();
const body = document.getElementById('edit-body').value.trim();
if (!title || !body) {
showError('Please fill in both title and body');
return;
}
const updatedPost = {
id: parseInt(id),
title,
body,
userId: 1
};
try {
const result = await fetchWithErrorHandling(`${API_URL}/${id}`, {
method: 'PUT',
body: JSON.stringify(updatedPost)
});
showSuccess('Post updated successfully');
closeModal();
getAllPosts();
} catch (error) {
// Error already shown
}
}
async function deletePost() {
const postId = document.getElementById('delete-id').value;
if (!postId || postId < 1) {
showError('Please enter a valid Post ID');
return;
}
if (!confirm(`Are you sure you want to delete post #${postId}?`)) {
return;
}
try {
await fetchWithErrorHandling(`${API_URL}/${postId}`, {
method: 'DELETE'
});
showSuccess(`Post #${postId} deleted successfully`);
document.getElementById('delete-id').value = '';
getAllPosts();
} catch (error) {
// Error already shown
}
}
// UI Rendering
function renderPosts(posts) {
if (!posts || posts.length === 0) {
postsContainer.innerHTML = '<div class="loading">No posts found</div>';
return;
}
postsContainer.innerHTML = posts.map(post => `
<div class="post-card" data-id="${post.id}">
<h4>${post.title}</h4>
<p>${post.body.substring(0, 150)}${post.body.length > 150 ? '...' : ''}</p>
<div class="post-meta">
<small>Post ID: ${post.id} | User ID: ${post.userId}</small>
</div>
<div class="post-actions">
<button onclick="openEditModal(${post.id}, '${post.title.replace(/'/g, "\\'")}', '${post.body.replace(/'/g, "\\'")}')">
Edit
</button>
<button class="danger" onclick="confirmDelete(${post.id})">
Delete
</button>
</div>
</div>
`).join('');
}
function openEditModal(id, title, body) {
document.getElementById('edit-id').value = id;
document.getElementById('edit-title').value = title;
document.getElementById('edit-body').value = body;
editModal.classList.add('active');
}
async function confirmDelete(id) {
if (!confirm(`Delete post #${id}?`)) {
return;
}
try {
await fetchWithErrorHandling(`${API_URL}/${id}`, {
method: 'DELETE'
});
showSuccess(`Post #${id} deleted`);
getAllPosts();
} catch (error) {
// Error already shown
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
getAllPosts();
// Close modal when clicking outside
editModal.addEventListener('click', (e) => {
if (e.target === editModal) {
closeModal();
}
});
// Close modal with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && editModal.classList.contains('active')) {
closeModal();
}
});
});
</script>
</body>
</html>
Comments (0)
No comments yet. Be the first to comment!