Table of Contents
- Prerequisites
- Project Overview: What We’ll Build
- Setting Up the Frontend with React
- Building React Components
- Setting Up the Backend with Node.js & Express
- Database Integration with MongoDB
- Connecting Frontend and Backend
- Testing the Application
- Deploying the App
- Conclusion
- References
Prerequisites
Before diving in, ensure you have the following tools installed:
- Node.js & npm: Download from nodejs.org (includes npm).
- Code Editor: We’ll use VS Code (free and popular).
- Git: For version control (optional but recommended).
- MongoDB Account: Sign up for free at MongoDB Atlas (we’ll use their cloud database).
- Basic Knowledge: Familiarity with HTML, CSS, JavaScript, and ES6+ syntax (arrow functions, async/await, etc.).
Project Overview: What We’ll Build
Our Task Manager App will let users:
- Add new tasks with a description.
- View all tasks in a list.
- Mark tasks as “complete” (with a strikethrough).
- Delete tasks.
Architecture:
- Frontend: React (UI rendering, state management with hooks).
- Backend: Node.js + Express (REST API endpoints).
- Database: MongoDB (stores tasks).
- Deployment: Frontend on Vercel, Backend on Render, Database on MongoDB Atlas.
Setting Up the Frontend with React
Let’s start by creating the React frontend.
Step 1: Create a React App
Open your terminal and run:
npx create-react-app task-manager-frontend
cd task-manager-frontend
This creates a new React project with a basic folder structure.
Step 2: Install Dependencies
We’ll need a few libraries:
axios: To make API calls to the backend.react-router-dom: For navigation (though our app is simple, it’s good practice).
Install them with:
npm install axios react-router-dom
Step 3: Project Structure
Clean up the default files (delete logo.svg, App.test.js, etc.) and organize your src folder like this:
src/
├── components/ # Reusable UI parts (Navbar, TaskForm, TaskList)
├── pages/ # Full pages (Home)
├── services/ # API calls to backend
├── App.js # Main app component
├── index.js # Entry point
└── .env # Environment variables (e.g., backend URL)
Building React Components
Let’s build the core components for our app.
1. Navbar Component
A simple header for the app. Create src/components/Navbar.js:
// src/components/Navbar.js
import React from 'react';
const Navbar = () => {
return (
<nav style={styles.navbar}>
<h1>Task Manager</h1>
</nav>
);
};
const styles = {
navbar: {
background: '#2c3e50',
color: 'white',
padding: '1rem',
textAlign: 'center',
},
};
export default Navbar;
2. TaskForm Component
A form to add new tasks. Create src/components/TaskForm.js:
// src/components/TaskForm.js
import React, { useState } from 'react';
const TaskForm = ({ addTask }) => {
const [description, setDescription] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!description.trim()) return; // Ignore empty tasks
addTask(description); // Pass task to parent component
setDescription(''); // Clear input
};
return (
<form onSubmit={handleSubmit} style={styles.form}>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add a new task..."
style={styles.input}
/>
<button type="submit" style={styles.button}>Add Task</button>
</form>
);
};
const styles = {
form: {
display: 'flex',
gap: '0.5rem',
margin: '1rem 0',
},
input: {
flex: 1,
padding: '0.5rem',
fontSize: '1rem',
},
button: {
padding: '0.5rem 1rem',
background: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
},
};
export default TaskForm;
3. TaskItem Component
A single task in the list. Create src/components/TaskItem.js:
// src/components/TaskItem.js
import React from 'react';
const TaskItem = ({ task, onDelete, onToggleComplete }) => {
return (
<div style={styles.taskItem}>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleComplete(task._id)}
style={styles.checkbox}
/>
<span style={task.completed ? styles.completed : styles.text}>
{task.description}
</span>
<button
onClick={() => onDelete(task._id)}
style={styles.deleteButton}
>
×
</button>
</div>
);
};
const styles = {
taskItem: {
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.75rem',
border: '1px solid #ddd',
borderRadius: '4px',
margin: '0.5rem 0',
},
checkbox: {
width: '1.2rem',
height: '1.2rem',
},
text: {
flex: 1,
fontSize: '1rem',
},
completed: {
flex: 1,
fontSize: '1rem',
textDecoration: 'line-through',
color: '#888',
},
deleteButton: {
background: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '50%',
width: '1.5rem',
height: '1.5rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
};
export default TaskItem;
4. TaskList Component
Displays all tasks. Create src/components/TaskList.js:
// src/components/TaskList.js
import React from 'react';
import TaskItem from './TaskItem';
const TaskList = ({ tasks, onDelete, onToggleComplete }) => {
if (tasks.length === 0) {
return <p style={styles.emptyMessage}>No tasks yet. Add a task above!</p>;
}
return (
<div>
{tasks.map((task) => (
<TaskItem
key={task._id}
task={task}
onDelete={onDelete}
onToggleComplete={onToggleComplete}
/>
))}
</div>
);
};
const styles = {
emptyMessage: {
color: '#888',
textAlign: 'center',
padding: '1rem',
},
};
export default TaskList;
5. Home Page
Combine components into a home page. Create src/pages/Home.js:
// src/pages/Home.js
import React, { useState, useEffect } from 'react';
import TaskForm from '../components/TaskForm';
import TaskList from '../components/TaskList';
import axios from 'axios';
const Home = () => {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch tasks from backend on load
useEffect(() => {
const fetchTasks = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/tasks`);
setTasks(res.data);
} catch (err) {
setError('Failed to fetch tasks. Please try again later.');
console.error(err);
} finally {
setLoading(false);
}
};
fetchTasks();
}, []);
// Add a new task
const addTask = async (description) => {
try {
const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/tasks`, {
description,
});
setTasks([...tasks, res.data]); // Add new task to state
} catch (err) {
setError('Failed to add task.');
console.error(err);
}
};
// Delete a task
const deleteTask = async (id) => {
try {
await axios.delete(`${process.env.REACT_APP_API_URL}/api/tasks/${id}`);
setTasks(tasks.filter((task) => task._id !== id)); // Remove from state
} catch (err) {
setError('Failed to delete task.');
console.error(err);
}
};
// Toggle task completion
const toggleComplete = async (id) => {
try {
const taskToUpdate = tasks.find((task) => task._id === id);
const res = await axios.patch(`${process.env.REACT_APP_API_URL}/api/tasks/${id}`, {
completed: !taskToUpdate.completed,
});
setTasks(tasks.map((task) => (task._id === id ? res.data : task)));
} catch (err) {
setError('Failed to update task.');
console.error(err);
}
};
if (loading) return <p>Loading tasks...</p>;
if (error) return <p style={{ color: 'red' }}>{error}</p>;
return (
<div style={styles.container}>
<h2>My Tasks</h2>
<TaskForm addTask={addTask} />
<TaskList
tasks={tasks}
onDelete={deleteTask}
onToggleComplete={toggleComplete}
/>
</div>
);
};
const styles = {
container: {
maxWidth: '600px',
margin: '0 auto',
padding: '1rem',
},
};
export default Home;
6. Update App.js
Finally, update src/App.js to use the Home page:
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
function App() {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
}
export default App;
Setting Up the Backend with Node.js & Express
Now, let’s build the backend API to handle data.
Step 1: Create a Backend Folder
Open a new terminal and run:
mkdir task-manager-backend
cd task-manager-backend
npm init -y
Step 2: Install Backend Dependencies
npm install express mongoose cors dotenv
npm install --save-dev nodemon
express: Web framework for Node.js.mongoose: ODM for MongoDB (simplifies database interactions).cors: Handles Cross-Origin Resource Sharing (allows frontend to call the backend).dotenv: Manages environment variables.nodemon: Auto-restarts the server during development.
Step 3: Configure Environment Variables
Create a .env file in the backend folder:
PORT=5000
MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.mongodb.net/taskmanager?retryWrites=true&w=majority
Replace <username> and <password> with your MongoDB Atlas credentials (we’ll set this up next).
Step 4: Create the Server
Create server.js:
// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
// Middleware
app.use(cors()); // Allow cross-origin requests
app.use(express.json()); // Parse JSON bodies
// Connect to MongoDB
mongoose
.connect(process.env.MONGODB_URI)
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));
// Basic route
app.get('/', (req, res) => {
res.send('Task Manager API is running!');
});
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Database Integration with MongoDB
Step 1: Set Up MongoDB Atlas
- Go to MongoDB Atlas and sign up.
- Create a new cluster (free tier is fine).
- Under “Database Access”, add a user (username/password).
- Under “Network Access”, allow access from anywhere (temporarily for development).
- Click “Connect” → “Connect your application” → Copy the connection string.
- Replace
<username>and<password>in your backend.envfile with the user credentials.
Step 2: Create a Task Model
Create models/Task.js in the backend folder:
// models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
description: {
type: String,
required: true,
trim: true,
},
completed: {
type: Boolean,
default: false,
},
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Task', taskSchema);
Step 3: Add API Routes
Create routes/tasks.js:
// routes/tasks.js
const express = require('express');
const router = express.Router();
const Task = require('../models/Task');
// GET all tasks
router.get('/', async (req, res) => {
try {
const tasks = await Task.find().sort({ createdAt: -1 }); // Newest first
res.json(tasks);
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// POST a new task
router.post('/', async (req, res) => {
try {
const newTask = new Task({
description: req.body.description,
});
const task = await newTask.save();
res.json(task);
} catch (err) {
res.status(400).json({ error: 'Invalid task data' });
}
});
// PATCH (update) a task's completion status
router.patch('/:id', async (req, res) => {
try {
const task = await Task.findById(req.params.id);
if (!task) return res.status(404).json({ error: 'Task not found' });
task.completed = req.body.completed;
await task.save();
res.json(task);
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// DELETE a task
router.delete('/:id', async (req, res) => {
try {
const task = await Task.findByIdAndDelete(req.params.id);
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json({ message: 'Task deleted' });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;
Step 4: Update Server.js to Use Routes
Add this to server.js (after connecting to MongoDB):
// Import routes
const taskRoutes = require('./routes/tasks');
app.use('/api/tasks', taskRoutes); // All task routes start with /api/tasks
Step 5: Update package.json for Nodemon
Add a script to package.json to run the server with nodemon:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Connecting Frontend and Backend
Step 1: Set Up Frontend Environment Variable
Create a .env file in the frontend folder:
REACT_APP_API_URL=http://localhost:5000
(We’ll update this to the deployed backend URL later.)
Step 2: Run Both Servers
- Backend: In the backend terminal, run
npm run dev(starts on port 5000). - Frontend: In the frontend terminal, run
npm start(starts on port 3000).
Testing the Application
- Open
http://localhost:3000in your browser. - Add a task (e.g., “Learn React”).
- Check if it appears in the list.
- Mark it as complete (strikethrough should appear).
- Delete the task.
If everything works, your full-stack app is functioning!
Deployment
1. Deploy the Frontend (Vercel)
- Push your frontend code to GitHub.
- Go to Vercel and sign up with GitHub.
- Import your frontend repository.
- Set the environment variable
REACT_APP_API_URLto your deployed backend URL (we’ll get this next). - Click “Deploy”.
2. Deploy the Backend (Render)
- Push your backend code to GitHub.
- Go to Render and sign up.
- Create a new “Web Service”.
- Import your backend repository.
- Set:
- Build Command:
npm install - Start Command:
npm start - Environment Variables: Add
MONGODB_URI(your Atlas connection string).
- Build Command:
- Click “Create Web Service”. Copy the deployed URL (e.g.,
https://task-manager-backend.onrender.com).
3. Update Frontend Environment Variable
Go back to Vercel, update REACT_APP_API_URL to your Render backend URL, and redeploy.
Conclusion
Congratulations! You’ve built a full-stack application with React, Node.js, Express, and MongoDB—from scratch! You’ve learned:
- How to structure a React app with components and hooks.
- How to build a REST API with Express.
- How to connect a database (MongoDB) to the backend.
- How to deploy both frontend and backend.
Next steps to expand your app:
- Add user authentication (JWT, OAuth).
- Implement state management with Redux or Context API.
- Add due dates or categories to tasks.
- Style with a UI library like Material-UI or Tailwind CSS.