The Beginner's Guide to APIs with JavaScript (Fetch & Axios)
In today's interconnected web, APIs (Application Programming Interfaces) are the backbone of modern web development. Whether you're building a weather app, fetching user data, or integrating third-party services, understanding how to work with APIs in JavaScript is essential. This comprehensive guide will walk you through everything you need to know about making API calls using both the native Fetch API and the popular Axios library.
What is an API?
An API (Application Programming Interface) is a set of rules and protocols that allows different software applications to communicate with each other. Think of it as a waiter in a restaurant – you (the client) make a request through the waiter (API) to the kitchen (server), and the waiter brings back your food (data).
Types of APIs
REST APIs are the most common type you'll encounter. They use standard HTTP methods: - GET: Retrieve data - POST: Create new data - PUT: Update existing data - DELETE: Remove data
GraphQL APIs allow you to request specific data fields, reducing over-fetching.
WebSocket APIs enable real-time, bidirectional communication.
Understanding HTTP Requests and Responses
Before diving into code, let's understand the anatomy of HTTP requests and responses.
HTTP Request Components
`javascript
// Example of what happens behind the scenes
const request = {
method: 'GET',
url: 'https://api.example.com/users',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
},
body: JSON.stringify({ name: 'John' }) // Only for POST/PUT requests
};
`
HTTP Response Components
`javascript
// What you receive back
const response = {
status: 200,
statusText: 'OK',
headers: {
'content-type': 'application/json'
},
data: {
id: 1,
name: 'John Doe',
email: 'john@example.com'
}
};
`
Common HTTP Status Codes
- 200: Success - 201: Created - 400: Bad Request - 401: Unauthorized - 404: Not Found - 500: Internal Server Error
The Fetch API: JavaScript's Built-in Solution
The Fetch API is built into modern browsers and provides a clean, promise-based interface for making HTTP requests.
Basic Fetch Syntax
`javascript
fetch(url, options)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
`
Making Your First GET Request
`javascript
// Simple GET request
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// Check if request was successful
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const users = await response.json();
console.log(users);
return users;
} catch (error) {
console.error('Failed to fetch users:', error);
}
}
fetchUsers();
`
POST Requests with Fetch
`javascript
async function createUser(userData) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const newUser = await response.json(); console.log('User created:', newUser); return newUser; } catch (error) { console.error('Failed to create user:', error); } }
// Usage
createUser({
name: 'Jane Doe',
email: 'jane@example.com',
phone: '123-456-7890'
});
`
PUT and DELETE Requests
`javascript
// Update user
async function updateUser(userId, userData) {
try {
const response = await fetch(https://jsonplaceholder.typicode.com/users/${userId}, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const updatedUser = await response.json(); return updatedUser; } catch (error) { console.error('Failed to update user:', error); } }
// Delete user
async function deleteUser(userId) {
try {
const response = await fetch(https://jsonplaceholder.typicode.com/users/${userId}, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
console.log(User ${userId} deleted successfully);
} catch (error) {
console.error('Failed to delete user:', error);
}
}
`
Introducing Axios: A Powerful HTTP Client
While Fetch is great, Axios offers additional features and a more convenient API. It's a popular third-party library that simplifies HTTP requests.
Installing Axios
`bash
Using npm
npm install axiosUsing yarn
yarn add axiosUsing CDN (in HTML)
`Basic Axios Usage
`javascript
import axios from 'axios';
// GET request
async function fetchUsers() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
console.log(response.data);
return response.data;
} catch (error) {
console.error('Error fetching users:', error.response?.data || error.message);
}
}
`
Axios vs Fetch Comparison
`javascript
// Fetch
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
};
// Axios - automatically parses JSON
const axiosData = async () => {
const response = await axios.get('https://api.example.com/data');
return response.data;
};
`
Axios Configuration and Interceptors
`javascript
// Create an axios instance with default config
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
}
});
// Request interceptor
api.interceptors.request.use(
config => {
// Add auth token to requests
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = Bearer ${token};
}
console.log('Request sent:', config);
return config;
},
error => {
return Promise.reject(error);
}
);
// Response interceptor
api.interceptors.response.use(
response => {
console.log('Response received:', response);
return response;
},
error => {
if (error.response?.status === 401) {
// Handle unauthorized access
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
`
Error Handling Best Practices
Proper error handling is crucial for building robust applications.
Comprehensive Error Handling with Fetch
`javascript
async function robustFetch(url, options = {}) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.text();
throw new Error(HTTP ${response.status}: ${errorData});
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
`
Error Handling with Axios
`javascript
async function handleAxiosRequest() {
try {
const response = await axios.get('/api/data', {
timeout: 5000
});
return response.data;
} catch (error) {
if (error.response) {
// Server responded with error status
console.error('Response error:', error.response.status, error.response.data);
} else if (error.request) {
// Request was made but no response received
console.error('Network error:', error.request);
} else {
// Something else happened
console.error('Error:', error.message);
}
throw error;
}
}
`
Working with Different Data Formats
Handling JSON Data
`javascript
// Fetch JSON data
async function fetchJSON(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('JSON parsing error:', error);
}
}
// Send JSON data
async function sendJSON(url, data) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
console.error('Error sending JSON:', error);
}
}
`
Working with Form Data
`javascript
// Send form data
async function sendFormData(url, formData) {
try {
const response = await fetch(url, {
method: 'POST',
body: formData // Don't set Content-Type for FormData
});
return await response.json();
} catch (error) {
console.error('Error sending form data:', error);
}
}
// Create FormData from form element const form = document.getElementById('myForm'); const formData = new FormData(form);
// Or create FormData manually
const manualFormData = new FormData();
manualFormData.append('name', 'John Doe');
manualFormData.append('email', 'john@example.com');
`
File Upload Example
`javascript
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My uploaded file');
try {
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(Upload progress: ${percentCompleted}%);
}
});
return response.data;
} catch (error) {
console.error('Upload failed:', error);
}
}
`
Authentication and Headers
API Key Authentication
`javascript
const API_KEY = 'your-api-key-here';
async function fetchWithApiKey(url) {
try {
const response = await fetch(url, {
headers: {
'Authorization': Bearer ${API_KEY},
'Content-Type': 'application/json'
}
});
return await response.json();
} catch (error) {
console.error('API request failed:', error);
}
}
`
JWT Token Authentication
`javascript
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = localStorage.getItem('authToken');
}
async request(endpoint, options = {}) {
const url = ${this.baseURL}${endpoint};
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
if (this.token) {
config.headers.Authorization = Bearer ${this.token};
}
try { const response = await fetch(url, config); if (response.status === 401) { this.handleUnauthorized(); throw new Error('Unauthorized'); }
return await response.json(); } catch (error) { console.error('API request failed:', error); throw error; } }
handleUnauthorized() { localStorage.removeItem('authToken'); // Redirect to login page window.location.href = '/login'; }
setToken(token) { this.token = token; localStorage.setItem('authToken', token); } }
const api = new ApiClient('https://api.example.com');
`
Building Your First API-Powered App: Weather Dashboard
Let's build a practical weather dashboard that demonstrates real API usage.
HTML Structure
`html
Weather Dashboard
`JavaScript Weather App
`javascript
class WeatherApp {
constructor() {
this.API_KEY = 'your-openweather-api-key'; // Get from openweathermap.org
this.BASE_URL = 'https://api.openweathermap.org/data/2.5';
this.displayElement = document.getElementById('weatherDisplay');
}
async getCurrentWeather(city) {
try {
this.showLoading();
const response = await fetch(
${this.BASE_URL}/weather?q=${city}&appid=${this.API_KEY}&units=metric
);
if (!response.ok) {
throw new Error(Weather data not found for ${city});
}
const data = await response.json(); this.displayCurrentWeather(data); // Get 5-day forecast await this.getForecast(city); } catch (error) { this.showError(error.message); } }
async getForecast(city) {
try {
const response = await fetch(
${this.BASE_URL}/forecast?q=${city}&appid=${this.API_KEY}&units=metric
);
if (!response.ok) { throw new Error('Forecast data not available'); }
const data = await response.json(); this.displayForecast(data); } catch (error) { console.error('Forecast error:', error); } }
displayCurrentWeather(data) { const { name, main, weather, wind } = data; const currentWeather = weather[0];
const html = `
${name}
${Math.round(main.temp)}°C
${currentWeather.description}
Feels like: ${Math.round(main.feels_like)}°C
Humidity: ${main.humidity}%
Wind Speed: ${wind.speed} m/s
Pressure: ${main.pressure} hPa
this.displayElement.innerHTML = html; }
displayForecast(data) { const forecastHTML = `
5-Day Forecast
${new Date(item.dt * 1000).toLocaleDateString()}
${Math.round(item.main.temp)}°C
${item.weather[0].description}
this.displayElement.innerHTML += forecastHTML; }
showLoading() { this.displayElement.innerHTML = '
showError(message) {
this.displayElement.innerHTML = ;
}
}
// Initialize the app const weatherApp = new WeatherApp();
// Search function async function searchWeather() { const city = document.getElementById('cityInput').value.trim(); if (city) { await weatherApp.getCurrentWeather(city); } }
// Allow Enter key to trigger search document.getElementById('cityInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') { searchWeather(); } });
// Load default city on page load
window.addEventListener('load', () => {
weatherApp.getCurrentWeather('London');
});
`
Building a Todo App with API Integration
Let's create a more complex example with full CRUD operations.
Todo App with Local Storage API
`javascript
class TodoApp {
constructor() {
this.todos = [];
this.API_URL = 'https://jsonplaceholder.typicode.com/todos';
this.init();
}
async init() { await this.loadTodos(); this.render(); this.bindEvents(); }
async loadTodos() {
try {
// For demo, we'll load from JSONPlaceholder and store locally
const response = await fetch(${this.API_URL}?_limit=10);
const todos = await response.json();
// Store in localStorage
localStorage.setItem('todos', JSON.stringify(todos));
this.todos = todos;
} catch (error) {
console.error('Failed to load todos:', error);
// Fallback to localStorage
const stored = localStorage.getItem('todos');
this.todos = stored ? JSON.parse(stored) : [];
}
}
async addTodo(title) { const newTodo = { id: Date.now(), // Simple ID generation title: title.trim(), completed: false, userId: 1 };
try { // Simulate API call const response = await fetch(this.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(newTodo) });
if (response.ok) { this.todos.unshift(newTodo); this.saveTodos(); this.render(); } } catch (error) { console.error('Failed to add todo:', error); } }
async toggleTodo(id) { const todoIndex = this.todos.findIndex(todo => todo.id === id); if (todoIndex === -1) return;
const todo = this.todos[todoIndex]; todo.completed = !todo.completed;
try {
// Simulate API update
await fetch(${this.API_URL}/${id}, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(todo)
});
this.saveTodos(); this.render(); } catch (error) { console.error('Failed to update todo:', error); // Revert on error todo.completed = !todo.completed; } }
async deleteTodo(id) {
try {
await fetch(${this.API_URL}/${id}, {
method: 'DELETE'
});
this.todos = this.todos.filter(todo => todo.id !== id); this.saveTodos(); this.render(); } catch (error) { console.error('Failed to delete todo:', error); } }
saveTodos() { localStorage.setItem('todos', JSON.stringify(this.todos)); }
render() { const container = document.getElementById('todoContainer'); const completedCount = this.todos.filter(todo => todo.completed).length; container.innerHTML = `
My Todos
${completedCount} of ${this.todos.length} completed
handleAddTodo() { const input = document.getElementById('newTodoInput'); const title = input.value.trim(); if (title) { this.addTodo(title); input.value = ''; } }
bindEvents() { document.addEventListener('keypress', (e) => { if (e.key === 'Enter' && e.target.id === 'newTodoInput') { this.handleAddTodo(); } }); } }
// Initialize the todo app
const todoApp = new TodoApp();
`
Advanced Techniques and Best Practices
Caching API Responses
`javascript
class CachedApiClient {
constructor(baseURL, cacheDuration = 5 60 1000) { // 5 minutes default
this.baseURL = baseURL;
this.cache = new Map();
this.cacheDuration = cacheDuration;
}
async get(endpoint) {
const cacheKey = ${this.baseURL}${endpoint};
const cached = this.cache.get(cacheKey);
// Return cached data if still valid if (cached && Date.now() - cached.timestamp < this.cacheDuration) { console.log('Returning cached data'); return cached.data; }
try {
const response = await fetch(${this.baseURL}${endpoint});
const data = await response.json();
// Cache the response this.cache.set(cacheKey, { data, timestamp: Date.now() });
return data; } catch (error) { // Return cached data if available, even if expired if (cached) { console.log('Returning stale cached data due to error'); return cached.data; } throw error; } }
clearCache() {
this.cache.clear();
}
}
`
Rate Limiting and Throttling
`javascript
class RateLimitedApiClient {
constructor(requestsPerSecond = 5) {
this.requestsPerSecond = requestsPerSecond;
this.requestQueue = [];
this.isProcessing = false;
}
async request(url, options) { return new Promise((resolve, reject) => { this.requestQueue.push({ url, options, resolve, reject }); this.processQueue(); }); }
async processQueue() { if (this.isProcessing || this.requestQueue.length === 0) { return; }
this.isProcessing = true;
while (this.requestQueue.length > 0) { const { url, options, resolve, reject } = this.requestQueue.shift();
try { const response = await fetch(url, options); const data = await response.json(); resolve(data); } catch (error) { reject(error); }
// Wait before processing next request await new Promise(resolve => setTimeout(resolve, 1000 / this.requestsPerSecond) ); }
this.isProcessing = false;
}
}
`
Retry Logic for Failed Requests
`javascript
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
// Don't retry for client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(Client error: ${response.status});
}
throw new Error(Server error: ${response.status});
} catch (error) {
console.log(Attempt ${attempt} failed:, error.message);
if (attempt === maxRetries) {
throw error;
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
console.log(Retrying in ${delay}ms...);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
`
Testing API Calls
Simple Testing Framework
`javascript
class ApiTester {
constructor() {
this.tests = [];
}
async test(name, testFunction) {
try {
console.log(Running test: ${name});
await testFunction();
console.log(✅ ${name} passed);
} catch (error) {
console.error(❌ ${name} failed:, error.message);
}
}
async runAll() { for (const test of this.tests) { await this.test(test.name, test.fn); } } }
// Example tests const tester = new ApiTester();
tester.test('Fetch users returns array', async () => { const users = await fetch('https://jsonplaceholder.typicode.com/users') .then(r => r.json()); if (!Array.isArray(users)) { throw new Error('Response is not an array'); } if (users.length === 0) { throw new Error('Users array is empty'); } });
tester.test('Create user returns new user with ID', async () => { const newUser = await fetch('https://jsonplaceholder.typicode.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Test User', email: 'test@example.com' }) }).then(r => r.json()); if (!newUser.id) { throw new Error('New user does not have an ID'); } });
// Run tests
tester.runAll();
`
Performance Optimization
Parallel Requests
`javascript
// Instead of sequential requests
async function fetchSequential() {
const users = await fetch('/api/users').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { users, posts, comments };
}
// Use parallel requests
async function fetchParallel() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
}
`
Request Cancellation
`javascript
class CancellableRequest {
constructor() {
this.controller = null;
}
async fetch(url, options = {}) { // Cancel previous request if exists if (this.controller) { this.controller.abort(); }
this.controller = new AbortController(); try { const response = await fetch(url, { ...options, signal: this.controller.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Request was cancelled'); } else { throw error; } } }
cancel() { if (this.controller) { this.controller.abort(); } } }
// Usage const request = new CancellableRequest();
// This will cancel the previous request if still pending
request.fetch('/api/search?q=javascript');
request.fetch('/api/search?q=python'); // Cancels the above request
`
Conclusion
Working with APIs in JavaScript is a fundamental skill for modern web development. Whether you choose the native Fetch API or the feature-rich Axios library, understanding how to make requests, handle responses, and manage errors will enable you to build powerful, data-driven applications.
Key takeaways from this guide:
1. Start with Fetch: It's built into browsers and perfect for basic API interactions 2. Consider Axios: For more complex applications requiring advanced features 3. Handle errors gracefully: Always implement proper error handling 4. Optimize performance: Use caching, parallel requests, and request cancellation 5. Test your API calls: Ensure reliability with proper testing 6. Secure your requests: Implement proper authentication and handle sensitive data carefully
Remember that working with APIs is as much about understanding the data and the service you're consuming as it is about the technical implementation. Always read the API documentation, understand rate limits, and respect the service's terms of use.
As you continue your journey with JavaScript and APIs, practice with different public APIs, experiment with various authentication methods, and gradually build more complex applications. The skills you've learned in this guide will serve as a solid foundation for creating amazing web experiences.