How To Build a CRUD App with React and RESTful APIs

Take meetings from “meh” to magical. Here’s how facilitators and participants can co-create a work session for the books.

Building a modern web application often comes down to mastering one core concept: managing data. Whether you are running an online store, a tracking tool, or a workspace manager, your software needs to allow users to create new files, read existing data, update information, and delete what they no longer need. In the development space, we call these CRUD operations.

When you combine the user interface power of React with the stability of a Node.js and Express backend via RESTful APIs, you gain a scalable foundation for full-stack software development. It gives you complete control over how data moves between your frontend layout and your backend database.

While starting out with full-stack development might feel like juggling too many pieces at once, breaking the process into clear, predictable steps makes it straightforward. This guide will walk you through building a clean, modern CRUD application from scratch.

What is a CRUD Application?

Before looking at the technical setup, let’s break down what a CRUD application actually does. The acronym stands for four foundational actions:

  • Create: Adding new entries (e.g., submitting a new form or writing a post).
  • Read: Fetching and viewing existing entries on your screen.
  • Update: Editing and changing an entry that already exists.
  • Delete: Removing an entry entirely from the system.

In a traditional setup, the frontend handles what the user sees, while the backend processes these requests and interacts with data storage. They talk to each other using HTTP requests (like GET, POST, PUT, and DELETE) sent over a network. This bridge is called a RESTful API.

Building a secure, efficient data pipeline is essential for custom software success. If you are aiming to create a highly optimized commercial platform rather than a learning tool, working with a dedicated team can save months of trial and error. At Charisol, we specialize in crafting custom digital products development that turn ideas into scalable web applications.

Prerequisites for This Tutorial

To follow along comfortably, you only need a couple of development tools installed on your local computer:

  • Node.js and npm: The runtime environment and package manager required to run JavaScript server-side and install libraries. You can grab the latest installer directly from the official Node.js website.
  • A Text Editor: Any modern code editor works perfectly. Visual Studio Code is highly recommended, but tools like Sublime Text work great too.
  • Basic JavaScript Knowledge: Familiarity with modern JavaScript (like arrow functions and async arrays) will help you understand the code lines easily.

Part 1: Setting Up the Backend Server

We will start by building our server-side environment. This backend acts as our data center, receiving requests from our React app, processing them, and returning the proper data packets.

Step 1: Initializing Your Project

First, create a new folder on your computer where your full-stack project will live. Open your system terminal, navigate into that folder, and run the following command to create a standard configuration file:

Bash

npm init -y

This generates a fresh package.json file inside your directory, ready to track our project extensions.

Step 2: Installing Dependencies

Our server relies on two lightweight JavaScript libraries to manage routing and security. Run the installation command below in your terminal:

Bash

npm install express cors
  • Express: A minimalist framework that turns Node.js into a robust web server.
  • Cors: A security utility that enables cross-origin resource sharing, allowing our frontend app (running on one port) to talk to our backend app (running on another port).

Step 3: Setting Up Your Entry File

Create a file named server.js directly in your main project folder. Open it up and paste the base foundation code below to spin up an active server process:

JavaScript

// server.js
const express = require('express');
const cors = require('cors');
const app = express();
const port = 5000;

app.use(cors());
app.use(express.json());

// Your API routes will be defined here

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});

This sets up a basic web application development server running on port 5000, configured to automatically parse incoming JSON payloads.

Step 4: Coding Your RESTful API Routes

Now, we need to create the actual communication paths for data modification. To keep this guide easy to follow, we will hold our tasks inside a temporary, local array inside the server’s memory. In production, this would be replaced by an actual persistent database system.

Add the following logic directly into your server.js file right above the app.listen line:

JavaScript

// server.js (continued)
let data = [
  { id: 1, title: 'Task 1', description: 'This is Task 1' },
  { id: 2, title: 'Task 2', description: 'This is Task 2' },
];

// READ ALL: Get every single task in our array
app.get('/api/tasks', (req, res) => {
  res.json(data);
});

// READ ONE: Locate an individual task by its specific ID
app.get('/api/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = data.find((item) => item.id === id);
  if (task) {
    res.json(task);
  } else {
    res.status(404).json({ message: 'Task not found' });
  }
});

// CREATE: Append a completely new task object to our collection
app.post('/api/tasks', (req, res) => {
  const { title, description } = req.body;
  const newTask = { id: data.length + 1, title, description };
  data.push(newTask);
  res.status(201).json(newTask);
});

// UPDATE: Find a specific task and modify its properties safely
app.put('/api/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const { title, description } = req.body;
  const task = data.find((item) => item.id === id);
  
  if (task) {
    task.title = title || task.title;
    task.description = description || task.description;
    res.json(task);
  } else {
    res.status(404).json({ message: 'Task not found' });
  }
});

// DELETE: Strip away a task using an array filtering method
app.delete('/api/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  data = data.filter((item) => item.id !== id);
  res.sendStatus(204);
});

Step 5: Launching Your Functional Backend

With your routes properly configured, drop back into your terminal environment and execute your file using Node:

Bash

node server.js

