Australian flagJoin us at the FIDO seminar in Melbourne – Feb 7, 2025!
react express crud app mongodbEngineering

React and Express CRUD app with MongoDB and Node.js

This tutorial teaches you how to create a basic CRUD app using React in the frontend and Node.js (Express) in the backend with a MongoDB database.

Blog-Post-Author

Lukas

Created: January 16, 2025

Updated: January 17, 2025


Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.

In this comprehensive tutorial, you'll learn how to create a full-stack CRUD (Create, Read, Update, Delete) application using React for the front end and Node.js with Express for the back end, all powered by a MongoDB database. We'll utilize technologies such as React Router, Axios, and TailwindCSS to enhance the user experience.

Project Overview#

We'll develop a full-stack application where users can manage tutorials. Each tutorial will have the following attributes:

  • ID: Unique identifier
  • Title: Name of the tutorial
  • Description: Detailed information
  • Published Status: Indicates if the tutorial is published

Users can perform the following actions:

  • Create a new tutorial
  • Retrieve all tutorials or a specific one by ID
  • Update existing tutorials
  • Delete tutorials
  • Search tutorials by title

Example Screenshots#

Add a New Tutorial#

Add Item

Display All Tutorials#

Show All Items

Edit a Tutorial#

Edit Item

Search Tutorials by Title#

Search Tutorials

Architecture#

The application follows a client-server architecture:

  • Backend: Node.js with Express handles RESTful APIs and interacts with the MongoDB database using mongoose.
  • Frontend: React.js communicates with the backend via Axios for HTTP requests and uses React Router for navigation.

Backend Implementation#

