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:
Create a data table in the dashboard (upload a CSV or start from scratch).
Instantly generate RESTful API endpoints for CRUD operations.
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:
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.
Log in to your MantaHQ account. If you don't have one, sign up for free.
Navigate to your dashboard and click the Create Workspace button.
Give your new workspace a name (e.g., "team-directory").
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.
In the left sidebar, click Data Tables.
Click Create New, choose the Generate Table from a CSV file option and upload your team directory file.
Alternatively, you can select Create Table Manually and manually add your data.
Give your table a name and description (optional).
Click Create Table.
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.
After importing your data, you'll see a preview of your table.
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
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
In the left sidebar, click Workspaces and select the
team-directory
workspace you created earlier.Click the Create New Service button.
In the DataService Builder, give it a name like
get-team-members
and a brief description.For the method, ensure that
Get Data
is selected. This represents theGET
endpoint. This endpoint will fetch all team members from your data table, then click Next.Select your data source.
Create the POST
Endpoint
Click the Create New Service button again.
Name this new service
add-team-member
.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.Select where you want the data to be stored.
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.
Open your terminal or command prompt.
Run the following command, replacing
team-directory-app
with your preferred project name:npm create vite@latest team-directory-app -- --template react
Navigate into your new project directory:
cd team-directory-app
Run
npm install
to install the project's dependencies.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.
Inside the
src
folder, deleteApp.css
.Create a new folder called
components
insidesrc
.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.
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;
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.What this code does:
It uses the
useState
hook to manageisLoading
anderror
states, which we'll use for user feedback.The
useEffect
hook runs once when the component mounts, triggering ourfetchTeamMembers
function.The
fetchTeamMembers
function is anasync
function that usesfetch
to get the data from your MantaHQGET
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.
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 anasync
function that sends aPOST
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 aLoading...
message while data is being fetched.An error state in
App.jsx
that displays a red error message if theGET
request fails.Simple
alert()
messages inAddMemberForm.jsx
for success and failure of thePOST
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!
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.
Subscribe to my newsletter
Read articles from Chimamanda Justus directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
