Skip to content
Zeha Irawan
Twitter

Integrating React.js frontend and Express.js backend

React.js, Express.js6 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 todo
mkdir server && cd server && npm init -y
touch main.js
npm i express cors mongoose dotenv
npm i -D nodemon

Setup React.js frontend

At the root of the project run

npx create-react-app client
cd 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=5000
MONGO_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 todos
router.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 code
app.use(cors());
// ...existing code

CORS

Modify client/src/App.js

Now you should see the todo in the frontend.

Todo List

Create Todo (POST)

Modify server/routes/api/todos.js to create a todo

// @route POST api/todos
// @desc Create a todo
router.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 todo
router.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 todo
router.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 base
FROM node:20-slim
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy the rest of the application code
COPY . .
# Build the React application
RUN npm run build
# Install a simple server to serve the build
RUN npm install -g serve
# Expose the port the app runs on
EXPOSE 3000
# Command to run the app
CMD ["serve", "-s", "build"]

server/Dockerfile

# Use the official Node.js 20 slim image
FROM node:20-slim
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on
EXPOSE 5000
# Command to run the application
CMD ["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: true
volumes:
mongo-data: