Project Architecture Overview
The MEAN stack (MongoDB, Express, Angular/React, Node) separates concerns clearly. The database stores persistent data, the backend handles business logic and HTTP requests, and the frontend manages the user interface. For this small project, we will create a simple task manager that allows users to add, view, and delete tasks. This architecture ensures that the frontend remains lightweight while the backend handles data validation and storage. Using separate ports for development avoids CORS issues during local testing, which is a common pitfall for beginners.Setting Up the Backend Server
First, initialize the backend directory and install the necessary dependencies. Runnpm init -y followed by npm install express mongoose cors dotenv. Create a file named server.js to serve as the entry point. Modern versions of Mongoose no longer require the useNewUrlParser or useUnifiedTopology options, as they are enabled by default. Including them now triggers deprecation warnings. Instead, pass the connection string directly to mongoose.connect().
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(express.json());
app.use(cors());
// Connect to MongoDB using environment variables
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
app.listen(5000, () => {
console.log('Server running on port 5000');
});
This code sets up an Express server that listens on port 5000. It uses the cors middleware to allow requests from your React frontend, typically running on port 3000. The dotenv package loads environment variables from a .env file, keeping sensitive data like your MongoDB URI out of version control. Always store your connection string in MONGO_URI within your environment file.
Creating the Data Model
Define a Mongoose schema to represent your data. For a task manager, each task needs a title and a completion status. Create a folder namedmodels and inside it, a file called Task.js. Schemas enforce structure and provide built-in validation.
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
completed: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
module.exports = mongoose.model('Task', taskSchema);
The timestamps: true option automatically adds createdAt and updatedAt fields to every document. This is useful for auditing and sorting. By exporting the model, you can import it into your route files. This separation keeps your code modular and easy to test.
Implementing API Routes
Create aroutes folder with a tasks.js file. Here, you define endpoints for CRUD operations. Use async/await to handle database promises cleanly.
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();
res.json(tasks);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Add a new task
router.post('/', async (req, res) => {
const task = new Task({
title: req.body.title
});
try {
const newTask = await task.save();
res.status(201).json(newTask);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
module.exports = router;
In server.js, include these routes using app.use('/api/tasks', tasksRoutes). This prefixes all task-related endpoints with /api/tasks, preventing conflicts with other potential routes. Always wrap database calls in try/catch blocks to handle errors gracefully and return appropriate HTTP status codes.
Building the React Frontend
Generate a React app usingnpx create-react-app client. Install Axios for HTTP requests: npm install axios. In src/App.js, use the useEffect hook to fetch data when the component mounts.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [tasks, setTasks] = useState([]);
const [title, setTitle] = useState('');
useEffect(() => {
fetchTasks();
}, []);
const fetchTasks = async () => {
const res = await axios.get('http://localhost:5000/api/tasks');
setTasks(res.data);
};
const addTask = async () => {
await axios.post('http://localhost:5000/api/tasks', { title });
fetchTasks();
setTitle('');
};
return (
setTitle(e.target.value)} />
{tasks.map(task => - {task.title}
)}
);
}
export default App;
This component maintains state for the list of tasks and the input field. When the user clicks "Add", it sends a POST request to the backend. The backend saves the task and returns the new object, which triggers a refetch to update the UI. This pattern ensures the frontend always reflects the current database state.
Running the Application
Start the backend withnode server.js and the frontend with npm start in the client directory. Ensure your MongoDB Atlas cluster is accessible from your IP address. If you encounter connection issues, check your network settings and firewall rules. For more details on handling specific database errors, see Troubleshooting BSON Type to Date Conversion in MongoDB.
Frequently Asked Questions
Why am I getting a CORS error?
CORS errors occur because browsers block requests from one origin to another for security. Ensure the cors middleware is installed and applied in your Express server before defining routes. You can also configure specific origins if needed.
How do I deploy this full-stack app?
You can deploy the backend on Heroku or Render and the frontend on Vercel or Netlify. Set the MONGO_URI environment variable in your deployment platform's dashboard. Update the frontend API URLs to point to the production backend endpoint.
Can I use TypeScript instead of JavaScript?
Yes, both React and Express support TypeScript natively. You would need to install type definitions for Express and Mongoose using @types/express and @types/mongoose. Define interfaces for your models to get compile-time checking.
What happens if the database connection fails?
If mongoose.connect rejects, the server should log the error and potentially exit. In production, implement a retry mechanism or health checks. Use process.on('unhandledRejection') to catch any unhandled promise rejections during startup.
How do I handle authentication in this setup?
Add a JWT library like jsonwebtoken and bcryptjs. Hash passwords before saving them to the database. After login, generate a token and send it to the client. Verify the token on protected routes using middleware that reads the Authorization header.