Setup Node.js Application#

  1. Create Project Directory:

    mkdir react-node-express-mongodb-crud cd react-node-express-mongodb-crud
  2. Initialize Node.js App:

    npm init -y
  3. Install Dependencies:

    npm install express mongoose cors --save
  4. Use ESModule Syntax Add the following line to your package.json file:

    { "type": "module" // ... }
Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

Configure MongoDB Database#

  1. Create Configuration File (app/config/db.config.js):
// app/config/db.config.js export default { HOST: 'localhost', PORT: 27017, DB: 'node_js_jwt_auth_db', };
  1. Initialize the MongoDB database (app/models/index.js):
import dbConfig from '../config/db.config.js'; import mongoose from 'mongoose'; import Tutorial from './tutorial.model.js'; mongoose.Promise = global.Promise; const db = {}; db.mongoose = mongoose; db.url = `mongodb://${dbConfig.HOST}:${dbConfig.PORT}/${dbConfig.DB}`; db.tutorials = Tutorial(mongoose); export default db;
  1. Define Tutorial Model (app/models/tutorial.model.js):
export default (mongoose) => { let schema = mongoose.Schema( { title: String, description: String, published: Boolean }, {timestamps: true} ); schema.method("toJSON", function () { const {__v, _id, ...object} = this.toObject(); object.id = _id; return object; }); const Tutorial = mongoose.model("tutorial", schema); return Tutorial; };

If you don't hava a MongoDB database for local development, you can use Docker to create a temporary MongoDB container:

docker run --rm -p 27017:27017 mongo
Slack Icon

Become part of our Passkeys Community for updates and support.

Join

Define Routes and Controllers#

  1. Create Controller (app/controllers/tutorial.controller.js):
import db from "../models/index.js"; const Tutorial = db.tutorials; // Create and Save a new Tutorial export const create = (req, res) => { // Validate request if (!req.body.title) { res.status(400).send({ message: "Content can not be empty!" }); return; } // Create a Tutorial const tutorial = new Tutorial({ title: req.body.title, description: req.body.description, published: req.body.published ? req.body.published : false }); // Save Tutorial in the database tutorial .save(tutorial) .then(data => { res.send(data); }) .catch(err => { res.status(500).send({ message: err.message || "Some error occurred while creating the Tutorial." }); }); }; // Retrieve all Tutorials export const findAll = (req, res) => { // Allow a filter condition via query parameter const title = req.query.title; const condition = title ? {title: {[Op.like]: `%${title}%`}} : null; Tutorial.find(condition) .then(data => { res.send(data); }) .catch(err => { res.status(500).send({ message: err.message || "Some error occurred while retrieving tutorials." }); }); }; // Find a single Tutorial by ID export const findOne = (req, res) => { const id = req.params.id; // Find Tutorial by primary key Tutorial.findById(id) .then(data => { if (!data) res.status(404).send({message: "Not found Tutorial with id " + id}); else res.send(data); }) .catch(err => { res .status(500) .send({message: "Error retrieving Tutorial with id=" + id}); }); }; // Update a Tutorial by ID export const update = (req, res) => { const id = req.params.id; // Update the Tutorial with the specified ID Tutorial.findByIdAndUpdate(id, req.body, {useFindAndModify: false}) .then(data => { if (!data) { res.status(404).send({ message: `Cannot update Tutorial with id=${id}. Maybe Tutorial was not found!` }); } else res.send({message: "Tutorial was updated successfully."}); }) .catch(err => { res.status(500).send({ message: "Error updating Tutorial with id=" + id }); }); }; // Delete a Tutorial by ID export const deleteOne = (req, res) => { const id = req.params.id; // Delete the Tutorial with the specified ID Tutorial.findByIdAndRemove(id) .then(data => { if (!data) { res.status(404).send({ message: `Cannot delete Tutorial with id=${id}. Maybe Tutorial was not found!` }); } else { res.send({ message: "Tutorial was deleted successfully!" }); } }) .catch(err => { res.status(500).send({ message: "Could not delete Tutorial with id=" + id }); }); }; // Delete all Tutorials export const deleteAll = (req, res) => { // Delete all Tutorials Tutorial.deleteMany({}) .then(data => { res.send({ message: `${data.deletedCount} Tutorials were deleted successfully!` }); }) .catch(err => { res.status(500).send({ message: err.message || "Some error occurred while removing all tutorials." }); }); }; // Find all published Tutorials export const findAllPublished = (req, res) => { // Find all Tutorials with published = true Tutorial.find({published: true}) .then(data => { res.send(data); }) .catch(err => { res.status(500).send({ message: err.message || "Some error occurred while retrieving tutorials." }); }); };
  1. Set Up Routes (app/routes/tutorial.routes.js):
import * as tutorials from "../controllers/tutorial.controller.js"; import express from "express"; export default (app) => { let router = express.Router(); // Create a new Tutorial router.post("/", tutorials.create); // Retrieve all Tutorials router.get("/", tutorials.findAll); // Retrieve a single Tutorial with id router.get("/:id", tutorials.findOne); // Update a Tutorial with id router.put("/:id", tutorials.update); // Delete a Tutorial with id router.delete("/:id", tutorials.deleteOne); // Delete all Tutorials router.delete("/", tutorials.deleteAll); // Find all published Tutorials router.get("/published", tutorials.findAllPublished); app.use('/api/tutorials', router); };
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

Run the Server#

  1. Create Server File (server.js):
import express from "express"; import cors from "cors"; import db from "./app/models/index.js"; import tutorialRoutes from "./app/routes/tutorial.routes.js"; const app = express(); const corsOptions = { origin: "http://localhost:5173" }; app.use(cors(corsOptions)); app.use(express.json()); app.use(express.urlencoded({extended: true})); // Simple route app.get("/", (req, res) => { res.json({message: "Welcome to the Tutorial Application."}); }); // Routes tutorialRoutes(app); // Sync database db.mongoose.connect(db.url) .then(() => { console.log("Connected to the database!"); }) .catch(err => { console.log("Cannot connect to the database!", err); process.exit(); }); const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); });
  1. Start the Server:
node server.js

Output:

Server is running on port 8080. Synced db.
StateOfPasskeys Icon

Want to find out how many people can use passkeys?

View Adoption Data

Frontend Implementation#

File structure#

Your final file structure will look like this:

frontend/ ├─ index.html ├─ package.json ├─ postcss.config.js ├─ tailwind.config.js ├─ vite.config.js ├─ src/ │ ├─ App.jsx │ ├─ main.jsx │ ├─ index.css │ ├─ services/ │ │ └─ tutorial.service.js │ └─ pages/ │ ├─ AddTutorial.jsx │ ├─ Tutorial.jsx │ └─ TutorialsList.jsx

Create the React App#

Run the following commands to set up a new React app using Vite:

npm create vite@latest frontend -- --template react cd frontend npm i npm i react-router-dom axios npm install -D tailwindcss autoprefixer npx tailwindcss init -p

Finally, set up the tailwind content option in the configuration file tailwind.config.js:

/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], }

Then, open src/index.css (Vite has created this file for you) and add the Tailwind directives:

@tailwind base; @tailwind components; @tailwind utilities;
Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

Initialize the App layout#

The App.jsx component configures React Router routes and sets up a basic Tailwind navbar. We’ll navigate between:

  • /tutorials – list of tutorials
  • /add – form to create new tutorial
  • /tutorials/:id – editing a single tutorial
import { Routes, Route, Link } from "react-router-dom"; import TutorialsList from "./pages/TutorialsList"; import AddTutorial from "./pages/AddTutorial"; import Tutorial from "./pages/Tutorial"; function App() { return ( <BrowserRouter> <div> {/* NAVBAR */} <nav className="bg-blue-600 p-4 text-white"> <div className="flex space-x-4"> <Link to="/tutorials" className="hover:text-gray-300 font-bold"> Tutorials </Link> <Link to="/add" className="hover:text-gray-300"> Add </Link> </div> </nav> {/* ROUTES */} <div className="container mx-auto mt-8 px-4"> <Routes> <Route path="/" element={<TutorialsList/>}/> <Route path="/tutorials" element={<TutorialsList/>}/> <Route path="/add" element={<AddTutorial/>}/> <Route path="/tutorials/:id" element={<Tutorial/>}/> </Routes> </div> </div> </BrowserRouter> ); } export default App;

