Working with REST APIs
What is a REST API?
REST stands for Representational State Transfer. It is an architectural style for building web services that allow different software applications to communicate over the internet using standard HTTP methods. A REST API exposes resources -- such as users, posts, products, or orders -- at specific URLs called endpoints. Each resource can be created, read, updated, or deleted using HTTP methods, forming the foundation of modern web applications.
REST APIs follow a set of principles that make them predictable and easy to work with. They are stateless, meaning each request contains all the information needed to process it. They use uniform interfaces, meaning resources are identified by URLs and manipulated through standard HTTP methods. They return representations of resources, typically in JSON format, rather than the resources themselves.
REST Principles and Resources
In REST, everything revolves around resources. A resource is any piece of data your API manages. Resources are organized into collections and individual items. Consider a blog API: the collection of all posts lives at /posts, while a single post lives at /posts/1. This URL structure is called a resource path, and it should be intuitive and consistent.
Key REST principles include:
- Client-Server Separation -- The frontend and backend are independent. The client handles the user interface, while the server handles data storage and business logic.
- Statelessness -- Each request from client to server must contain all information needed to understand and process that request. The server does not store any client context between requests.
- Uniform Interface -- Resources are identified by URLs and manipulated using HTTP methods. Responses include enough information for the client to modify or delete the resource.
- Resource-Based URLs -- URLs should represent nouns, not verbs. Use
/usersinstead of/getUsers. Use/posts/5instead of/getPostById?id=5.
HTTP Methods for CRUD Operations
REST APIs map the four fundamental data operations -- Create, Read, Update, and Delete (CRUD) -- to specific HTTP methods. Understanding these mappings is essential for working with any API.
- GET -- Retrieves data from the server. Should never modify data. Used for reading resources.
- POST -- Creates a new resource on the server. Sends data in the request body.
- PUT -- Replaces an entire resource with new data. Requires sending the complete updated resource.
- PATCH -- Partially updates a resource. Only sends the fields that need to change.
- DELETE -- Removes a resource from the server.
Example: CRUD Operations Overview
// REST API endpoint patterns for a "posts" resource:
//
// GET /posts -- Retrieve all posts (collection)
// GET /posts/1 -- Retrieve a single post (item)
// POST /posts -- Create a new post
// PUT /posts/1 -- Replace post with ID 1 entirely
// PATCH /posts/1 -- Update specific fields of post 1
// DELETE /posts/1 -- Delete post with ID 1
HTTP Status Codes
Every API response includes a status code that tells you what happened with your request. Status codes are grouped into ranges, each indicating a category of response. Knowing these codes helps you handle API responses correctly in your application.
- 2xx -- Success: The request was successful.
200 OK-- Standard success response for GET, PUT, and PATCH requests.201 Created-- A new resource was successfully created (typically after POST).204 No Content-- Success with no response body (typically after DELETE).
- 4xx -- Client Error: Something was wrong with your request.
400 Bad Request-- The request data was invalid or malformed.401 Unauthorized-- Authentication is required but was not provided.403 Forbidden-- You are authenticated but lack permission for this action.404 Not Found-- The requested resource does not exist.429 Too Many Requests-- You have exceeded the rate limit.
- 5xx -- Server Error: Something went wrong on the server side.
500 Internal Server Error-- A generic server error occurred.503 Service Unavailable-- The server is temporarily down or overloaded.
Making GET Requests with Fetch
The fetch() function is the modern, built-in way to make HTTP requests in JavaScript. It returns a Promise that resolves to the Response object. Let us start with the simplest operation -- fetching data from a public API. We will use JSONPlaceholder, a free REST API for testing and prototyping.
Example: Fetching a List of Users
// Fetch all users from JSONPlaceholder API
async function fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// Check if the response was successful
if (!response.ok) {
throw new Error('Failed to fetch users: ' + response.status);
}
// Parse the JSON response body
const users = await response.json();
console.log('Total users:', users.length);
console.log('First user:', users[0].name);
return users;
}
// Call the function
fetchUsers().then(users => {
users.forEach(user => {
console.log(user.name + ' - ' + user.email);
});
});
response.ok property is true when the status code is in the 200-299 range. Always check this before parsing the response body, because fetch() only rejects the Promise on network failures, not on HTTP error status codes like 404 or 500.Fetching a Single Resource
To fetch a specific resource, append its ID to the endpoint URL. This is the standard REST pattern for retrieving individual items from a collection.
Example: Fetching a Single Post by ID
async function fetchPost(postId) {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/' + postId
);
if (!response.ok) {
if (response.status === 404) {
console.error('Post not found with ID:', postId);
return null;
}
throw new Error('HTTP Error: ' + response.status);
}
const post = await response.json();
console.log('Title:', post.title);
console.log('Body:', post.body);
return post;
}
// Fetch post with ID 1
fetchPost(1);
// Fetch a non-existent post
fetchPost(9999); // Will log "Post not found"
Making POST Requests -- Creating Data
To create a new resource, send a POST request with the data in the request body. You must specify the Content-Type header so the server knows how to parse the body, and you must serialize your data with JSON.stringify().
Example: Creating a New Post
async function createPost(title, body, userId) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title,
body: body,
userId: userId,
}),
});
if (!response.ok) {
throw new Error('Failed to create post: ' + response.status);
}
const newPost = await response.json();
console.log('Created post with ID:', newPost.id);
return newPost;
}
// Create a new post
createPost(
'Understanding REST APIs',
'REST APIs are the backbone of modern web applications...',
1
);
Making PUT and PATCH Requests -- Updating Data
PUT replaces the entire resource, while PATCH updates only the specified fields. Choose the right method based on whether you are sending a complete replacement or a partial update.
Example: Updating a Post with PUT and PATCH
// PUT -- Replace the entire post
async function replacePost(postId, postData) {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/' + postId,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
}
);
if (!response.ok) {
throw new Error('Failed to replace post: ' + response.status);
}
return await response.json();
}
// PATCH -- Update only the title
async function updatePostTitle(postId, newTitle) {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/' + postId,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title: newTitle }),
}
);
if (!response.ok) {
throw new Error('Failed to update post: ' + response.status);
}
return await response.json();
}
// Replace entire post
replacePost(1, {
title: 'Complete Replacement',
body: 'This is a completely new body.',
userId: 1,
});
// Update only the title
updatePostTitle(1, 'Updated Title Only');
Making DELETE Requests
DELETE requests remove a resource from the server. They typically do not require a request body. A successful deletion usually returns a 200 or 204 status code.
Example: Deleting a Post
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);
}
console.log('Post ' + postId + ' deleted successfully');
return true;
}
deletePost(1);
Query Parameters and URL Construction
Query parameters allow you to filter, sort, search, and paginate API results. They are appended to the URL after a ? character. The URLSearchParams class provides a clean, safe way to build query strings without worrying about special character encoding.
Example: Building URLs with URLSearchParams
// Manual approach -- error-prone with special characters
const unsafeUrl = 'https://api.example.com/search?q=hello world&page=1';
// Safe approach -- URLSearchParams handles encoding
function buildApiUrl(baseUrl, params) {
const url = new URL(baseUrl);
const searchParams = new URLSearchParams(params);
url.search = searchParams.toString();
return url.toString();
}
// Build a search URL with multiple parameters
const searchUrl = buildApiUrl('https://api.example.com/products', {
q: 'laptop case',
category: 'electronics',
min_price: '25',
max_price: '100',
sort: 'price_asc',
page: '1',
});
console.log(searchUrl);
// https://api.example.com/products?q=laptop+case&category=electronics&min_price=25&max_price=100&sort=price_asc&page=1
// Fetch with query parameters
async function searchProducts(query, page) {
const params = new URLSearchParams({
q: query,
page: page.toString(),
limit: '20',
});
const response = await fetch(
'https://api.example.com/products?' + params.toString()
);
if (!response.ok) {
throw new Error('Search failed: ' + response.status);
}
return await response.json();
}
URLSearchParams or the URL constructor to build query strings. They automatically handle special characters like spaces, ampersands, and question marks that would break a manually constructed URL. This prevents subtle bugs that are difficult to debug.Filtering and Querying with JSONPlaceholder
Many REST APIs support filtering via query parameters. JSONPlaceholder supports filtering by any field on the resource. This is a common pattern you will encounter in real APIs.
Example: Filtering Posts by User ID
// Get all posts by a specific user
async function fetchPostsByUser(userId) {
const params = new URLSearchParams({ userId: userId.toString() });
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts?' + params.toString()
);
if (!response.ok) {
throw new Error('Failed to fetch posts: ' + response.status);
}
const posts = await response.json();
console.log('User ' + userId + ' has ' + posts.length + ' posts');
return posts;
}
// Get comments for a specific post
async function fetchCommentsForPost(postId) {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/' + postId + '/comments'
);
if (!response.ok) {
throw new Error('Failed to fetch comments: ' + response.status);
}
return await response.json();
}
// Nested resource: get comments for post 1
fetchCommentsForPost(1).then(comments => {
comments.forEach(comment => {
console.log(comment.name + ': ' + comment.body.substring(0, 50) + '...');
});
});
Pagination Handling
Most APIs do not return all records at once. Instead, they split results into pages. Pagination is critical for performance -- imagine loading 10,000 products at once. Common pagination patterns include page-based, offset-based, and cursor-based pagination.
Example: Page-Based Pagination
// Pagination controller
class PaginatedFetcher {
constructor(baseUrl, pageSize) {
this.baseUrl = baseUrl;
this.pageSize = pageSize || 10;
this.currentPage = 1;
this.totalPages = null;
this.isLoading = false;
}
async fetchPage(page) {
if (this.isLoading) {
console.log('Already loading, please wait...');
return null;
}
this.isLoading = true;
try {
const params = new URLSearchParams({
_page: page.toString(),
_limit: this.pageSize.toString(),
});
const response = await fetch(
this.baseUrl + '?' + params.toString()
);
if (!response.ok) {
throw new Error('Failed to fetch page: ' + response.status);
}
// Many APIs include total count in headers
const totalCount = response.headers.get('X-Total-Count');
if (totalCount) {
this.totalPages = Math.ceil(
parseInt(totalCount) / this.pageSize
);
}
const data = await response.json();
this.currentPage = page;
return {
data: data,
page: this.currentPage,
totalPages: this.totalPages,
hasNext: this.totalPages ? page < this.totalPages : data.length === this.pageSize,
hasPrevious: page > 1,
};
} finally {
this.isLoading = false;
}
}
async nextPage() {
return await this.fetchPage(this.currentPage + 1);
}
async previousPage() {
if (this.currentPage > 1) {
return await this.fetchPage(this.currentPage - 1);
}
return null;
}
}
// Usage
const postFetcher = new PaginatedFetcher(
'https://jsonplaceholder.typicode.com/posts',
5
);
// Fetch the first page
postFetcher.fetchPage(1).then(result => {
console.log('Page', result.page, 'of', result.totalPages);
console.log('Posts:', result.data.length);
console.log('Has next page:', result.hasNext);
});
Fetching All Pages Automatically
Sometimes you need to collect data from all pages. This requires making sequential requests until no more data is available. Be careful with this pattern -- always respect rate limits and consider if you truly need all the data at once.
Example: Collecting All Pages
async function fetchAllPages(baseUrl, pageSize) {
const allData = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const params = new URLSearchParams({
_page: page.toString(),
_limit: pageSize.toString(),
});
const response = await fetch(baseUrl + '?' + params.toString());
if (!response.ok) {
throw new Error('Failed on page ' + page + ': ' + response.status);
}
const data = await response.json();
allData.push(...data);
// If we got fewer items than the page size, we are on the last page
hasMore = data.length === pageSize;
page++;
console.log('Fetched page ' + (page - 1) + ', total items: ' + allData.length);
}
return allData;
}
// Fetch all posts in batches of 10
fetchAllPages('https://jsonplaceholder.typicode.com/posts', 10)
.then(allPosts => {
console.log('Total posts fetched:', allPosts.length);
});
Authentication: API Keys and Bearer Tokens
Most real-world APIs require authentication to identify who is making requests. The two most common methods are API keys (sent as a query parameter or header) and Bearer tokens (sent in the Authorization header). Understanding both patterns is essential for working with production APIs.
Example: Authentication Methods
// Method 1: API Key in query parameter
async function fetchWithApiKey(endpoint) {
const params = new URLSearchParams({
api_key: 'your_api_key_here',
});
const response = await fetch(endpoint + '?' + params.toString());
return await response.json();
}
// Method 2: API Key in custom header
async function fetchWithHeaderKey(endpoint) {
const response = await fetch(endpoint, {
headers: {
'X-API-Key': 'your_api_key_here',
},
});
return await response.json();
}
// Method 3: Bearer Token in Authorization header
async function fetchWithBearerToken(endpoint, token) {
const response = await fetch(endpoint, {
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
console.error('Token expired or invalid. Please log in again.');
// Redirect to login or refresh the token
return null;
}
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
return await response.json();
}
// Method 4: Storing and refreshing tokens
class AuthenticatedClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.accessToken = null;
this.refreshToken = null;
}
async login(username, password) {
const response = await fetch(this.baseUrl + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Login failed: ' + response.status);
}
const data = await response.json();
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
return data;
}
async request(endpoint, options) {
if (!options) options = {};
if (!options.headers) options.headers = {};
options.headers['Authorization'] = 'Bearer ' + this.accessToken;
const response = await fetch(this.baseUrl + endpoint, options);
// If token expired, try refreshing
if (response.status === 401 && this.refreshToken) {
await this.refreshAccessToken();
options.headers['Authorization'] = 'Bearer ' + this.accessToken;
return await fetch(this.baseUrl + endpoint, options);
}
return response;
}
async refreshAccessToken() {
const response = await fetch(this.baseUrl + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken }),
});
if (!response.ok) {
throw new Error('Token refresh failed. Please log in again.');
}
const data = await response.json();
this.accessToken = data.accessToken;
}
}
Rate Limiting
APIs limit how many requests you can make in a given time period to prevent abuse and ensure fair usage. When you hit a rate limit, the API returns a 429 Too Many Requests status. The response headers often tell you when you can retry. Handling rate limits gracefully is essential for building reliable applications.
Example: Rate Limit Handler
class RateLimitedFetcher {
constructor(requestsPerSecond) {
this.minInterval = 1000 / requestsPerSecond;
this.lastRequestTime = 0;
this.queue = [];
this.processing = false;
}
async fetch(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;
while (this.queue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.minInterval) {
await new Promise(resolve =>
setTimeout(resolve, this.minInterval - timeSinceLastRequest)
);
}
const { url, options, resolve, reject } = this.queue.shift();
try {
this.lastRequestTime = Date.now();
const response = await fetch(url, options);
// Handle rate limit response
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitTime = retryAfter
? parseInt(retryAfter) * 1000
: 5000;
console.log('Rate limited. Waiting ' + waitTime + 'ms...');
await new Promise(r => setTimeout(r, waitTime));
// Retry the request
this.lastRequestTime = Date.now();
const retryResponse = await fetch(url, options);
resolve(retryResponse);
} else {
resolve(response);
}
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
// Allow maximum 2 requests per second
const limiter = new RateLimitedFetcher(2);
// These requests will be automatically spaced out
async function fetchMultipleUsers() {
const userIds = [1, 2, 3, 4, 5];
const users = [];
for (const id of userIds) {
const response = await limiter.fetch(
'https://jsonplaceholder.typicode.com/users/' + id
);
const user = await response.json();
users.push(user);
console.log('Fetched user:', user.name);
}
return users;
}
Loading States and UI Feedback
Users need visual feedback when data is being loaded from an API. A well-designed application shows loading indicators, handles empty states, and displays errors clearly. This prevents confusion and makes the application feel responsive.
Example: Managing Loading States
class UIStateManager {
constructor(containerId) {
this.container = document.getElementById(containerId);
}
showLoading(message) {
this.container.innerHTML =
'<div class="loading-state">' +
' <div class="spinner"></div>' +
' <p>' + (message || 'Loading...') + '</p>' +
'</div>';
}
showError(message, retryCallback) {
this.container.innerHTML =
'<div class="error-state">' +
' <p class="error-message">' + message + '</p>' +
' <button class="retry-button">Try Again</button>' +
'</div>';
if (retryCallback) {
this.container
.querySelector('.retry-button')
.addEventListener('click', retryCallback);
}
}
showEmpty(message) {
this.container.innerHTML =
'<div class="empty-state">' +
' <p>' + (message || 'No data available.') + '</p>' +
'</div>';
}
showContent(html) {
this.container.innerHTML = html;
}
}
// Usage with API call
async function loadUserList() {
const ui = new UIStateManager('user-list');
ui.showLoading('Loading users...');
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users'
);
if (!response.ok) {
throw new Error('Server returned ' + response.status);
}
const users = await response.json();
if (users.length === 0) {
ui.showEmpty('No users found.');
return;
}
let html = '<ul class="user-list">';
users.forEach(user => {
html += '<li class="user-card">';
html += ' <h3>' + user.name + '</h3>';
html += ' <p>' + user.email + '</p>';
html += ' <p>' + user.company.name + '</p>';
html += '</li>';
});
html += '</ul>';
ui.showContent(html);
} catch (error) {
ui.showError(
'Failed to load users: ' + error.message,
loadUserList
);
}
}
Building a Complete API Client
For any real project, you should create a reusable API client class that handles common tasks like base URL management, default headers, error handling, and response parsing. This avoids repeating the same boilerplate code in every API call.
Example: Reusable API Client Class
class ApiClient {
constructor(baseUrl, defaultHeaders) {
this.baseUrl = baseUrl;
this.defaultHeaders = Object.assign(
{ 'Content-Type': 'application/json' },
defaultHeaders || {}
);
}
async request(endpoint, options) {
const url = this.baseUrl + endpoint;
const config = {
headers: Object.assign({}, this.defaultHeaders, options.headers || {}),
method: options.method || 'GET',
};
if (options.body) {
config.body = JSON.stringify(options.body);
}
if (options.params) {
const searchParams = new URLSearchParams(options.params);
const fullUrl = url + '?' + searchParams.toString();
const response = await fetch(fullUrl, config);
return await this.handleResponse(response);
}
const response = await fetch(url, config);
return await this.handleResponse(response);
}
async handleResponse(response) {
if (!response.ok) {
const errorBody = await response.text();
let errorMessage;
try {
const errorJson = JSON.parse(errorBody);
errorMessage = errorJson.message || errorBody;
} catch (e) {
errorMessage = errorBody;
}
const error = new Error(errorMessage);
error.status = response.status;
error.statusText = response.statusText;
throw error;
}
// Handle 204 No Content
if (response.status === 204) {
return null;
}
return await response.json();
}
async get(endpoint, params) {
return await this.request(endpoint, { method: 'GET', params: params });
}
async post(endpoint, body) {
return await this.request(endpoint, { method: 'POST', body: body });
}
async put(endpoint, body) {
return await this.request(endpoint, { method: 'PUT', body: body });
}
async patch(endpoint, body) {
return await this.request(endpoint, { method: 'PATCH', body: body });
}
async delete(endpoint) {
return await this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');
// GET all posts
api.get('/posts').then(posts => {
console.log('Posts:', posts.length);
});
// GET posts with query parameters
api.get('/posts', { userId: '1' }).then(posts => {
console.log('User 1 posts:', posts.length);
});
// POST a new post
api.post('/posts', {
title: 'New Post',
body: 'This is the content.',
userId: 1,
}).then(post => {
console.log('Created:', post.id);
});
// DELETE a post
api.delete('/posts/1').then(() => {
console.log('Post deleted');
});
Real-World Data Display: User List Application
Let us build a practical example that fetches users from the JSONPlaceholder API and displays them in a dynamic, interactive list with search filtering. This demonstrates how to combine API calls with DOM manipulation to build a real feature.
Example: Interactive User List with Search
class UserListApp {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.users = [];
this.filteredUsers = [];
this.api = new ApiClient('https://jsonplaceholder.typicode.com');
}
async initialize() {
this.renderSearchBar();
await this.loadUsers();
}
renderSearchBar() {
const searchHtml =
'<div class="search-container">' +
' <input type="text" id="user-search"' +
' placeholder="Search users by name or email...">' +
'</div>' +
'<div id="user-results"></div>';
this.container.innerHTML = searchHtml;
document.getElementById('user-search')
.addEventListener('input', (e) => {
this.filterUsers(e.target.value);
});
}
async loadUsers() {
const resultsDiv = document.getElementById('user-results');
resultsDiv.innerHTML = '<p>Loading users...</p>';
try {
this.users = await this.api.get('/users');
this.filteredUsers = this.users;
this.renderUsers();
} catch (error) {
resultsDiv.innerHTML =
'<p class="error">Failed to load users: ' +
error.message + '</p>';
}
}
filterUsers(query) {
const lowerQuery = query.toLowerCase();
this.filteredUsers = this.users.filter(user => {
return (
user.name.toLowerCase().includes(lowerQuery) ||
user.email.toLowerCase().includes(lowerQuery) ||
user.company.name.toLowerCase().includes(lowerQuery)
);
});
this.renderUsers();
}
renderUsers() {
const resultsDiv = document.getElementById('user-results');
if (this.filteredUsers.length === 0) {
resultsDiv.innerHTML = '<p>No users match your search.</p>';
return;
}
let html = '<div class="user-grid">';
this.filteredUsers.forEach(user => {
html += '<div class="user-card" data-user-id="' + user.id + '">';
html += ' <h3>' + user.name + '</h3>';
html += ' <p class="email">' + user.email + '</p>';
html += ' <p class="company">' + user.company.name + '</p>';
html += ' <p class="city">' + user.address.city + '</p>';
html += ' <button onclick="app.viewUserPosts(' + user.id + ')">';
html += ' View Posts';
html += ' </button>';
html += '</div>';
});
html += '</div>';
resultsDiv.innerHTML = html;
}
async viewUserPosts(userId) {
const user = this.users.find(u => u.id === userId);
try {
const posts = await this.api.get('/posts', {
userId: userId.toString()
});
let html = '<button onclick="app.renderUsers()">Back to Users</button>';
html += '<h2>Posts by ' + user.name + '</h2>';
posts.forEach(post => {
html += '<div class="post-card">';
html += ' <h4>' + post.title + '</h4>';
html += ' <p>' + post.body + '</p>';
html += '</div>';
});
document.getElementById('user-results').innerHTML = html;
} catch (error) {
console.error('Failed to load posts:', error);
}
}
}
// Initialize the application
const app = new UserListApp('app');
app.initialize();
Post Management: Full CRUD Interface
Let us build a complete post management interface that demonstrates all four CRUD operations together. This represents a realistic admin panel where you can create, view, edit, and delete resources through a REST API.
Example: Complete Post Manager
class PostManager {
constructor(api) {
this.api = api;
this.posts = [];
}
async loadPosts(page, limit) {
const params = {
_page: (page || 1).toString(),
_limit: (limit || 10).toString(),
};
this.posts = await this.api.get('/posts', params);
return this.posts;
}
async createPost(title, body, userId) {
const newPost = await this.api.post('/posts', {
title: title,
body: body,
userId: userId,
});
this.posts.unshift(newPost);
console.log('Post created with ID:', newPost.id);
return newPost;
}
async updatePost(postId, updates) {
const updatedPost = await this.api.patch(
'/posts/' + postId,
updates
);
const index = this.posts.findIndex(p => p.id === postId);
if (index !== -1) {
this.posts[index] = Object.assign(this.posts[index], updatedPost);
}
console.log('Post ' + postId + ' updated');
return updatedPost;
}
async deletePost(postId) {
await this.api.delete('/posts/' + postId);
this.posts = this.posts.filter(p => p.id !== postId);
console.log('Post ' + postId + ' deleted');
}
async getPostWithComments(postId) {
const post = await this.api.get('/posts/' + postId);
const comments = await this.api.get(
'/posts/' + postId + '/comments'
);
post.comments = comments;
return post;
}
}
// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');
const manager = new PostManager(api);
// Full CRUD workflow
async function demonstrateCrud() {
// Read -- load first page of posts
const posts = await manager.loadPosts(1, 5);
console.log('Loaded', posts.length, 'posts');
// Create -- add a new post
const newPost = await manager.createPost(
'My New Post',
'This is the content of my new post.',
1
);
// Update -- change the title
await manager.updatePost(newPost.id, {
title: 'Updated Title for My Post',
});
// Read with details -- get post and its comments
const detailed = await manager.getPostWithComments(1);
console.log('Post has', detailed.comments.length, 'comments');
// Delete -- remove the post
await manager.deletePost(newPost.id);
console.log('CRUD workflow complete');
}
demonstrateCrud();
Practice Exercise
Build a complete Todo application using the JSONPlaceholder API (https://jsonplaceholder.typicode.com/todos). Your application should include the following features: a reusable API client class with methods for GET, POST, PATCH, and DELETE operations; a function that loads the first 20 todos and displays them in the page with checkboxes showing their completion status; a form to create new todos using a POST request; the ability to toggle a todo's completion status using a PATCH request; a delete button on each todo that removes it with a DELETE request; pagination controls that let the user navigate through pages of 20 todos each; and a filter that allows viewing all todos, only completed todos, or only incomplete todos. Implement proper loading states with a spinner while data loads, error messages when requests fail, and a retry button that reloads the data. Use URLSearchParams for all query parameter construction. Test your application by creating, updating, and deleting multiple todos in sequence.