A full-stack Todo application built with the MERN stack (MongoDB Atlas, Express.js, React, Node.js), deployed on an Ubuntu EC2 instance.
- Project Overview
- Prerequisites
- Step 1 – Backend Configuration
- Step 2 – Frontend Creation
- API Endpoints
- Screenshots
This application allows users to manage a list of tasks through a browser interface. The React frontend communicates with an Express REST API, which reads and writes data to a cloud-hosted MongoDB Atlas database.
| Layer | Technology | Role |
|---|---|---|
| MongoDB | MongoDB Atlas | Cloud NoSQL database |
| Express.js | Express 4.x | REST API & routing |
| React | React 18.x | Frontend UI |
| Node.js | Node.js 18.x | JavaScript runtime |
Supported operations:
- Display all tasks —
HTTP GET - Add a new task —
HTTP POST - Delete an existing task —
HTTP DELETE
- Ubuntu EC2 instance (with SSH access)
- MongoDB Atlas account (free tier)
- Postman installed locally
- Port
5000and3000open in EC2 Security Group inbound rules
Start by updating and upgrading the Ubuntu package list to ensure all system packages are current before installing new software.
# Update package list
sudo apt update
# Upgrade installed packages
sudo apt upgradeNode.js 18.x is installed from the official NodeSource repository to ensure we get a stable LTS version rather than the outdated version available in Ubuntu's default repositories.
# Fetch the Node.js 18.x setup script from NodeSource
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
# Install Node.js (this also installs npm)
sudo apt-get install -y nodejsNote: The command above installs both
nodejsandnpm. NPM is a package manager for Node.js — similar toaptfor Ubuntu — and is used to install Node modules, packages, and manage dependency conflicts.
# Verify Node.js installation
node -v
# Verify npm installation
npm -v# Create the project directory
mkdir Todo
# Confirm the directory was created
ls
# Change into the Todo directory
cd Todo
# Initialise the project (creates package.json)
npm initFollow the prompts after
npm init. You can press Enter to accept defaults for each field, then typeyeswhen asked to write thepackage.jsonfile.
# Confirm package.json was created
lsShould show the
npm initprompts being accepted in the terminal, followed bylsoutput confirmingpackage.jsonis present in theTododirectory.
Express is a framework for Node.js that simplifies route definition, HTTP method handling, and many other low-level details of building a web server.
# Install Express
npm install express
# Create the main server entry file
touch index.js
# Confirm index.js was created
ls
# Install the dotenv module (for environment variables)
npm install dotenvOpen index.js and add the initial server code:
vim index.js// index.js
const express = require('express');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.use((req, res, next) => {
res.send('Welcome to Express');
});
app.listen(port, () => {
console.log(`Server running on port ${port}`)
});Use
:wto save and:qato exit vim.
# Start the server to confirm it works
node index.jsNow open port 5000 in your EC2 Security Group inbound rules (the same way TCP port 80 was opened for Nginx), then test in a browser:
http://<PublicIP-or-PublicDNS>:5000
Create a routes directory and an api.js file to define the application's API endpoints:
mkdir routes && cd routes && touch api.js
vim api.js// routes/api.js
const express = require('express');
const router = express.Router();
const Todo = require('../models/todo');
router.get('/todos', (req, res, next) => {
Todo.find({}, 'action')
.then(data => res.json(data))
.catch(next);
});
router.post('/todos', (req, res, next) => {
if (req.body.action) {
Todo.create(req.body)
.then(data => res.json(data))
.catch(next);
} else {
res.json({ error: "The input field is empty" });
}
});
router.delete('/todos/:id', (req, res, next) => {
Todo.findOneAndDelete({ _id: req.params.id })
.then(data => res.json(data))
.catch(next);
});
module.exports = router;MongoDB is a NoSQL database, so instead of tables and rows it uses collections and documents. A Mongoose schema defines the structure of those documents.
# Go back to the Todo root directory
cd ..
# Install Mongoose
npm install mongoose
# Create the models folder, navigate into it, and create the schema file
mkdir models && cd models && touch todo.js
# Open the file
vim todo.js// models/todo.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create schema for todo
const TodoSchema = new Schema({
action: {
type: String,
required: [true, 'The todo text field is required']
}
});
// Create model for todo
const Todo = mongoose.model('todo', TodoSchema);
module.exports = Todo;Instead of a local MongoDB installation, this project uses MongoDB Atlas — a fully managed cloud database service. The free M0 tier is sufficient for this application.
- Sign up or log in at cloud.mongodb.com
- Click "Build a Database" → Select the Free (M0) tier
- Choose AWS as the cloud provider and select a region near you
- Name your cluster (e.g.,
TodoCluster) and click Create
Should show the Atlas dashboard with your cluster listed and a green "Active" status, confirming the cluster is running.
In Atlas, navigate to Network Access → Add IP Address:
- For this project, allow access from anywhere:
0.0.0.0/0 - Important: Set the expiry to 1 Week (not the default 6 Hours) to avoid losing access mid-project
Navigate to Database Access → Add New Database User:
Username: todoadmin
Password: <your secure password>
Role: Read and Write to Any Database
Get your connection string from Atlas by clicking Connect → Connect your application, then:
# In the Todo root directory, create the .env file
touch .env
vi .envAdd your Atlas connection string:
DB = 'mongodb+srv://<username>:<password>@<cluster-address>/<dbname>?retryWrites=true&w=majority'Replace
<username>,<password>,<cluster-address>, and<dbname>with your actual Atlas credentials.⚠️ Never commit.envto GitHub. Add it to.gitignore.
Now update index.js to connect to MongoDB Atlas using the .env variable:
vim index.js
# Press esc, type :, then %d and Enter to clear the file, then press i to insert// index.js (updated)
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const routes = require('./routes/api');
const path = require('path');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
// Connect to the database
mongoose.connect(process.env.DB, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log(`Database connected successfully`))
.catch(err => console.log(err));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.use(bodyParser.json());
app.use('/api', routes);
app.use((err, req, res, next) => {
console.log(err);
next();
});
app.listen(port, () => {
console.log(`Server running on port ${port}`)
});# Start the server
node index.jsBefore building the frontend, all API endpoints are tested using Postman to confirm the backend and database are working correctly.
Note: Ensure the
Content-Typeheader is set toapplication/jsonfor POST requests.
Method: POST
URL: http://<PublicIP-or-PublicDNS>:5000/api/todos
Headers: Content-Type: application/json
Body (raw JSON):
{
"action": "Finish project 8 and 9"
}
Should show Postman with the POST request to
/api/todos, the JSON body visible in the Body tab, a 200 status, and the response showing the newly created todo document with its MongoDB_idandactionfield.
Method: GET
URL: http://<PublicIP-or-PublicDNS>:5000/api/todos
Should show Postman with the GET request returning a 200 OK status and a JSON array of todo objects in the response body, confirming data is being retrieved from MongoDB Atlas.
Method: DELETE
URL: http://<PublicIP-or-PublicDNS>:5000/api/todos/<todo_id>
Replace
<todo_id>with the_idvalue from a previous GET or POST response.
Should show Postman with the DELETE request using a real
_idin the URL and a 200 OK status, with the deleted document returned in the response body.
From the Todo root directory, create the React frontend using create-react-app:
npx create-react-app clientThis creates a client/ folder inside Todo/ containing all the React source code.
# Install concurrently — runs backend and frontend simultaneously from one terminal
npm install concurrently --save-dev
# Install nodemon — auto-restarts the server when backend files change
npm install nodemon --save-devOpen Todo/package.json and replace the scripts section with:
"scripts": {
"start": "node index.js",
"start-watch": "nodemon index.js",
"dev": "concurrently \"npm run start-watch\" \"cd client && npm start\""
}This dev script runs both the Express backend (with nodemon) and the React frontend simultaneously from a single terminal command.
cd client
vi package.jsonAdd the following key-value pair anywhere in the JSON object:
"proxy": "http://localhost:5000"This proxy setting allows the React app to make API calls using a relative path like
/api/todosinstead of the full URLhttp://localhost:5000/api/todos, making the frontend code cleaner and environment-agnostic.
Now run the full application from the Todo directory:
cd ..
npm run devNote: Open TCP port
3000in your EC2 Security Group inbound rules so the React app is accessible from the browser.
Should show the React Todo application loaded in the browser at
http://localhost:3000(or the EC2 public IP on port 3000), with the task input and list visible.
From the Todo directory, navigate to the React source folder and create the component files:
cd client/src
mkdir components
cd components
touch Input.js ListTodo.js Todo.jsvi Input.jsimport React, { Component } from 'react';
import axios from 'axios';
class Input extends Component {
state = { action: "" }
addTodo = () => {
const task = { action: this.state.action };
if (task.action && task.action.length > 0) {
axios.post('/api/todos', task)
.then(res => {
if (res.data) {
this.props.getTodos();
this.setState({ action: "" });
}
})
.catch(err => console.log(err));
} else {
console.log('input field required');
}
}
handleChange = (e) => {
this.setState({ action: e.target.value });
}
render() {
let { action } = this.state;
return (
<div>
<input type="text" onChange={this.handleChange} value={action} />
<button onClick={this.addTodo}>add todo</button>
</div>
);
}
}
export default Input;Install Axios (HTTP client used by the Input component):
# Navigate back to the client folder
cd ../..
npm install axioscd src/components
vi ListTodo.jsimport React from 'react';
const ListTodo = ({ todos, deleteTodo }) => {
return (
<ul>
{
todos && todos.length > 0 ?
todos.map(todo => (
<li key={todo._id} onClick={() => deleteTodo(todo._id)}>
{todo.action}
</li>
))
:
<li>No todo(s) left</li>
}
</ul>
);
}
export default ListTodo;vi Todo.jsimport React, { Component } from 'react';
import axios from 'axios';
import Input from './Input';
import ListTodo from './ListTodo';
class Todo extends Component {
state = { todos: [] }
componentDidMount() {
this.getTodos();
}
getTodos = () => {
axios.get('/api/todos')
.then(res => {
if (res.data) {
this.setState({ todos: res.data });
}
})
.catch(err => console.log(err));
}
deleteTodo = (id) => {
axios.delete(`/api/todos/${id}`)
.then(res => {
if (res.data) {
this.getTodos();
}
})
.catch(err => console.log(err));
}
render() {
let { todos } = this.state;
return (
<div>
<h1>My Todo(s)</h1>
<Input getTodos={this.getTodos} />
<ListTodo todos={todos} deleteTodo={this.deleteTodo} />
</div>
);
}
}
export default Todo;Should show the React Todo app in the browser with at least one task added and displayed in the list — demonstrating the complete MERN stack working end-to-end: React frontend → Express API → MongoDB Atlas.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/todos |
Retrieve all todo tasks |
POST |
/api/todos |
Create a new todo task |
DELETE |
/api/todos/:id |
Delete a task by its MongoDB _id |
David Babayo Steghub project 3






