Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 80

Frontend-Backend Integration: Data Flow and State Management

Recommended resources

Hello! Welcome back to our final capstone project.

In the last lesson, you did an excellent job building a fully interactive UI for our task manager. You now have a DashboardPage where you can add, view, update, and delete tasks. Currently, all this functionality is powered by React's local state, which means the data is temporary and exists only in the browser.

Today, we're going to bridge the gap between the frontend and the backend. The goal of this lesson is to integrate your React frontend with the Node.js API you built earlier. We'll replace the local state logic with real API calls to fetch data from and send data to your PostgreSQL database. This will transform your task manager from a UI prototype into a true full-stack application.

By the end of this lesson, creating a task in your app will mean adding a new row to your database, and seeing a list of tasks will mean you've fetched them live from your server.

Fetch and Display Data using API in React
This diagram illustrates our goal: the React app (client) will communicate through an API to our server to fetch, display, and manage data from the database.

Part 1: The Big Picture - How Frontend and Backend Talk

Before we write any code, let's solidify our understanding of how the client and server interact. Your React application (the client) runs in the user's browser, while your Node.js/Express application (the server) runs on a machine somewhere else (for now, on your local machine but on a different port).

They communicate using HTTP requests. When the user performs an action in the UI, React sends a specific type of request to a specific API endpoint on the server.

To get a clear visual and conceptual overview of this flow, let's watch the first few minutes of this excellent video from freeCodeCamp.org.

PERN Stack Course - Postgres, Express, React, and Node

This video, 'PERN Stack Course', provides a fantastic diagram and explanation of how a React frontend communicates with a Node.js/PostgreSQL backend.

Watch from the beginning to 03:31. Focus on the diagram that shows how clicking buttons in React triggers HTTP requests (POST, PUT, DELETE, GET) to the RESTful API, which then instructs the database.

As the video explains, our CRUD operations map directly to HTTP methods:

  • Create -> POST
  • Read -> GET
  • Update -> PUT or PATCH
  • Delete -> DELETE

Part 2: Setting Up the Connection

There's a small but critical hurdle we need to overcome first. Your React development server (e.g., http://localhost:5173) and your Node.js API server (e.g., http://localhost:8080) are running on different "origins" (the combination of protocol, host, and port). For security reasons, browsers block HTTP requests between different origins by default. This is known as the Cross-Origin Resource Sharing (CORS) policy.

While the proper production fix is to configure CORS on the backend server, most modern frontend development tools provide a simple proxy solution for development. We'll tell our React dev server: "Any request you don't recognize, just forward it to our backend server."

This is a key piece of the development setup for any full-stack application.

Development Environment Setup

The article 'How To Develop and Build PERN Stack' clearly explains this development setup and how to configure the proxy.

Read the section titled 'Development Environment Setup'. It explains why a proxy is needed and shows how to add it to your React app's package.json. However, since you're using Vite, the configuration is slightly different. We'll place it in vite.config.js instead.

To implement this in your Vite project, open your vite.config.js file and add a server.proxy configuration. It should look like this:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // String shorthand: '/api' -> 'http://localhost:8080/api'
      '/api': {
        target: 'http://localhost:8080', // Your backend server
        changeOrigin: true,
      }
    }
  }
})

Make sure your backend server port matches 8080 (or whatever you configured). After adding this, you must restart your Vite development server for the changes to take effect. Now, when you make a request to /api/tasks from your React code, Vite will automatically proxy it to http://localhost:8080/api/tasks, bypassing any CORS issues.


Part 3: Creating a Professional Data Service

To keep our code clean and organized, we won't put API call logic directly inside our components. Instead, we'll create a dedicated "data service" file. This is a common best practice that makes your code more modular, maintainable, and easier to test.

This service will be responsible for all communication with the backend. We'll use a library called Axios to make our HTTP requests. It's a very popular alternative to fetch with a simpler API, especially for POST and PUT requests.

First, install Axios in your frontend project:

# In your frontend project directory
npm install axios

Next, let's create our service file.

Create Data Service

The article 'React, Node.js, Express and PostgreSQL CRUD app' demonstrates this pattern perfectly by creating a tutorial.service.js file.

Read the section '4.4 Create Data Service'. It shows how to create a central file for Axios requests. We will adapt this to create our own task.service.js.

Now, create a new folder src/services and a file inside it named task.service.js. Based on the pattern in the article, your service file will look like this:

// src/services/task.service.js
import axios from "axios";

const apiClient = axios.create({
  baseURL: "/api", // Thanks to the proxy, this will be forwarded
  headers: {
    "Content-type": "application/json",
  },
});

// For our Task Manager, we'll implement these functions
const getAll = () => {
  return apiClient.get("/tasks");
};

const create = (data) => {
  return apiClient.post("/tasks", data);
};

const update = (id, data) => {
  return apiClient.put(`/tasks/${id}`, data);
};

const remove = (id) => {
  return apiClient.delete(`/tasks/${id}`);
};

const taskService = {
  getAll,
  create,
  update,
  remove,
};

export default taskService;

This service now provides a clean, reusable API for your components to interact with the backend.


Part 4: Wiring Up the CRUD Operations

Now for the main event! We'll modify your DashboardPage.jsx and TaskItem.jsx to use this new service. We will follow a detailed video walkthrough that implements the exact same integration.

Build a Full Stack CRUD App using React Tailwind Node PostgreSQL | Best practice & Industry standard

This video, 'Build a Full Stack CRUD App', is a fantastic end-to-end guide. We will use specific segments to see how to connect a React frontend to a Node backend for each CRUD operation. The video creator puts the Axios calls directly in the components, but you will call your new taskService functions instead.

Watch the following segments to see the patterns for fetching, creating, updating, and deleting data. Remember to adapt the code to use your taskService.

Step 4.1: Fetching All Tasks (Read)

First, let's fetch and display tasks from the database when the page loads.

Video Guide: Watch from 01:17:57 to 01:22:12.

In DashboardPage.jsx, import your taskService and use the useEffect hook to call taskService.getAll().

// At the top of DashboardPage.jsx
import React, { useState, useEffect } from 'react';
import taskService from '../services/task.service';
// ... other imports

// Inside the DashboardPage component
const [tasks, setTasks] = useState([]);

useEffect(() => {
  // Define a function to fetch tasks
  const fetchTasks = async () => {
    try {
      const response = await taskService.getAll();
      setTasks(response.data); // Axios wraps the response in a `data` object
    } catch (error) {
      console.error("Failed to fetch tasks:", error);
      // Here you could set an error state to show a message to the user
    }
  };

  fetchTasks();
}, []); // The empty array ensures this runs only once when the component mounts

With this change, your page should now load and display the tasks currently in your database!

Step 4.2: Creating New Tasks (Create)

Now let's connect the "Add Task" form.

Video Guide: Watch from 01:25:45 to 01:31:17.

Modify your addTask function to call the service. A key question is how to update the UI afterwards. A simple and robust way is to re-fetch the entire list.

// A function inside DashboardPage.jsx
const addTask = async (text) => {
  if (!text.trim()) return;

  try {
    const newTask = { text, completed: false }; // The backend will handle the ID
    await taskService.create(newTask);
    
    // Simple solution: re-fetch all tasks to update the list
    const response = await taskService.getAll();
    setTasks(response.data);

  } catch (error) {
    console.error("Failed to create task:", error);
  }
};

Step 4.3: Deleting and Updating Tasks (Delete & Update)

The logic for delete and update is very similar.

Video Guide: Watch from 01:31:17 to 01:38:28.

Update your deleteTask and toggleComplete functions to call the service and then re-fetch the list.

// A function inside DashboardPage.jsx
const deleteTask = async (id) => {
  try {
    await taskService.remove(id);
    // Re-fetch to update the list
    const response = await taskService.getAll();
    setTasks(response.data);
  } catch (error) {
    console.error("Failed to delete task:", error);
  }
};

const toggleComplete = async (id, currentStatus) => {
  try {
    await taskService.update(id, { completed: !currentStatus });
    // Re-fetch to update the list
    const response = await taskService.getAll();
    setTasks(response.data);
  } catch (error) {
    console.error("Failed to update task:", error);
  }
};

Remember to pass the required arguments (id, currentStatus) from your TaskItem component.

Test your understanding!

In the toggleComplete function above, we re-fetch the entire list of tasks from the server after a single task is updated. What is a potential performance downside to this approach, and can you think of an alternative way to update the local tasks state without making another GET request?

Show answer

Downside: Re-fetching the entire list can be inefficient, especially if the list is very long. It involves an extra network round-trip and forces React to re-process and re-render the entire list, even though only one item changed.

Alternative: A more performant approach is to update the local state manually. The taskService.update call could return the updated task object from the API. You could then use this object to update the local tasks array with .map(), just like we did in the previous lesson.

const toggleComplete = async (id, currentStatus) => {
  try {
    const response = await taskService.update(id, { completed: !currentStatus });
    const updatedTask = response.data; // Assuming API returns the updated task
    
    setTasks(tasks.map(task => 
      task.id === id ? updatedTask : task
    ));
  } catch (error) {
    console.error("Failed to update task:", error);
  }
};

This avoids the extra network call and is often called an "optimistic update" if you update the UI before the API call completes.


Part 5: Improving State Synchronization

Re-fetching the list after every change works, but it's repetitive. A cleaner pattern is to centralize the data fetching logic. The video you've been watching has a great "bonus" section that demonstrates this.

The idea is to lift the state and the fetching logic to a common parent component (DashboardPage is fine for now, but in larger apps it might be App.jsx). Then, you pass the fetchTasks function down as a prop to any component that needs to trigger a refresh.

Build a Full Stack CRUD App using React Tailwind Node PostgreSQL | Best practice & Industry standard

Let's watch the final part of the video to see a professional pattern for synchronizing state after CRUD operations.

Watch from 01:38:28 to 01:41:33. The video author lifts the state and fetching logic up to App.jsx. You can apply this same principle within your DashboardPage.jsx by passing your fetchTasks function as a prop to your form and task list components. This eliminates the need to write the fetching code inside every single handler (addTask, deleteTask, etc.).

By adapting this pattern, your handler functions become much cleaner:

// Centralized fetch function in DashboardPage
const fetchTasks = async () => { /* ... as defined before ... */ };

// Cleaner addTask function
const addTask = async (text) => {
  try {
    await taskService.create({ text, completed: false });
    fetchTasks(); // Just call the central function
  } catch (error) {
    console.error("Failed to create task:", error);
  }
};

// Cleaner deleteTask function
const deleteTask = async (id) => {
  try {
    await taskService.remove(id);
    fetchTasks(); // Just call the central function
  } catch (error) {
    console.error("Failed to delete task:", error);
  }
};

This is a much more scalable and maintainable way to manage data synchronization.


Conclusion

Fantastic work! You have successfully connected your React frontend to your Node.js backend. Your task manager is now a fully functional full-stack application that persists data in a PostgreSQL database.

Key Takeaways:

  • You learned about the client-server communication model and how HTTP methods map to CRUD operations.
  • You solved the cross-origin (CORS) problem for local development by configuring a proxy in Vite.
  • You created a professional data service using Axios to centralize API calls, separating concerns and making your components cleaner.
  • You integrated all four CRUD operations, replacing local state logic with asynchronous API calls.
  • You explored and implemented strategies for state synchronization to ensure the UI always reflects the database state.

Preview of the Next Lesson:
Our application is functional, but it's not secure or personalized. Currently, all users see the same list of tasks. In our next lesson, we will tackle authentication. You will learn how to implement protected routes and UI elements based on user authentication status, ensuring that users can only see and manage their own tasks.