Building a CRUD App with React and Node.js

In this tutorial, we will be building a CRUD (Create, Read, Update, Delete) application using React for the frontend and Node.js for the backend. We will start by setting up the development environment, creating the necessary database, building the backend API endpoints, and finally creating the frontend components. By the end of this tutorial, you will have a fully functional CRUD app that allows users to perform basic CRUD operations on a database.

building crud app react node js

Introduction

What is a CRUD App

A CRUD app is an application that allows users to create, read, update, and delete data. These are the basic operations that most applications require when interacting with a database. In this tutorial, we will be building a simple CRUD app that allows users to manage a list of products.

Why use React and Node.js

React is a popular JavaScript library for building user interfaces. It allows developers to build reusable UI components and provides a virtual DOM that efficiently updates the UI when the underlying data changes. Node.js, on the other hand, is a JavaScript runtime that allows developers to build scalable and efficient server-side applications. By using React for the frontend and Node.js for the backend, we can build a full-stack JavaScript application with a consistent programming language and toolset.

Overview of the tutorial

In this tutorial, we will start by setting up the development environment. We will install Node.js, create a new React project using Create React App, and set up the server using Express. Next, we will create the necessary database for our app. We will choose a database, set it up, and create the required tables. Once the database is set up, we will build the backend API endpoints. We will implement the CRUD operations for managing the products and handle errors and validation. Finally, we will build the frontend components. We will create the necessary components, implement data fetching from the backend, and handle user input.

Setting up the Development Environment

Installing Node.js

Before we can start building our CRUD app, we need to install Node.js. Node.js can be downloaded from the official website (https://nodejs.org) and comes with npm, the package manager for Node.js. Once Node.js is installed, we can verify the installation by running the following command in the terminal:

node -v

This should print the version of Node.js installed on your machine.

Creating a new React project

Next, let's create a new React project using Create React App. Create React App is a tool that sets up a new React project with all the necessary configuration and dependencies. To create a new React project, run the following command in the terminal:

npx create-react-app crud-app

This will create a new directory called crud-app with the basic project structure and files. Change into the crud-app directory by running the following command:

cd crud-app

Setting up the server

Now that we have our React project set up, let's set up the server using Express. Express is a minimal and flexible Node.js web application framework that provides a set of robust features for building web applications. To set up the server, we first need to install Express. Run the following command in the terminal:

npm install express

Once Express is installed, create a new file called server.js in the root directory of your project. Inside server.js, add the following code:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

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

This code sets up a simple Express server that listens on port 3000. When a request is made to the root URL (/), it responds with "Hello World!". To start the server, run the following command in the terminal:

node server.js

You should see the message "Server listening at http://localhost:3000" printed in the terminal. You can now access the server at http://localhost:3000 in your browser and see the "Hello World!" message.

Creating the Database

Choosing a database

Before we can start building the backend API endpoints, we need to choose a database. There are many different databases available, each with its own strengths and weaknesses. For this tutorial, we will be using PostgreSQL, a popular open-source relational database management system. PostgreSQL is known for its stability, scalability, and extensibility.

To install PostgreSQL, visit the official website (https://www.postgresql.org) and follow the installation instructions for your operating system.

Setting up the database

Once PostgreSQL is installed, we can create a new database for our app. Open a terminal and run the following command to open the PostgreSQL command-line interface:

psql

Inside the PostgreSQL command-line interface, run the following command to create a new database called crud_app:

CREATE DATABASE crud_app;

You can verify that the database was created successfully by running the following command:

\l

This will list all the databases on your PostgreSQL server, and you should see crud_app in the list.

Creating the necessary tables

Now that we have our database set up, let's create the necessary tables for our app. We will be creating a products table to store the products. Inside the PostgreSQL command-line interface, run the following command to connect to the crud_app database:

\c crud_app

Once connected to the database, run the following command to create the products table:

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  price NUMERIC(10, 2) NOT NULL,
  description TEXT
);

This command creates a table called products with four columns: id, name, price, and description. The id column is an auto-incrementing primary key, the name column is a string, the price column is a numeric value with two decimal places, and the description column is a text field.

You can verify that the table was created successfully by running the following command:

\d

This will list all the tables in the crud_app database, and you should see products in the list.

Building the Backend

Creating API endpoints

Now that our database is set up, let's build the backend API endpoints for our CRUD app. We will be using Express to handle the HTTP requests and interact with the database. Inside server.js, add the following code:

const express = require('express');
const app = express();
const port = 3000;

app.use(express.json());

app.get('/api/products', (req, res) => {
  // Code to get all products from the database
});

app.get('/api/products/:id', (req, res) => {
  // Code to get a single product by id from the database
});

app.post('/api/products', (req, res) => {
  // Code to create a new product in the database
});

app.put('/api/products/:id', (req, res) => {
  // Code to update a product by id in the database
});

app.delete('/api/products/:id', (req, res) => {
  // Code to delete a product by id from the database
});

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

This code sets up the API endpoints for getting all products, getting a single product by id, creating a new product, updating a product by id, and deleting a product by id. We use the Express get, post, put, and delete methods to define the routes and provide the corresponding callback functions to handle the requests.

Implementing CRUD operations

Now let's implement the CRUD operations inside the API endpoints. Inside server.js, add the following code:

const express = require('express');
const app = express();
const port = 3000;

app.use(express.json());

app.get('/api/products', (req, res) => {
  // Code to get all products from the database
  // ...
  const products = await db.query('SELECT * FROM products');
  res.json(products.rows);
});

app.get('/api/products/:id', (req, res) => {
  // Code to get a single product by id from the database
  // ...
  const product = await db.query('SELECT * FROM products WHERE id = $1', [req.params.id]);
  res.json(product.rows[0]);
});

app.post('/api/products', (req, res) => {
  // Code to create a new product in the database
  // ...
  const { name, price, description } = req.body;
  const product = await db.query('INSERT INTO products (name, price, description) VALUES ($1, $2, $3) RETURNING *', [name, price, description]);
  res.json(product.rows[0]);
});

app.put('/api/products/:id', (req, res) => {
  // Code to update a product by id in the database
  // ...
  const { name, price, description } = req.body;
  const product = await db.query('UPDATE products SET name = $1, price = $2, description = $3 WHERE id = $4 RETURNING *', [name, price, description, req.params.id]);
  res.json(product.rows[0]);
});

app.delete('/api/products/:id', (req, res) => {
  // Code to delete a product by id from the database
  // ...
  const product = await db.query('DELETE FROM products WHERE id = $1 RETURNING *', [req.params.id]);
  res.json(product.rows[0]);
});

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

This code uses the db object to query the database and perform the CRUD operations. We use the await keyword to wait for the database queries to complete and then send the results back as JSON responses.

Handling errors and validation

It's important to handle errors and perform validation when building a CRUD app. Let's add error handling and validation to our API endpoints. Inside server.js, add the following code:

const express = require('express');
const app = express();
const port = 3000;

app.use(express.json());

app.get('/api/products', (req, res) => {
  try {
    const products = await db.query('SELECT * FROM products');
    res.json(products.rows);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.get('/api/products/:id', (req, res) => {
  try {
    const product = await db.query('SELECT * FROM products WHERE id = $1', [req.params.id]);
    if (product.rows.length === 0) {
      res.status(404).json({ error: 'Product not found' });
    } else {
      res.json(product.rows[0]);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/api/products', (req, res) => {
  try {
    const { name, price, description } = req.body;
    if (!name || !price) {
      res.status(400).json({ error: 'Name and price are required' });
      return;
    }
    const product = await db.query('INSERT INTO products (name, price, description) VALUES ($1, $2, $3) RETURNING *', [name, price, description]);
    res.json(product.rows[0]);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.put('/api/products/:id', (req, res) => {
  try {
    const { name, price, description } = req.body;
    if (!name || !price) {
      res.status(400).json({ error: 'Name and price are required' });
      return;
    }
    const product = await db.query('UPDATE products SET name = $1, price = $2, description = $3 WHERE id = $4 RETURNING *', [name, price, description, req.params.id]);
    if (product.rows.length === 0) {
      res.status(404).json({ error: 'Product not found' });
    } else {
      res.json(product.rows[0]);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.delete('/api/products/:id', (req, res) => {
  try {
    const product = await db.query('DELETE FROM products WHERE id = $1 RETURNING *', [req.params.id]);
    if (product.rows.length === 0) {
      res.status(404).json({ error: 'Product not found' });
    } else {
      res.json(product.rows[0]);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

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

This code adds try-catch blocks around the database queries to catch any errors that may occur. If an error occurs, we log it to the console and send an appropriate error response with a status code.

Building the Frontend

Creating components

Now that the backend is complete, let's move on to building the frontend components using React. Inside the src directory of your React project, create a new directory called components. Inside the components directory, create a new file called ProductList.js. Inside ProductList.js, add the following code:

import React, { useEffect, useState } from 'react';

const ProductList = () => {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products')
      .then((response) => response.json())
      .then((data) => setProducts(data))
      .catch((error) => console.error(error));
  }, []);

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} - {product.price}
        </li>
      ))}
    </ul>
  );
};

export default ProductList;

This code defines a functional component called ProductList. Inside the component, we use the useState hook to create a state variable products and a setter function setProducts. We use the useEffect hook to fetch the list of products from the backend API when the component is mounted. We then map over the products array and render a list item for each product.

Implementing data fetching

Now let's use the ProductList component to fetch and display the list of products. Inside the src directory of your React project, open the App.js file and replace the existing code with the following:

import React from 'react';
import ProductList from './components/ProductList';

const App = () => {
  return (
    <div>
      <h1>CRUD App with React and Node.js</h1>
      <ProductList />
    </div>
  );
};

export default App;

This code imports the ProductList component and renders it inside the App component. When you run the app, you should see the heading "CRUD App with React and Node.js" and a list of products fetched from the backend API.

Handling user input

Now let's add a form to create new products. Inside the components directory, create a new file called ProductForm.js. Inside ProductForm.js, add the following code:

import React, { useState } from 'react';

const ProductForm = () => {
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = (event) => {
    event.preventDefault();
    const product = { name, price, description };
    fetch('/api/products', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(product),
    })
      .then((response) => response.json())
      .then((data) => console.log(data))
      .catch((error) => console.error(error));
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" value={name} onChange={(event) => setName(event.target.value)} />
      </label>
      <label>
        Price:
        <input type="number" value={price} onChange={(event) => setPrice(event.target.value)} />
      </label>
      <label>
        Description:
        <textarea value={description} onChange={(event) => setDescription(event.target.value)} />
      </label>
      <button type="submit">Create</button>
    </form>
  );
};

export default ProductForm;

This code defines a functional component called ProductForm. Inside the component, we use the useState hook to create state variables for the name, price, and description fields. We use the handleSubmit function to handle the form submission. When the form is submitted, we create a new product object with the form values and send a POST request to the backend API to create the product.

To use the ProductForm component, open the App.js file and add the following code inside the div element:

<ProductForm />

When you run the app, you should see a form with input fields for the name, price, and description, and a "Create" button.