Create Data Service#

This service handles Axios HTTP requests to our Node/Express backend (http://localhost:8080/api). Update the baseURL if your server runs on a different address or port.

import axios from "axios"; const http = axios.create({ baseURL: "http://localhost:8080/api", headers: { "Content-Type": "application/json", }, }); const getAll = () => { return http.get("/tutorials"); }; const get = (id) => { return http.get(`/tutorials/${id}`); }; const create = (data) => { return http.post("/tutorials", data); }; const update = (id, data) => { return http.put(`/tutorials/${id}`, data); }; const remove = (id) => { return http.delete(`/tutorials/${id}`); }; const removeAll = () => { return http.delete("/tutorials"); }; const findByTitle = (title) => { return http.get(`/tutorials?title=${title}`); }; export default { getAll, get, create, update, remove, removeAll, findByTitle, };
Demo Icon

Want to try passkeys yourself? Check our Passkeys Demo.

Try Passkeys

Add item Component#

A component for creating new tutorials under src/pages/AddTutorial.jsx. It allows entering title and description, and then calls TutorialService.create().

import React, { useState } from "react"; import TutorialService from "../services/tutorial.service"; function AddTutorial() { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [submitted, setSubmitted] = useState(false); const saveTutorial = () => { const data = { title, description }; TutorialService.create(data) .then((response) => { console.log(response.data); setSubmitted(true); }) .catch((e) => { console.log(e); }); }; const newTutorial = () => { setTitle(""); setDescription(""); setSubmitted(false); }; return ( <div className="max-w-sm mx-auto p-4 bg-white rounded shadow"> {submitted ? ( <div> <h4 className="font-bold text-green-600 mb-4"> Tutorial submitted successfully! </h4> <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={newTutorial}> Add Another </button> </div> ) : ( <div> <h4 className="font-bold text-xl mb-2">Add Tutorial</h4> <div className="mb-2"> <label className="block mb-1 font-medium">Title</label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" value={title} onChange={(e) => setTitle(e.target.value)} /> </div> <div className="mb-2"> <label className="block mb-1 font-medium">Description</label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" value={description} onChange={(e) => setDescription(e.target.value)} /> </div> <button className="bg-green-500 text-white px-3 py-1 rounded mt-2" onClick={saveTutorial} > Submit </button> </div> )} </div> ); } export default AddTutorial;

Tutorial List Component#

A component under src/pages/TutorialsList.jsx that:

  • Displays a search bar to filter by tutorial title
  • Lists tutorials on the left
  • Shows the selected tutorial on the right
  • Provides a button to remove all tutorials
import { useState, useEffect } from "react"; import TutorialService from "../services/tutorial.service"; import { Link } from "react-router-dom"; function TutorialsList() { const [tutorials, setTutorials] = useState([]); const [currentTutorial, setCurrentTutorial] = useState(null); const [currentIndex, setCurrentIndex] = useState(-1); const [searchTitle, setSearchTitle] = useState(""); useEffect(() => { retrieveTutorials(); }, []); const onChangeSearchTitle = (e) => { setSearchTitle(e.target.value); }; const retrieveTutorials = () => { TutorialService.getAll() .then((response) => { setTutorials(response.data); console.log(response.data); }) .catch((e) => { console.log(e); }); }; const refreshList = () => { retrieveTutorials(); setCurrentTutorial(null); setCurrentIndex(-1); }; const setActiveTutorial = (tutorial, index) => { setCurrentTutorial(tutorial); setCurrentIndex(index); }; const removeAllTutorials = () => { TutorialService.removeAll() .then((response) => { console.log(response.data); refreshList(); }) .catch((e) => { console.log(e); }); }; const findByTitle = () => { TutorialService.findByTitle(searchTitle) .then((response) => { setTutorials(response.data); setCurrentTutorial(null); setCurrentIndex(-1); console.log(response.data); }) .catch((e) => { console.log(e); }); }; return ( <div className="flex flex-col lg:flex-row gap-8"> {/* LEFT COLUMN: SEARCH + LIST */} <div className="flex-1"> <div className="flex mb-4"> <input type="text" className="border border-gray-300 rounded-l px-2 py-1 w-full" placeholder="Search by title" value={searchTitle} onChange={onChangeSearchTitle} /> <button className="bg-blue-500 text-white px-4 py-1 rounded-r" onClick={findByTitle} > Search </button> </div> <h4 className="font-bold text-lg mb-2">Tutorials List</h4> <ul className="divide-y divide-gray-200 border border-gray-200 rounded"> {tutorials && tutorials.map((tutorial, index) => ( <li className={ "px-4 py-2 cursor-pointer " + (index === currentIndex ? "bg-blue-100" : "") } onClick={() => setActiveTutorial(tutorial, index)} key={index} > {tutorial.title} </li> ))} </ul> <button className="bg-red-500 text-white px-3 py-1 rounded mt-4" onClick={removeAllTutorials} > Remove All </button> </div> {/* RIGHT COLUMN: DETAILS */} <div className="flex-1"> {currentTutorial ? ( <div className="p-4 bg-white rounded shadow"> <h4 className="font-bold text-xl mb-2">Tutorial</h4> <div className="mb-2"> <strong>Title: </strong> {currentTutorial.title} </div> <div className="mb-2"> <strong>Description: </strong> {currentTutorial.description} </div> <div className="mb-2"> <strong>Status: </strong> {currentTutorial.published ? "Published" : "Pending"} </div> <Link to={`/tutorials/${currentTutorial.id}`} className="inline-block bg-yellow-400 text-black px-3 py-1 rounded" > Edit </Link> </div> ) : ( <div> <p>Please click on a Tutorial...</p> </div> )} </div> </div> ); } export default TutorialsList;

Tutorial Component#

A function component under src/pages/Tutorial.jsx for viewing and editing a single tutorial. It uses:

  • useParams() to get :id from the URL
  • useNavigate() to redirect
  • TutorialService for get, update, and delete operations
import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import TutorialService from "../services/tutorial.service"; function Tutorial() { const { id } = useParams(); const navigate = useNavigate(); const [currentTutorial, setCurrentTutorial] = useState({ id: null, title: "", description: "", published: false, }); const [message, setMessage] = useState(""); const getTutorial = (id) => { TutorialService.get(id) .then((response) => { setCurrentTutorial(response.data); console.log(response.data); }) .catch((e) => { console.log(e); }); }; useEffect(() => { if (id) getTutorial(id); }, [id]); const handleInputChange = (event) => { const { name, value } = event.target; setCurrentTutorial({ ...currentTutorial, [name]: value }); }; const updatePublished = (status) => { const data = { ...currentTutorial, published: status, }; TutorialService.update(currentTutorial.id, data) .then((response) => { setCurrentTutorial({ ...currentTutorial, published: status }); console.log(response.data); }) .catch((e) => { console.log(e); }); }; const updateTutorial = () => { TutorialService.update(currentTutorial.id, currentTutorial) .then((response) => { console.log(response.data); setMessage("The tutorial was updated successfully!"); }) .catch((e) => { console.log(e); }); }; const deleteTutorial = () => { TutorialService.remove(currentTutorial.id) .then((response) => { console.log(response.data); navigate("/tutorials"); }) .catch((e) => { console.log(e); }); }; return ( <div> {currentTutorial ? ( <div className="max-w-sm mx-auto p-4 bg-white rounded shadow"> <h4 className="font-bold text-xl mb-2">Edit Tutorial</h4> <div className="mb-2"> <label className="block font-medium" htmlFor="title"> Title </label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" id="title" name="title" value={currentTutorial.title} onChange={handleInputChange} /> </div> <div className="mb-2"> <label className="block font-medium" htmlFor="description"> Description </label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" id="description" name="description" value={currentTutorial.description} onChange={handleInputChange} /> </div> <div className="mb-2"> <strong>Status:</strong> {currentTutorial.published ? "Published" : "Pending"} </div> <div className="space-x-2 mt-2"> {currentTutorial.published ? ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(false)} > Unpublish </button> ) : ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(true)} > Publish </button> )} <button className="bg-red-500 text-white px-3 py-1 rounded" onClick={deleteTutorial} > Delete </button> <button className="bg-green-500 text-white px-3 py-1 rounded" onClick={updateTutorial} > Update </button> </div> {message && <p className="text-green-600 mt-2">{message}</p>} </div> ) : ( <div> <p>Loading tutorial...</p> </div> )} </div> ); } export default Tutorial;

Run the application#

npm run dev

Now you can access the application at http://localhost:5173. Open that URL in your browser. You can now navigate to:

  • /tutorials – see all tutorials
  • /add – add a tutorial
  • /tutorials/:id – edit a specific tutorial

(Make sure your Node/Express back end is running on http://localhost:8080 or update the baseURL in tutorial.service.js accordingly.)

Conclusion#

You've successfully built a full-stack CRUD application using React, Node.js, Express, and MongoDB. This project showcases how to set up a RESTful API with Node.js and Express, manage data with mongoose, and create a responsive frontend with React and Bootstrap.

Happy coding and see you in the next tutorial!

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free