What is a REST API?
REST (Representational State Transfer) is an architectural style for building web services. RESTful APIs use HTTP methods to perform CRUD operations and are the backbone of modern web applications.
HTTP Methods
GET - Retrieve Data
// Fetch all users
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data));
// Fetch specific user
fetch('https://api.example.com/users/123')
.then(response => response.json())
.then(user => console.log(user));
POST - Create Data
// Create new user
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
})
.then(response => response.json())
.then(user => console.log('Created:', user));
PUT - Update Data
// Update entire user
fetch('https://api.example.com/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Updated',
email: 'john.updated@example.com'
})
})
.then(response => response.json())
.then(user => console.log('Updated:', user));
PATCH - Partial Update
// Update only specific fields
fetch('https://api.example.com/users/123', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'newemail@example.com'
})
})
.then(response => response.json())
.then(user => console.log('Patched:', user));
DELETE - Remove Data
// Delete user
fetch('https://api.example.com/users/123', {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
console.log('User deleted successfully');
}
});
Modern Fetch with Async/Await
Much cleaner syntax using async/await:
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
// Usage
const users = await getUsers();
Authentication
Bearer Token Authentication
async function fetchWithAuth(url) {
const token = localStorage.getItem('authToken');
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.json();
}
API Key Authentication
const API_KEY = 'your-api-key';
async function fetchWithApiKey(url) {
const response = await fetch(url, {
headers: {
'X-API-Key': API_KEY
}
});
return response.json();
}
Error Handling
Comprehensive Error Handling
async function robustFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
`HTTP ${response.status}: ${response.statusText}`
);
}
// Parse JSON
const data = await response.json();
return { data, error: null };
} catch (error) {
// Network errors, JSON parsing errors, etc.
console.error('Fetch error:', error);
return { data: null, error: error.message };
}
}
// Usage
const { data, error } = await robustFetch('/api/users');
if (error) {
console.error('Failed to fetch users:', error);
} else {
console.log('Users:', data);
}
Building an API Client
Create a reusable API client class:
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = null;
}
setToken(token) {
this.token = token;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (this.token) {
headers.Authorization = `Bearer ${this.token}`;
}
const config = {
...options,
headers
};
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// Convenience methods
get(endpoint) {
return this.request(endpoint);
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
}
// Usage
const api = new APIClient('https://api.example.com');
api.setToken('your-token');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });
Query Parameters
Building URLs with Parameters
// Manual approach
const userId = 123;
const filter = 'active';
const url = `https://api.example.com/users?id=${userId}&status=${filter}`;
// Better: Using URLSearchParams
const params = new URLSearchParams({
id: userId,
status: filter,
page: 1,
limit: 10
});
const url = `https://api.example.com/users?${params}`;
// https://api.example.com/users?id=123&status=active&page=1&limit=10
// In fetch
const response = await fetch(url);
Pagination
Handling Paginated Responses
async function fetchAllPages(baseUrl) {
let allData = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}&limit=100`);
const data = await response.json();
allData = allData.concat(data.results);
hasMore = data.hasNextPage;
page++;
}
return allData;
}
Rate Limiting
Respecting Rate Limits
class RateLimitedClient {
constructor(requestsPerSecond = 10) {
this.queue = [];
this.processing = false;
this.interval = 1000 / requestsPerSecond;
}
async request(url, options) {
return new Promise((resolve, reject) => {
this.queue.push({ url, options, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
const { url, options, resolve, reject } = this.queue.shift();
try {
const response = await fetch(url, options);
const data = await response.json();
resolve(data);
} catch (error) {
reject(error);
}
setTimeout(() => {
this.processing = false;
this.processQueue();
}, this.interval);
}
}
Best Practices
- Use HTTPS - Always use secure connections
- Handle errors gracefully - Provide meaningful error messages
- Implement retries - Retry failed requests with exponential backoff
- Cache responses - Reduce unnecessary API calls
- Validate data - Check data before sending to API
- Use appropriate methods - GET for reading, POST for creating, etc.
- Version your API - Include version in URL or headers
- Document everything - Good documentation makes APIs usable
Conclusion
Working with REST APIs is a fundamental skill for modern web development. By understanding HTTP methods, proper error handling, authentication, and best practices, you can build robust applications that communicate effectively with backend services.
Remember to always handle errors gracefully, respect rate limits, and write clean, maintainable code that other developers can understand and use!