How to Build a CRUD App with React and MantaHQ (Part 1)

Create, Read, Update, Delete (CRUD) is the foundation of most apps, from simple to-do lists to complex event management systems. In this article, you'll learn how to build a full CRUD app.

We'll start by showing you how to instantly create a backend using MantaHQ. You'll then set up a React project and implement the "Read" and "Create" operations. By the end of this first part, you'll have a functional frontend connected to your MantaHQ data, all without writing a single line of server-side code.

Prerequisites

To follow along with this article, you'll need:

  • Basic React Knowledge: You should be comfortable with fundamental React concepts such as components, state, and hooks.

  • A MantaHQ Account: You can sign up for a free account here.

  • Node.js: Node.js must be installed on your machine to use npm and run the React development server.

  • Your Data: A CSV or spreadsheet file that you'll use to create your API in MantaHQ.

  • A Text Editor: An editor like Visual Studio Code is recommended.

Understanding CRUD Apps

CRUD is a foundational concept in software development that stands for Create, Read, Update, and Delete. These four operations are the essential building blocks for interacting with a database or any persistent data storage.

  • Create: Adding a new record to a database. (e.g., adding a new user to a users table).

  • Read: Retrieving data from a database. (e.g., fetching a list of all products).

  • Update: Modifying an existing record. (e.g., changing a user's email address).

  • Delete: Removing a record from a database. (e.g., deleting a user account).

These four functions are the primary operations for any app that manages data, from a simple to-do list to a large e-commerce platform.

Why MantaHQ? A Modern Approach to the Traditional Stack

Traditionally, building a CRUD app requires a significant amount of setup. A typical stack often includes:

  • Frontend: A JavaScript framework like React.

  • Backend: A server built with Node.js, Python, or Go.

  • Database: A system like PostgreSQL or MySQL.

  • API: Writing a RESTful API to connect the frontend and backend.

This process involves managing multiple technologies, configuring servers, and writing boilerplate code for each CRUD operation. It’s flexible, but it’s also time-consuming.

MantaHQ changes that workflow. With MantaHQ, you:

  1. Create a data table in the dashboard (upload a CSV or start from scratch).

  2. Instantly generate RESTful API endpoints for CRUD operations.

  3. Focus on building the frontend instead of writing backend code.

This approach saves hours of setup time and reduces maintenance. You can:

  • Build an MVP in a day instead of a week.

  • Skip hosting and server configuration.

  • Avoid writing repetitive CRUD logic.

Project Overview: Building a Team Directory App

In this tutorial, you'll build a Team Directory app. This application lets you:

  • Add a new team member (Create)

  • View all team members (Read)

  • (In Part 2) Edit a team member’s details (Update)

  • (In Part 2) Remove a team member (Delete)

The app will have:

  • Frontend: A React interface to display team members and a form to add new ones.

  • Backend: MantaHQ-generated APIs to store and retrieve team data.

Here's a preview of what you'll build by the end of this article:

Alt text: A webpage section introducing team members with a heading "Meet the people who make it happen." It features four individuals: Olivia Brown, a Data Scientist; William Jones, a Technical Writer; Sophia Garcia, a Software Engineer; and Daniel Miller, a Product Manager. There's a form with fields for first name, last name, role, and email address, alongside a "+ Add Member" button. Build a CRUD app with MantaHQ and React like this one

Setting Up the Backend with Manta

In this section, you'll use MantaHQ to create your backend. This process will turn your data into a fully functional REST API without any server-side coding.

Create a Workspace

A workspace is a container for your projects in MantaHQ. It keeps your data tables and APIs organized.

  1. Log in to your MantaHQ account. If you don't have one, sign up for free.

  2. Navigate to your dashboard and click the Create Workspace button.

  3. Give your new workspace a name (e.g., "team-directory").

Screenshot of a workspace creation interface on MANTAHQ. The screen includes a field to enter "team-directory" and a highlighted "Create Workspace" button. A sidebar menu lists options like Dashboard, Workspaces, and Data Services.

Import Your Data

Now, you'll add the data for your team directory. MantaHQ allows you to either import a spreadsheet or create a new table from scratch.

  1. In the left sidebar, click Data Tables.

  2. Click Create New, choose the Generate Table from a CSV file option and upload your team directory file.

  3. Alternatively, you can select Create Table Manually and manually add your data.

  4. Give your table a name and description (optional).

  5. Click Create Table.

Screenshot of MANTAHQ DASHBOARD. The sidebar menu on the left highlights "Data Tables." The main area shows options for creating a table, including "Generate Table from a CSV File."

Define Your Columns

MantaHQ automatically infers data types from your spreadsheet, but it's important to review and adjust them to ensure your API works correctly.

  1. After importing your data, you'll see a preview of your table.

  2. Click on each column header to configure its properties like the expected format, if the data received should be unique and if it should be required

  3. Click Save Changes.

Generate Your API Endpoints

MantaHQ's core feature is its ability to instantly generate API endpoints for your table. You will create both a GET endpoint to retrieve data and a POST endpoint to add new team members.

Create the GET Endpoint

  1. In the left sidebar, click Workspaces and select the team-directory workspace you created earlier.

  2. Click the Create New Service button.

  3. In the DataService Builder, give it a name like get-team-members and a brief description.

  4. For the method, ensure that Get Data is selected. This represents the GET endpoint. This endpoint will fetch all team members from your data table, then click Next.

  5. Select your data source.

Create the POST Endpoint

  1. Click the Create New Service button again.

  2. Name this new service add-team-member.

  3. For the method, select Create Data (POST). This endpoint will be used to send new team member data from your React app to the backend.

  4. Select where you want the data to be stored.

  5. Make sure to specify the fields that you want to collect data for.

Click the Test Service button to see a preview of your API endpoints and their responses. This is where you'll find the base URL for your API, which you'll use in your React app.

Setting Up the React App

Now that your backend is ready, you'll set up the React project that will consume the MantaHQ API.

Initialize the React Project

We'll use Vite, a fast build tool, to create a new React project.

  1. Open your terminal or command prompt.

  2. Run the following command, replacing team-directory-app with your preferred project name:

     npm create vite@latest team-directory-app -- --template react
    
  3. Navigate into your new project directory:

     cd team-directory-app
    
  4. Run npm install to install the project's dependencies.

  5. Start the development server with the following command:

     npm run dev
    

    Your app will now be running locally, typically at http://localhost:5173.

Organize Your File Structure

A well-organized project is easier to maintain. We'll create a simple folder structure for our components to keep the code clean.

  1. Inside the src folder, delete App.css.

  2. Create a new folder called components inside src.

  3. Inside the components folder, create four new files:

    • Header.jsx: This component will contain the page header.

    • TeamList.jsx: This component will be responsible for displaying the list of team members.

    • Member.jsx: This component will contain individual team members info.

    • AddMemberForm.jsx: This component will contain the form for adding new team members.

Develop Components and Manage State

Before we connect to the API, let's build the static UI for our app. You'll create components to display your team members and a form to add new ones.

First, open src/App.jsx and replace its contents with the following code. This file will hold the main state and render our three main components (Header.jsx, TeamList.jsx, and AddMemberForm.jsx).

import { useState } from "react";
import Header from "./components/Header";
import TeamList from "./components/TeamList";
import AddMemberForm from "./components/AddMemberForm";

function App() {
  const [teamMembers, setTeamMembers] = useState([]);

  return (
    <div className="app">
      <Header />
      <TeamList teamMembers={teamMembers} />
      <AddMemberForm />
    </div>
  );
}

export default App;

Next, create the component to display the header. Open src/components/Header.jsx and add this code:

export default function Header() {
  return (
    <header>
      <h1>Meet the people who make it happen</h1>
      <p>
        From product visionaries to engineering experts, each member brings
        unique skills and experiences that shape our success
      </p>
    </header>
  );
}

Next, create the component to display the list. Open src/components/TeamList.jsx and add this code:

import Member from "./Member";

export default function TeamList({ teamMembers }) {
  if (teamMembers.length === 0) {
    return <p>No team members found. Add one!</p>;
  }

  return (
    <div className="team-list">
      {teamMembers.map((member) => (
        <Member
          key={member.email}
          firstName={member.first_ame}
          lastName={member.last_name}
          role={member.role}
          email={member.email}
        />
      ))}
    </div>
  );
}

After that, create the component that will contain individual team members info. Open src/components/Member.jsx and add this code:

export default function Member({ firstName, lastName, role, email }) {
  return (
    <p>
      My name is {firstName} {lastName}. I am a {role}. My email is {email}
    </p>
  );
}

Finally, create the form component. Open src/components/AddMemberForm.jsx and add this code:

export default function AddMemberForm() {
  const handleSubmit = (e) => {
    e.preventDefault();
    // This is where our POST logic will go later.
    alert('Form submitted!');
  };

  return (
      <form onSubmit={handleSubmit}>
        <input type="text" placeholder="First Name" required />
        <input type="text" placeholder="Last Name" required />
        <input type="email" placeholder="Email" required />
        <input type="text" placeholder="Role (e.g. manager)" required />
        <button type="submit">Add Member</button>
      </form>
  );
}

Now, you should see a static UI with a header, a placeholder message for the team list, and a form.

A webpage section titled "Meet the people who make it happen" with a description encouraging the addition of team members. It includes fields for first name, last name, role, and email address, along with an "+ Add Member" button. No team members are currently listed.

Connecting React to Manta APIs

This is where you'll connect your frontend to the backend you created with MantaHQ, implementing the Read and Create functionality using the native fetch API.

Read: Fetch and Display Data

You'll use the MantaHQ GET endpoint to fetch your team data and display it in the list. This API call will happen when the app first loads.

Update your src/App.jsx file to include the data fetching logic inside a useEffect hook. Replace the existing content with the following:

import { useEffect, useState } from "react";
import Header from "./components/Header";
import TeamList from "./components/TeamList";
import AddMemberForm from "./components/AddMemberForm";

function App() {
  const [teamMembers, setTeamMembers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // Your MantaHQ API endpoint for fetching all team members.
  // Replace this with your actual MantaHQ API URL.
  const API_URL = URL;

  const fetchTeamMembers = async function () {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(API_URL);
      if (!response.ok) {
        throw new Error("Unable to fetch team data");
      }
      const data = await response.json();

      console.log(data);
      setTeamMembers(data.data);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    fetchTeamMembers();
  }, []);

  return (
    <div>
      <Header />
      {error && <p style={{ color: "red" }}>Error: {error}</p>}
      {isLoading ? (
        <p>Loading team members...</p>
      ) : (
        <>
          <TeamList teamMembers={teamMembers} />
          <AddMemberForm onMemberAdded={fetchTeamMembers} />
        </>
      )}
    </div>
  );
}

export default App;
💡
Note: The MantaHQ API returns data nested inside a data key, so we use data.data to access the array of team members. You can find your API_URL for the GET endpoint in the get-team-members service you created.

Screenshot of a MantaHQ web interface showing the "get-team-members" service documentation. The window displays the API URL and a "COPY API URL" button. A sidebar menu is visible on the left.

What this code does:

  • It uses the useState hook to manage isLoading and error states, which we'll use for user feedback.

  • The useEffect hook runs once when the component mounts, triggering our fetchTeamMembers function.

  • The fetchTeamMembers function is an async function that uses fetch to get the data from your MantaHQ GET endpoint.

  • try...catch is used for robust error handling. If the network response isn't okay, it throws an error.

  • The UI now conditionally renders a loading message, an error message, or the main components based on the state.

"Webpage section titled 'Meet the people who make it happen' features team members and their roles with contact emails. Below is a console log message indicating 'Data retrieved successfully' with a status code of 200. A red arrow points to the message with a green checkmark labeled 'Response'."

Create: Add a New Entry

Now, you'll add the logic to your form to send data to the add_team_member POST endpoint you created in MantaHQ.

Update your src/components/AddMemberForm.jsx file with the following code. This version manages the form inputs with state and handles the API call.

import { useState } from "react";

export default function AddMemberForm({ onMemberAdded }) {
  const [formData, setFormData] = useState({
    first_name: "",
    last_name: "",
    role: "",
    email: "",
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prevState) => ({
      ...prevState,
      [name]: value,
    }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Replace this with your actual MantaHQ API URL.
    try {
      const response = await fetch(
        URL,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(formData),
        }
      );

      if (!response.ok) {
        throw new Error("Failed to add new team member");
      }

      alert("Team member added successfully!");
      setFormData({
        first_name: "",
        last_name: "",
        email: "",
        role: "",
      });
      onMemberAdded();
    } catch (error) {
      alert(`Error: ${error.message}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="first_name"
        placeholder="First name"
        value={formData.first_name}
        onChange={handleChange}
      />
      <input
        type="text"
        name="last_name"
        placeholder="Last name"
        value={formData.last_name}
        onChange={handleChange}
      />
      <input
        type="text"
        name="role"
        placeholder="Role (e.g. manager)"
        value={formData.role}
        onChange={handleChange}
      />
      <input
        type="email"
        name="email"
        placeholder="Email address"
        value={formData.email}
        onChange={handleChange}
      />
      <button type="submit">+ Add Member</button>
    </form>
  );
}

What this code does:

  • It uses useState to manage the form data as a single object.

  • The handleChange function updates the state with each keystroke, creating a controlled component.

  • The handleSubmit function is an async function that sends a POST request to your MantaHQ endpoint.

  • JSON.stringify(formData) converts the JavaScript object into a JSON string, which is the required format for the API call.

  • Upon success, the form is cleared, and a success message is displayed. A try...catch block handles any errors.

Error Handling & User Feedback

You've already integrated a basic form of error handling. The try...catch blocks in both your App.jsx and AddMemberForm.jsx will catch any issues with the API calls and log them to the console.

For the user, you've implemented:

  • A loading state in App.jsx that shows a Loading... message while data is being fetched.

  • An error state in App.jsx that displays a red error message if the GET request fails.

  • Simple alert() messages in AddMemberForm.jsx for success and failure of the POST request.

This provides the user with clear feedback on the status of their actions, which is a critical part of a robust application.

UI Enhancements

Now that your app's core functionality is in place, let's make it look better. To make the app look more professional.

1. Add Styles to the index.css File

Add the following CSS rules to your index.css file in the src folder.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --primary-color: #fd6939;
  --text-color: #271d00;
  --background-color: #efefe7;
  --white: #ffffff;
  --accent: #efefe7;
}

/* IGNORE: LOCAL FONT (used to match the design) */
/* USE: @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;700&family=Inter:wght@400;500;700&display=swap'); */
/* font-family: 'Cormorant Garamond', serif; */

@font-face {
  font-family: "Recoleta Font Family";
  font-style: normal;
  font-weight: 400;
  src: url("./assets/Recoleta Font Family.otf") format("opentype");
}

@font-face {
  font-family: "Aeonik";
  font-style: normal;
  font-weight: 400;
  src: url("./assets/Aeonik-Regular.otf") format("opentype");
}

@font-face {
  font-family: "Aeonik";
  font-style: normal;
  font-weight: 500;
  src: url("./assets/Aeonik-Medium.otf") format("opentype");
}

body {
  font-family: "Recoleta Font Family";
  background-color: var(--background-color);
  color: var(--text-color);
}

.app {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* HEADER */

header {
  margin: 60px 0 60px 0;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}

header h1 {
  font-size: 54px;
  font-style: normal;
  font-weight: 400;
  line-height: 70px;
  letter-spacing: -1.05px;
  max-width: 550px;
}

header p {
  font-family: "Aeonik";
  font-weight: 500;
  font-size: 22px;
  font-style: normal;
  font-weight: 400;
  line-height: 30px;
  max-width: 900px;
}

/* TEAM LIST */
.team-list {
  display: flex;
  max-width: 1182px;
  justify-content: center;
  align-items: center;
  align-content: center;
  gap: 18px;
  flex-wrap: wrap;
  margin: 20px 0 120px 0;
}

.card {
  display: flex;
  padding: 40px 30px;
  flex-direction: column;
  align-items: center;
  border-radius: 20px;
  background: #fbfaf8;
  max-width: 270px;
  cursor: pointer;
}

.card img {
  border-radius: 50%;
  margin-bottom: 32px;
}

.card .name {
  text-transform: capitalize;
  font-size: 24px;
  margin-bottom: 5px;
}

.card .role {
  font-family: Aeonik;
  font-size: 16px;
  font-style: normal;
  font-weight: 400;
  letter-spacing: -0.2px;
  text-transform: capitalize;
  margin-bottom: 24px;
}

.card .email {
  font-family: Aeonik;
  font-size: 15px;
  font-style: normal;
  font-weight: 400;
  text-transform: lowercase;
  color: var(--primary-color);
}

/* FORM */
form {
  border-radius: 60px 60px 0 0;
  border: 1px solid rgba(39, 29, 0, 0.12);
  background: rgba(220, 220, 204, 0.5);
  backdrop-filter: blur(9px);
  display: flex;
  width: 100%;
  padding: 24px 0;
  align-items: center;
  justify-content: center;
  gap: 16px;
  position: fixed;
  bottom: 0;
  left: 0;
  z-index: 1000;
  font-family: "Aeonik";
}

input {
  max-width: 300px;
  padding: 12px 16px 12px 16px;
  flex-shrink: 0;
  border-radius: 40px;
  border: 1px solid rgba(39, 29, 0, 0.17);
  background: var(--accent);
}

form button {
  border-radius: 40px;
  background: var(--primary-color);
  box-shadow: 0 4px 33px 0 #fd6939;
  display: flex;
  padding: 12px 24px;
  justify-content: center;
  align-items: center;
  border: none;
  font-family: "Aeonik";
  font-size: 16px;
  font-style: normal;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.4s ease-in-out;
}

form button:hover {
  padding: 12px 32px;
  color: var(--white);
  background-color: var(--text-color);
  box-shadow: 0 4px 33px 0 #271d00;
}

2. Update Member.jsx to Dynamically Color Initials

Next, we'll give each team member an avatar based on the member's name with a dynamic background color with UI Avatars free API

export default function Member({ firstName, lastName, role, email }) {
  return (
    <div className="card">
      <img
        src={`https://ui-avatars.com/api/?name=${firstName}+${lastName}&background=random`}
        alt="avatar"
      />
      <p className="name">
        {firstName} {lastName}
      </p>
      <p className="role">{role}</p>
      <p className="email">{email}</p>
    </div>
  );
}

Now your app should look exactly like the design!

Alt text: A webpage section introducing team members with a heading "Meet the people who make it happen." It features four individuals: Olivia Brown, a Data Scientist; William Jones, a Technical Writer; Sophia Garcia, a Software Engineer; and Daniel Miller, a Product Manager. There's a form with fields for first name, last name, role, and email address, alongside a "+ Add Member" button. Build a CRUD app with MantaHQ and React like this one

Summary of Part 1 & What's Next

Congratulations! You've successfully built a frontend that can read and create data using a MantaHQ backend.

In Part 2 of this series, we'll expand on this foundation by implementing the remaining CRUD operations: Update and Delete. You'll learn how to edit existing team member information and remove entries from your directory.

0
Subscribe to my newsletter

Read articles from Chimamanda Justus directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chimamanda Justus
Chimamanda Justus