Your screen will print out a confirmation message verifying your operational server. You now have a complete, functional backend waiting to process requests at http://localhost:5000.

Part 2: Building the React Frontend Layout

Now that our data engine is running smoothly on port 5000, we can pivot to the client-side design. We will build an interface that renders our tasks, handles text submissions, and interacts with our server.

Step 1: Initializing the React Workspace

Open a clean second window inside your terminal application so that your backend server can continue running uninterrupted in the background. In this new window, run the following framework command to install a fresh structure:

Bash

npx create-react-app crud-app

This creates a complete subfolder named crud-app filled with all the baseline asset files needed for an interactive interface.

Step 2: Adding an HTTP Request Client

Move your active terminal line inside your new folder and install Axios, a highly reliable client for executing promise-based requests across networks:

Bash

cd crud-app
npm install axios

Step 3: Writing Individual Core Components

To keep our workspace clean, we will build specialized layout components inside separate component files. Head over to your src directory to set these up.

Displaying Existing Items with TaskList.js

Create a new file path named src/TaskList.js. This module uses a standard statehook combined with an initialization trigger to fetch all data rows immediately when the view mounts:

JavaScript

// TaskList.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const TaskList = ({ refreshTrigger, onSelectTask, onDeleteTask }) => {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    axios.get('http://localhost:5000/api/tasks')
      .then((response) => setTasks(response.data))
      .catch((error) => console.error('Error fetching list:', error));
  }, [refreshTrigger]);

  return (
    <div style={{ marginBottom: '20px' }}>
      <h2>Task List</h2>
      {tasks.length === 0 ? <p>No tasks available.</p> : (
        <ul>
          {tasks.map((task) => (
            <li key={task.id} style={{ marginBottom: '10px' }}>
              <strong>{task.title}</strong>
              <p>{task.description}</p>
              <button onClick={() => onSelectTask(task)}>Edit Task</button>
              <button onClick={() => onDeleteTask(task.id)} style={{ marginLeft: '10px' }}>Delete</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default TaskList;

Saving and Modifying with TaskForm.js

Create another file named src/TaskForm.js. This adaptive component functions seamlessly in two modes: it can accept clean input parameters to create a task, or accept preexisting data to execute a data update request.

JavaScript

// TaskForm.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const TaskForm = ({ selectedTask, onTransactionComplete }) => {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  useEffect(() => {
    if (selectedTask) {
      setTitle(selectedTask.title);
      setDescription(selectedTask.description);
    } else {
      setTitle('');
      setDescription('');
    }
  }, [selectedTask]);

  const handleSubmit = (e) => {
    e.preventDefault();
    const taskPayload = { title, description };

    if (selectedTask) {
      // Update Mode
      axios.put(`http://localhost:5000/api/tasks/${selectedTask.id}`, taskPayload)
        .then(() => {
          onTransactionComplete();
          setTitle('');
          setDescription('');
        })
        .catch((error) => console.error('Error updating task:', error));
    } else {
      // Create Mode
      axios.post('http://localhost:5000/api/tasks', taskPayload)
        .then(() => {
          onTransactionComplete();
          setTitle('');
          setDescription('');
        })
        .catch((error) => console.error('Error creating task:', error));
    }
  };

  return (
    <div style={{ marginBottom: '20px' }}>
      <h2>{selectedTask ? 'Edit Existing Task' : 'Add New Task'}</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label>Title: </label>
          <input 
            type="text" 
            value={title} 
            onChange={(e) => setTitle(e.target.value)} 
            required 
          />
        </div>
        <div style={{ marginTop: '10px' }}>
          <label>Description: </label>
          <textarea 
            value={description} 
            onChange={(e) => setDescription(e.target.value)} 
            required
          />
        </div>
        <div style={{ marginTop: '10px' }}>
          <button type="submit">{selectedTask ? 'Save Changes' : 'Add Task'}</button>
        </div>
      </form>
    </div>
  );
};

export default TaskForm;

Reviewing Individual Entries with TaskDetail.js

Create your third component asset file named src/TaskDetail.js. It fetches details for an isolated record based on a single variable parameter:

JavaScript

// TaskDetail.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const TaskDetail = ({ taskId }) => {
  const [task, setTask] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!taskId) return;
    setLoading(true);
    axios.get(`http://localhost:5000/api/tasks/${taskId}`)
      .then((response) => {
        setTask(response.data);
        setLoading(false);
      })
      .catch((error) => {
        console.error('Error fetching details:', error);
        setLoading(false);
      });
  }, [taskId]);

  if (!taskId) return <div>Select a task card to read its individual profile details.</div>;
  if (loading) return <div>Loading details...</div>;

  return (
    <div style={{ padding: '10px', border: '1px solid #ccc', marginTop: '10px' }}>
      <h3>Detailed View</h3>
      <p><strong>ID:</strong> {task?.id}</p>
      <p><strong>Title:</strong> {task?.title}</p>
      <p><strong>Description:</strong> {task?.description}</p>
    </div>
  );
};

export default TaskDetail;

Part 3: Connecting the Components in App.js

Now, let’s tie everything together in the central hub file. Open your main application root file located at src/App.js, remove its boilerplate placeholder structure, and clean it up by replacing it with this layout management design:

JavaScript

// App.js
import React, { useState } from 'react';
import axios from 'axios';
import TaskList from './TaskList';
import TaskForm from './TaskForm';
import TaskDetail from './TaskDetail';

const App = () => {
  const [refreshTrigger, setRefreshTrigger] = useState(0);
  const [selectedTask, setSelectedTask] = useState(null);
  const [detailedTaskId, setDetailedTaskId] = useState(null);

  const handleTransactionComplete = () => {
    setRefreshTrigger((prev) => prev + 1);
    setSelectedTask(null);
  };

  const handleSelectTaskForEdit = (task) => {
    setSelectedTask(task);
    setDetailedTaskId(task.id);
  };

  const handleDeleteTask = (id) => {
    axios.delete(`http://localhost:5000/api/tasks/${id}`)
      .then(() => {
        handleTransactionComplete();
        if (detailedTaskId === id) {
          setDetailedTaskId(null);
        }
      })
      .catch((error) => console.error('Error deleting task:', error));
  };

  return (
    <div style={{ padding: '30px', fontFamily: 'sans-serif', maxWidth: '600px', margin: '0 auto' }}>
      <h1>React CRUD App with RESTful API</h1>
      <hr />
      <TaskForm 
        selectedTask={selectedTask} 
        onTransactionComplete={handleTransactionComplete} 
      />
      <hr />
      <TaskList 
        refreshTrigger={refreshTrigger} 
        onSelectTask={handleSelectTaskForEdit}
        onDeleteTask={handleDeleteTask}
      />
      <hr />
      <TaskDetail taskId={detailedTaskId} />
    </div>
  );
};

export default App;

This setup coordinates data synchronization cleanly across your app. When you create, update, or delete a task, handleTransactionComplete signals the other components to reload, keeping the screen fully accurate without requiring page refreshes.

Part 4: Testing the Application

With both your backend engine running on port 5000 and your layout configured, go ahead and boot up your local interface engine. Open your terminal window pointing directly into crud-app and type:

Bash

npm start

A live application window will quickly launch directly inside your active browser environment at http://localhost:3000. You can now run your app through a complete operational validation test cycle:

OperationUser ActionExpected Internal Event Routine
CreateFill out the form input fields and press the submission button.Axios routes a standard POST payload down to the server array list, updating the interface instantly.
ReadScan through the listed elements visible on your screen layout.On startup, the UI performs a direct server lookup via GET to load existing objects.
UpdateClick the inline Edit Task button alongside any asset block.Inputs fill automatically with existing parameters, and saving updates the data via a PUT request.
DeleteClick the inline Delete button next to any active row.An HTTP DELETE network command removes the targeted entity index from your backend array.

Common Roadmap Enhancements

What you have built is an excellent foundation, but production-grade software often requires a few more layers. As you expand your development skills, consider focusing on these next milestones:

  • Form Validation: Ensure fields cannot be submitted with empty strings or invalid text lengths to protect data integrity.
  • Persistent Database Storage: Connect your Express backend to a real database system like PostgreSQL or MongoDB so your information survives server restarts.
  • State Management Upgrade: For larger structures, modern state tools like Zustand provide a highly effective way to manage shared global objects without deep prop-drilling.

Frequently Asked Questions

Why do we need CORS configuration in the Express setup?

Web browsers block scripts from making requests to a different domain or port than the one that served the original web page. Since your React app runs on port 3000 and your Node server runs on port 5000, configuring the cors middleware on your backend lets the browser know that requests from your frontend are safe to process.

Is an in-memory array suitable for production-level CRUD?

No. An in-memory array clears out entirely every time your server script restarts. It works perfectly for quick testing or tutorial guides, but production web solutions always connect to dedicated database engines to keep user files safe across long periods.

What is the primary difference between a prototype and a full MVP platform?

A prototype simply checks if an isolated feature idea functions correctly. A Minimum Viable Product (MVP) is a fully realized software solution designed with complete user flows and secure logic, built to handle real customer interactions in the market.

Building Tech That Scales

Taking an application from a local project to a fast, reliable system that supports thousands of users requires careful planning. If you are a startup founder or a business owner looking to build custom web applications, you do not have to handle the heavy technical lifting alone.

At Charisol, we build reliable, custom software solutions designed to help startups and small businesses grow smoothly. Founded by Dolapo Olisa—a DevOps and Software Engineer who transitions complex code challenges into elegant user experiences—our team acts as your dedicated digital design and development partner. We focus on building high-quality, maintainable software so you can focus entirely on your core business strategies.

Are you ready to transform your development roadmap into a high-performance application? Reach out to our design team today on our Get Started page and let us help you turn your product vision into a scalable, production-ready reality.

What specific software feature or digital application are you looking to design next for your business?

Subscribe to Charisol's newletter

By clicking “Subscribe” you agree to our TOS and Privacy Policy.

Related articles

Ready to build the next build thing?

Fill this form or click book a direct chat with our Operations Lead. Either way, we’ll get back in touch immediately.
Contact information

Thank you for reaching out

Our team will review your request and contact you soon.