In this lesson, we will explore how to fetch and display todos from the backend for our Full-Stack Todo List Application. By the end of this lesson, you'll know how to retrieve data from a backend server and display it on your webpage, transforming your todo list from static placeholder data to dynamic, real-time information. This crucial step will ensure that your application remains up-to-date with any changes made on the server. It also lays the foundation for more complex interactions between the frontend and backend.
To achieve our goal, we will focus on two key functions: one for fetching todo items from the backend server, and another for creating HTML elements for these todos on the client side, complete with event listeners to handle user interactions. Mastering these functions will improve your ability to manage server-client data flow, simplifying debugging and future enhancements.
We have been using Axios
for all our HTTP requests so far, but in this lesson, we will introduce a new API called fetch
. Axios is a promise-based HTTP client with features like automatic JSON data transformation and easier handling of request and response interceptors. In contrast, the Fetch API is a built-in JavaScript function for requests that requires more manual handling of response data and lacks some of the advanced features offered by Axios. However, the Fetch API is widely supported and a good choice for simpler use cases. Mastering both Axios and Fetch will make you versatile in handling HTTP requests in any project.
We will be using the same GET
, POST
, PUT
, DELETE
, and PATCH
requests, and as such here is an overview. Knowing when to use each request method is important for effective interaction with backend services. Misusing these methods can lead to unexpected behavior and potential errors.
A GET
request retrieves data from a server endpoint. It's the simplest form of a fetch request. This method is typically used to request data from a given resource without altering it. Here's a basic example using the Fetch API:
JavaScript1const res = await fetch('/api/todos'); 2if (!res.ok) { 3 throw new Error(`HTTP error! status: ${res.status}`); 4} 5const data = await res.json();
A POST
request sends data to the server, typically to create a new resource. This method changes the state of the server, hence it is used when submitting form data or uploading a file. Here's an example of how to use a POST
request to add a new todo item:
JavaScript1const newTodo = { text: 'New Todo Item', completed: false }; 2const res = await fetch('/api/todos', { 3 method: 'POST', 4 headers: { 'Content-Type': 'application/json' }, 5 body: JSON.stringify(newTodo) 6}); 7if (!res.ok) { 8 throw new Error(`HTTP error! status: ${res.status}`); 9} 10const createdTodo = await res.json();
Including the Content-Type: application/json
header ensures that the server correctly interprets the JSON data format being sent in the request body. Without this header, the server may not correctly parse the JSON data, potentially leading to errors in processing the request.
A PUT
request updates an existing resource entirely. This method will replace the current representation of the target resource with the provided data. Here is an example using the Fetch API to update an existing todo item:
JavaScript1const updatedTodo = { text: 'Updated Todo Item', completed: true }; 2const res = await fetch(`/api/todos/${todoId}`, { 3 method: 'PUT', 4 headers: { 'Content-Type': 'application/json' }, 5 body: JSON.stringify(updatedTodo) 6}); 7if (!res.ok) { 8 throw new Error(`HTTP error! status: ${res.status}`); 9} 10const updatedData = await res.json();
A PATCH
request updates an existing resource partially. This is commonly used for modifying specific fields without replacing the entire resource. Below is an example to update only the completed
status of a todo item:
JavaScript1const res = await fetch(`/api/todos/${todo._id}`, { 2 method: 'PATCH', 3 headers: { 'Content-Type': 'application/json' }, 4 body: JSON.stringify({ completed: true }) 5}); 6if (!res.ok) { 7 throw new Error(`HTTP error! status: ${res.status}`); 8}
A DELETE
request removes a resource from the server. This method is irreversible, so it should be used with caution. Here's a basic example to delete a specified todo item:
JavaScript1const res = await fetch(`/api/todos/${todoId}`, { 2 method: 'DELETE' 3}); 4if (!res.ok) { 5 throw new Error(`HTTP error! status: ${res.status}`); 6}
Each of these fetch functions involves making a request to a specific URL, potentially including headers and a body, and then handling the server's response. Understanding these methods will enable you to perform a wide range of CRUD operations with your backend server.
Now that we have gone over the new fetch
API, it is time to practically implement it. The first step in our client-side part of the application is the retrieval of data from the backend database using an asynchronous function. This asynchronous function will incorporate error handling to ensure robustness in different network conditions. We will do this with the following function:
JavaScript1const todoList = document.getElementById('todos'); 2 3async function loadTodos() { 4 try { 5 const res = await fetch('/api/todos'); 6 if (!res.ok) { 7 throw new Error(`HTTP error! status: ${res.status}`); 8 } 9 const todos = await res.json(); 10 todoList.innerHTML = ''; 11 todos.forEach(todo => { 12 const li = createTodoElement(todo); 13 todoList.appendChild(li); 14 }); 15 } catch (err) { 16 console.error('Error fetching todos:', err); 17 } 18}
Here we use a GET
request to fetch the todo items, loop through each item, and create a frontend element by calling the createTodoElement
function, which will be explained in the next section. This method ensures that all fetched todos are displayed accurately and efficiently. By incorporating error handling, we can make the application more robust and user-friendly.
After successfully fetching the data, the next step is to dynamically create the todo items and display them. Generating HTML elements for each todo item ensures that they are interactive and responsive to user actions. Additionally, we need to ensure that the created elements can reflect the state changes in the backend, keeping the UI consistent. Below are the steps involved:
-
Create the List Item (
li
) and Checkbox Elements:JavaScript1function createTodoElement(todo) { 2 // Create a list item element 3 const li = document.createElement('li'); 4 5 // Create a checkbox element and set its checked state based on the todo's completed status 6 const checkbox = document.createElement('input'); 7 checkbox.type = 'checkbox'; 8 checkbox.checked = todo.completed; 9 10 // Apply strikethrough styling if the todo is completed 11 if (todo.completed) { 12 li.style.textDecoration = 'line-through'; 13 }
-
Add Event Listener to the Checkbox to Handle State Changes:
JavaScript1 // Add an event listener to the checkbox to handle state changes 2 checkbox.addEventListener('change', async () => { 3 try { 4 // Send a PATCH request to update the completed status on the server 5 await fetch(`/api/todos/${todo._id}`, { 6 method: 'PATCH', 7 headers: { 'Content-Type': 'application/json' }, 8 body: JSON.stringify({ completed: checkbox.checked }) 9 }); 10 11 // Update the list item's styling based on the new completed status 12 li.style.textDecoration = checkbox.checked ? 'line-through' : 'none'; 13 } catch (err) { 14 console.error('Error updating todo:', err); 15 // Revert the checkbox state in case of an error 16 checkbox.checked = !checkbox.checked; 17 } 18 });
-
Set attributes and Append Elements to the List Item:
JavaScript1 // Set the todo's unique identifier as a data attribute on the list item 2 li.dataset.id = todo._id; 3 4 // Append the checkbox and the todo text to the list item 5 li.appendChild(checkbox); 6 li.appendChild(document.createTextNode(todo.text)); 7 8 // Return the constructed list item element 9 return li; 10}
-
Complete Function Example:
JavaScript1function createTodoElement(todo) { 2 // Create a list item element 3 const li = document.createElement('li'); 4 5 // Create a checkbox element and set its checked state based on the todo's completed status 6 const checkbox = document.createElement('input'); 7 checkbox.type = 'checkbox'; 8 checkbox.checked = todo.completed; 9 10 // Apply strikethrough styling if the todo is completed 11 if (todo.completed) { 12 li.style.textDecoration = 'line-through'; 13 } 14 15 // Add an event listener to the checkbox to handle state changes 16 checkbox.addEventListener('change', async () => { 17 try { 18 // Send a PATCH request to update the completed status on the server 19 await fetch(`/api/todos/${todo._id}`, { 20 method: 'PATCH', 21 headers: { 'Content-Type': 'application/json' }, 22 body: JSON.stringify({ completed: checkbox.checked }) 23 }); 24 25 // Update the list item's styling based on the new completed status 26 li.style.textDecoration = checkbox.checked ? 'line-through' : 'none'; 27 } catch (err) { 28 console.error('Error updating todo:', err); 29 // Revert the checkbox state in case of an error 30 checkbox.checked = !checkbox.checked; 31 } 32 }); 33 34 // Set the todo's unique identifier as a data attribute on the list item 35 li.dataset.id = todo._id; 36 37 // Append the checkbox and the todo text to the list item 38 li.appendChild(checkbox); 39 li.appendChild(document.createTextNode(todo.text)); 40 41 // Return the constructed list item element 42 return li; 43}
The event listener attached to the checkbox listens for changes in the checkbox state, i.e., whether it is checked or unchecked. When the state changes, it triggers an asynchronous function to update the backend using a PATCH
request. This PATCH
request sends a fetch call to our backend server targeting the specific todo item by its unique identifier. The URL /api/todos/${todo._id}
includes the todo's ID, and the request method PATCH
is used to update only the completed
field of the todo item. The request includes headers to specify that the content type is JSON and a body containing the updated completed
status of the todo.
Using this approach, each todo item becomes an interactive part of the user interface. This approach improves the maintainability of our application and facilitates further enhancements in the future. This not only enhances user experience but also maintains synchronization with the backend database.
In this lesson, we explored how to fetch todos from a backend server and display them on the frontend using JavaScript. We explained the usage of the fetch
API for making HTTP requests and how it differs from Axios. The core of our lesson focused on two key functions: one for fetching data and handling responses, and another for dynamically creating and updating todo list items in the DOM. By loading the fetched items and creating client-sided elements with proper event listeners, we ensured that the todo list is both interactive and responsive to user actions. This foundational knowledge will greatly aid in building more complex features in future lessons. Happy coding!