Integrating React.js frontend and Express.js backend
— React.js, Express.js — 6 min read
You can find the complete code for this tutorial in GitHub.
Setup Express.js backend
Prerequisites
- node 18+ installed
- npm or yarn
- mongodb installed For OSX
mkdir todo && cd todomkdir server && cd server && npm init -ytouch main.jsnpm i express cors mongoose dotenvnpm i -D nodemon
Setup React.js frontend
At the root of the project run
npx create-react-app clientcd client && npm start
server/main.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/', (req, res) => res.send('API Running'));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
modify package.json
"scripts": { "dev": "nodemon main.js"}
then run npm run dev
it should restart the server whenever you modify the code.
You can test it with curl http://localhost:5000
or with postman.
You should see API Running
in the postman or in the terminal.
Connecting Express.js with MongoDB
Execute the following commands in the terminal
-
mongosh
-
use todo
It will create a database called todo
if it doesn't exist.
show dbs
You should see todo
in the list.
db.createCollection('todos')
db.todos.insert({ title: 'Task 1', description: 'Description 1' })
Create a new file .env
PORT=5000MONGO_URI=mongodb://localhost:27017/todo
server/config/db.js
const mongoose = require('mongoose');require('dotenv').config();
const db = process.env.MONGO_URI;
const connectDB = async () => { try { await mongoose.connect(db);
console.log('MongoDB Connected...'); } catch (err) { console.error(err.message,'ERROR'); // Exit process with failure process.exit(1); }};
module.exports = connectDB;
modify main.js
to use the connectDB
function
// ...existing code// below app.use(express.json());connectDB();// ...existing code
You should see MongoDB Connected...
in the terminal.
Create a model
server/models/Todo.js
const mongoose = require('mongoose');
const TodoSchema = new mongoose.Schema({ title: { type: String, required: true, }, description: { type: String, },});
const Todo = mongoose.model('todo', TodoSchema);
module.exports = Todo;
Create a route and implementing Read (GET)
Create server/routes/api/todos.js
const express = require('express');const router = express.Router();const Todo = require('../../models/Todo');
// @route GET api/todos// @desc Get all todosrouter.get('/', auth, async (req, res) => { try { const todos = await Todo.find(); res.json(todos); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); }});
module.exports = router;
And modify main.js
to use the new route
// ...existing code
app.use('/api/todos', require('./routes/api/todos'));// ...existing code
To test it run
curl localhost:5000/api/todos
You should see the todo in the json format.
[{"_id":"6705a3adaaf170175b4c3d0d","name":"Task 1","description":"Description 1"}]
Displaying the data in the frontend
Modify client/src/App.js
import { useState, useEffect } from 'react';
function App() {
const [todos, setTodos] = useState([]);
const fetchTodos = async () => { const response = await fetch('http://localhost:5000/api/todos'); const data = await response.json(); setTodos(data); };
useEffect(() => { fetchTodos(); }, []);
return ( <div style={{ maxWidth: '600px', marginRight: 'auto', padding: '2rem' }}> <h1>Todo List</h1> <ul> {todos.map((todo) => ( <li key={todo._id} style={{ padding: '1rem', borderBottom: '1px solid #ccc' }}> <strong>{todo.title}</strong> <p>{todo.description}</p> </li> ))} </ul> </div> );}
export default App;
Handling CORS
Modify server/main.js
to use cors
const cors = require('cors');// ...existing codeapp.use(cors());// ...existing code
Modify client/src/App.js
Now you should see the todo in the frontend.
Create Todo (POST)
Modify server/routes/api/todos.js
to create a todo
// @route POST api/todos// @desc Create a todorouter.post("/", async (req, res) => { const { title, description } = req.body; try { const newTodo = new Todo({ title, description }); await newTodo.save(); res.json(newTodo); } catch (err) { console.error(err.message); res.status(500).send("Server Error"); }});
And then test it with
curl -X POST http://localhost:5000/api/todos \-H "Content-Type: application/json" \-d '{"title": "Your Todo Title", "description": "Your Todo Description"}'
The key concept here is that the data is sent in JSON format, and then we will display it in the frontend.
I'm using state management with React useState
Hooks, so everytime we make a change with API call (POST, PUT, DELETE), we need to update the state.
For example, in client/src/components/TodoForm.jsx
I'm reusing the same component to handle the todo creation and update.
import React, { useState, useEffect } from "react";
const TodoForm = ({ setTodos, selectedTodo, todos }) => { const [title, setTitle] = useState(""); const [description, setDescription] = useState("");
useEffect(() => { if (selectedTodo) { setTitle(selectedTodo.title); setDescription(selectedTodo.description); } else { setTitle(""); setDescription(""); } }, [selectedTodo]);
const handleSubmit = async (e) => { e.preventDefault();
if (selectedTodo) { const response = await fetch( `http://localhost:5000/api/todos/${selectedTodo._id}`, { method: "PUT", body: JSON.stringify({ title, description }), headers: { "Content-Type": "application/json" }, }, ); const data = await response.json(); setTodos((prev) => prev.map((todo) => (todo._id === selectedTodo._id ? data : todo)), ); alert("Todo updated successfully"); } else { const response = await fetch("http://localhost:5000/api/todos", { method: "POST", body: JSON.stringify({ title, description }), headers: { "Content-Type": "application/json" }, }); const data = await response.json(); setTodos((prev) => [...prev, data]);
setTitle(""); setDescription(""); alert("Todo created successfully"); } };
return ( <div> <h2>{selectedTodo ? "Update" : "Add"} Todo</h2> <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "1rem", padding: "1rem", }} > <input placeholder="Title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <textarea placeholder="Description" value={description} onChange={(e) => setDescription(e.target.value)} /> <button type="submit">{selectedTodo ? "Update" : "Add"} Todo</button> </form> </div> );};
export default TodoForm;
Update Todo (PUT)
Modify server/routes/api/todos.js
to update a todo
// @route PUT api/todos/:id// @desc Update a todorouter.put("/:id", async (req, res) => { const { title, description } = req.body; try { const updatedTodo = await Todo.findByIdAndUpdate(req.params.id, { title, description }, { new: true }); res.json(updatedTodo); } catch (err) { console.error(err.message); res.status(500).send("Server Error"); }});
And then test it with
curl -X PUT http://localhost:5000/api/todos/6705a3adaaf170175b4c3d0d \ 29ms-H "Content-Type: application/json" \-d '{"title": "Absolute new"}'
Delete Todo (DELETE)
Modify server/routes/api/todos.js
to delete a todo
// @route DELETE api/todos/:id// @desc Delete a todorouter.delete("/:id", async (req, res) => { try { await Todo.findByIdAndDelete(req.params.id); res.json({ msg: "Todo deleted" }); } catch (err) { console.error(err.message); res.status(500).send("Server Error"); }});
And then test it with
curl -X DELETE http://localhost:5000/api/todos/6705a3adaaf170175b4c3d0d
Dockerizing the application
You will need two Dockerfiles, one for the backend and one for the frontend.
client/Dockerfile
# Use the official slim Node.js image as a baseFROM node:20-slim
# Set the working directoryWORKDIR /app
# Copy package.json and package-lock.jsonCOPY package*.json ./
# Install dependenciesRUN npm install --production
# Copy the rest of the application codeCOPY . .
# Build the React applicationRUN npm run build
# Install a simple server to serve the buildRUN npm install -g serve
# Expose the port the app runs onEXPOSE 3000
# Command to run the appCMD ["serve", "-s", "build"]
server/Dockerfile
# Use the official Node.js 20 slim imageFROM node:20-slim
# Set the working directoryWORKDIR /app
# Copy package.json and package-lock.jsonCOPY package*.json ./
# Install dependenciesRUN npm install --production
# Copy the rest of the application codeCOPY . .
# Expose the port the app runs onEXPOSE 5000
# Command to run the applicationCMD ["node", "main.js"]
And then for easier build process, create a docker-compose.yml
file
services: backend: build: context: ./server ports: - "5000:5000" environment: - MONGODB_URI=mongodb://mongo:27017/todo depends_on: - mongo command: npm start
frontend: build: context: ./client ports: - "3000:3000" depends_on: - backend command: npm start
mongo: image: mongo restart: always ports: - "27017:27017" volumes: - mongo-data:/data/db environment: ME_CONFIG_BASICAUTH: truevolumes: mongo-data: