Build a Task Manager with MEAN Stack
Wire MongoDB, Express, Angular, and Node.js into a full-stack task manager.
Introduction
30 Second Summary
Every app you use daily, from your email to your streaming service, has a frontend talking to a backend that stores data in a database. Building that full loop yourself is the single most valuable skill in web development.
In this project, you will build a Task Manager application using the MEAN stack. You will wire together a MongoDB database, an Express API, and an Angular frontend, all running on Node.js.
What You'll Build
A full-stack Task Manager that lets you create, complete, and delete tasks through an Angular UI backed by an Express REST API and a cloud MongoDB Atlas database.
By the end of this project, you'll have:
- An Angular standalone component that renders a task list and a form to create new tasks.
- An Express.js REST API with full CRUD endpoints for tasks.
- A Mongoose model with schema validation connected to a free MongoDB Atlas cluster storing your data in the cloud.
- Secret Mission: Add priority levels with color-coded labels and filtering to your task manager.
Are there any prerequisites?
You should have a basic understanding of JavaScript or TypeScript. Familiarity with the terminal is helpful but not required.
Build the Express API
Your development environment is set up and your MongoDB Atlas cluster is waiting for data. Now it's time to build the backend that will power your task manager.
In this step, you'll create a REST API using Express.js that handles all the task operations: creating, reading, updating, and deleting. Mongoose connects your API to MongoDB Atlas and validates every task before it's saved.
In this step, get ready to:
- Create the API project directory and install dependencies.
- Set up Express with a Mongoose connection to your Atlas cluster.
- Add CRUD routes and start the server.
Create the project directory and install dependencies
Every Express project lives in its own folder with its own package.json file that tracks the project's dependencies.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your API folder and move into it by running these commands:
mkdir task-api
cd task-api
- Confirm the folder exists by running:
ls
You should see the task-api folder listed in the output.
- Initialize a new Node.js project by running:
npm init -y
What does this command do?
The npm init -y command creates a package.json file with default values. This file is your project's manifest that tracks the name, version, and dependencies your project needs.
- Install Express, Mongoose, and the CORS middleware by running:
npm install express@5.2.1 mongoose@9.6.2 cors@2.8.5
You should see output confirming that three packages were added.
What are these packages?
- express@5.2.1 is the web framework that handles HTTP requests and routing.
- mongoose@9.6.2 is the ODM (Object Document Mapper) that connects to MongoDB and validates your data.
- cors@2.8.5 enables cross-origin requests so your Angular frontend (port 4200) can talk to this API (port 3000).
Seeing permission errors or install failures?
- Make sure you ran cd task-api before installing. The packages need to install inside your project folder.
- If you see EACCES errors on macOS, try prefixing the install command with sudo.
Why is npm install failing in my task-api folder?
Now you need to update package.json to match the exact configuration your project needs. The npm init -y command created defaults that need adjusting.
- Open package.json in your code editor.
- Replace its entire contents with this configuration:
{
"name": "task-api",
"version": "1.0.0",
"description": "Express API for MEAN stack task manager",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"cors": "2.8.5",
"express": "5.2.1",
"mongoose": "9.6.2"
}
}
What changed from the defaults?
- main points to server.js instead of index.js. This tells Node.js which file is your app's entry point.
- The start script runs node server.js so you can start the server with npm start.
- The dependency versions are pinned exactly (no ^ prefix) for reproducibility.
- Save package.json.
You won't see a visible change yet. The next chunk creates the server file that uses these dependencies.
Set up Express with the Mongoose connection and Task model
Your API needs three things to function: a web server to handle requests, a database connection to persist data, and a schema that defines what a task looks like.
- Create a new file called server.js inside your task-api folder.
- Add the Express setup and Mongoose connection by pasting this code:
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
const app = express();
const PORT = 3000;
// Enable cross-origin requests and JSON body parsing
app.use(cors());
app.use(express.json());
// Connect to MongoDB Atlas
mongoose.connect('mongodb+srv://your-username:your-password@your-cluster.mongodb.net/taskmanager')
.then(() => console.log('Connected to MongoDB Atlas'))
.catch(err => console.error('MongoDB connection error:', err));
What does this code do?
- The import statements load the three packages you installed. Node.js v24 detects ES module syntax automatically, so no extra configuration is needed.
- app.use(cors()) allows your Angular app (running on a different port) to make requests to this API.
- app.use(express.json()) parses incoming JSON request bodies so you can read req.body in your routes.
- mongoose.connect() opens a connection to your MongoDB Atlas cluster. The .then() and .catch() log whether the connection succeeded or failed.
Replace the connection string placeholders
Replace your-username, your-password, and your-cluster in the connection string with the actual values from your MongoDB Atlas dashboard. You can find your connection string by clicking Connect on your cluster.
Now define what a task looks like in your database. Mongoose uses schemas to enforce a structure on your documents.
- Add the Task schema and model below the Mongoose connection in server.js:
// Define the shape of a task document
const taskSchema = new mongoose.Schema({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now }
});
// Create the Task model from the schema
const Task = mongoose.model('Task', taskSchema);
What does this code do?
- taskSchema defines three fields: title (required string), completed (boolean defaulting to false), and createdAt (date defaulting to the current time).
- mongoose.model('Task', taskSchema) creates a model you can use to create, query, update, and delete task documents. Mongoose automatically creates a tasks collection in your database.
- Save server.js.
You won't see a change yet. The next chunk adds routes that use this model to respond to HTTP requests.
Add CRUD routes and start the server
CRUD stands for Create, Read, Update, Delete. Each operation maps to an HTTP method: POST, GET, PUT, and DELETE.
- Add the GET and POST routes below the Task model in server.js:
// GET all tasks, newest first
app.get('/api/tasks', async (req, res) => {
const tasks = await Task.find().sort({ createdAt: -1 });
res.json(tasks);
});
// POST a new task
app.post('/api/tasks', async (req, res) => {
const task = new Task({ title: req.body.title });
const saved = await task.save();
res.status(201).json(saved);
});
What do these routes do?
- GET /api/tasks fetches all tasks from MongoDB, sorted so the newest appear first.
- POST /api/tasks creates a new task using the title from the request body. It responds with status 201 (Created) and the saved document.
- Add the PUT and DELETE routes plus the server start below the POST route:
// PUT (update) a task by ID
app.put('/api/tasks/:id', async (req, res) => {
const task = await Task.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
// DELETE a task by ID
app.delete('/api/tasks/:id', async (req, res) => {
const task = await Task.findByIdAndDelete(req.params.id);
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json({ message: 'Task deleted' });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What do these routes do?
- PUT /api/tasks/:id updates a task with the fields sent in the request body. The { new: true } option returns the updated document instead of the old one.
- DELETE /api/tasks/:id removes a task from the database permanently.
- app.listen(PORT) starts the Express server on port 3000 and logs a confirmation message.
- Save server.js.
- Start the server by running:
node server.js
You should see two messages in your terminal: Connected to MongoDB Atlas and Server running on http://localhost:3000.
Not seeing both messages?
- If you see a MongoDB connection error, double-check that you replaced the placeholder values in your connection string with your actual Atlas credentials.
- Make sure your IP address is still in the Atlas access list. If your IP changed since Step 1, go to Network Access in Atlas and add your current IP.
- If you see a syntax error, confirm you saved all the code sections in server.js and didn't accidentally delete a closing bracket or parenthesis.
Why can't my Express server connect to MongoDB Atlas?
- Stop the server by pressing Ctrl+C in your terminal. You'll restart it later when the frontend is ready.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete package.json:
{
"name": "task-api",
"version": "1.0.0",
"description": "Express API for MEAN stack task manager",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"cors": "2.8.5",
"express": "5.2.1",
"mongoose": "9.6.2"
}
}
Here is the complete server.js:
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
mongoose.connect('mongodb+srv://your-username:your-password@your-cluster.mongodb.net/taskmanager')
.then(() => console.log('Connected to MongoDB Atlas'))
.catch(err => console.error('MongoDB connection error:', err));
const taskSchema = new mongoose.Schema({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now }
});
const Task = mongoose.model('Task', taskSchema);
app.get('/api/tasks', async (req, res) => {
const tasks = await Task.find().sort({ createdAt: -1 });
res.json(tasks);
});
app.post('/api/tasks', async (req, res) => {
const task = new Task({ title: req.body.title });
const saved = await task.save();
res.status(201).json(saved);
});
app.put('/api/tasks/:id', async (req, res) => {
const task = await Task.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.delete('/api/tasks/:id', async (req, res) => {
const task = await Task.findByIdAndDelete(req.params.id);
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json({ message: 'Task deleted' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Your REST API is built and connected to the cloud. Next up, you'll create the Angular frontend that sends requests to these endpoints and displays your tasks in the browser.
Create the Angular Frontend
Your Express API is ready to serve data. Now you need a frontend that talks to it.
In this step, you will scaffold a new Angular application and build a component that communicates with your API. Angular uses services to separate data-fetching logic from display logic, so you will create a TaskService that handles all HTTP calls and a root component that renders the UI.
In this step, get ready to:
- Scaffold a new Angular application with the CLI.
- Generate a TaskService that calls your Express API.
- Build the root component with a task list, a form, and toggle/delete buttons.
Scaffold the Angular app
The Angular CLI can generate a complete project structure in one command. You will create a new app called task-app in the same parent folder as your task-api directory.
- Navigate back to your Desktop (the parent of task-api) by running this command:
cd ~/Desktop
- Scaffold the Angular app by running this command:
ng new task-app --style=css --ssr=false
What does this command do?
The ng new command creates a full Angular workspace with source files, configuration, and dependencies. The --style=css flag sets plain CSS as the stylesheet format. The --ssr=false flag disables server-side rendering since this is a client-only app.
Angular uses the 2025 file naming convention by default, so the root component file is named app.ts (not app.component.ts).
Will I see prompts?
The CLI may ask optional questions (for example, about analytics). All prompts can safely be allowed to default. Press Enter to accept any default when prompted.
- Confirm the project was created by running:
ls task-app/src/app
- You should see app.ts, app.config.ts, and other files listed in the output.
Don't see the app files?
Make sure you ran cd ~/Desktop before running ng new. If you were still inside task-api/, the new project would have been nested incorrectly.
Also check that the Angular CLI is installed globally. Run ng version to confirm.
Help me troubleshoot my Angular CLI scaffold
Generate and build the TaskService
Angular services encapsulate reusable logic. Your TaskService will hold all the HTTP methods that talk to your Express API.
- Move into the Angular project by running:
cd task-app
- Generate a service skeleton by running:
ng generate service task
What does ng generate service do?
This command creates task.service.ts inside src/app/. The file contains a basic Injectable class. You will replace its contents with your own HTTP logic next.
- Open src/app/task.service.ts in your editor and replace everything inside with the following code:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
// Define the shape of a task document from the API
export interface Task {
_id: string;
title: string;
completed: boolean;
createdAt: string;
}
@Injectable({ providedIn: 'root' })
export class TaskService {
// Inject Angular's HttpClient for making HTTP requests
private http = inject(HttpClient);
// Base URL pointing to your Express API
private apiUrl = 'http://localhost:3000/api/tasks';
// Fetch all tasks from the API
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(this.apiUrl);
}
// Create a new task with the given title
createTask(title: string): Observable<Task> {
return this.http.post<Task>(this.apiUrl, { title });
}
// Update a task by ID with partial changes
updateTask(id: string, updates: Partial<Task>): Observable<Task> {
return this.http.put<Task>(`${this.apiUrl}/${id}`, updates);
}
// Delete a task by ID
deleteTask(id: string): Observable<{ message: string }> {
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`);
}
}
What does this code do?
- The Task interface defines the exact shape of task documents that your Express API returns (including the _id field from MongoDB).
- The inject(HttpClient) call uses Angular's modern dependency injection pattern instead of constructor injection.
- Each method returns an Observable that the component will subscribe to. Observables allow Angular to handle asynchronous HTTP responses cleanly.
- The apiUrl points to http://localhost:3000/api/tasks, which matches the Express routes you created in the previous step.
- Save task.service.ts.
Seeing red underlines on HttpClient?
Your editor may show an error under HttpClient because the HTTP provider is not configured yet. This is expected. You will wire up provideHttpClient() in the next step.
Help me with HttpClient errors in my service
✔️ Awesome, I've got everything!
Great. Make sure you have saved task.service.ts before moving on.
ⓧ I'd like to double check the full code
Here is the complete task.service.ts file:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Task {
_id: string;
title: string;
completed: boolean;
createdAt: string;
}
@Injectable({ providedIn: 'root' })
export class TaskService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:3000/api/tasks';
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(this.apiUrl);
}
createTask(title: string): Observable<Task> {
return this.http.post<Task>(this.apiUrl, { title });
}
updateTask(id: string, updates: Partial<Task>): Observable<Task> {
return this.http.put<Task>(`${this.apiUrl}/${id}`, updates);
}
deleteTask(id: string): Observable<{ message: string }> {
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`);
}
}
Update the root component
The root component is where your UI lives. You will replace Angular's default placeholder with a task list, a creation form, and buttons for toggling completion and deleting tasks.
- Open src/app/app.ts in your editor and replace everything inside with the following code:
import { Component, OnInit, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TaskService, Task } from './task.service';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<div class="container">
<h1>Task Manager</h1>
<form (submit)="addTask($event)">
<input
type="text"
[(ngModel)]="newTaskTitle"
name="title"
placeholder="Enter a new task..."
required
/>
<button type="submit">Add Task</button>
</form>
<ul class="task-list">
@for (task of tasks; track task._id) {
<li [class.completed]="task.completed">
<span (click)="toggleTask(task)">{{ task.title }}</span>
<button class="delete-btn" (click)="deleteTask(task._id)">✕</button>
</li>
} @empty {
<li class="empty">No tasks yet. Add one above!</li>
}
</ul>
</div>
`,
styles: [`
.container {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
button {
padding: 0.75rem 1.5rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: #4338ca;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.task-list li span {
cursor: pointer;
}
.task-list li.completed span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
background: #ef4444;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.delete-btn:hover {
background: #dc2626;
}
.empty {
color: #999;
text-align: center;
font-style: italic;
}
`]
})
export class AppComponent implements OnInit {
// Inject the TaskService for HTTP operations
private taskService = inject(TaskService);
// Array to hold the current list of tasks
tasks: Task[] = [];
// Two-way bound input field for new task titles
newTaskTitle = '';
ngOnInit() {
this.loadTasks();
}
// Fetch all tasks from the API
loadTasks() {
this.taskService.getTasks().subscribe(tasks => {
this.tasks = tasks;
});
}
// Create a task and add it to the top of the list
addTask(event: Event) {
event.preventDefault();
if (!this.newTaskTitle.trim()) return;
this.taskService.createTask(this.newTaskTitle).subscribe(task => {
this.tasks.unshift(task);
this.newTaskTitle = '';
});
}
// Toggle the completed status of a task
toggleTask(task: Task) {
this.taskService.updateTask(task._id, { completed: !task.completed }).subscribe(updated => {
const index = this.tasks.findIndex(t => t._id === updated._id);
if (index !== -1) this.tasks[index] = updated;
});
}
// Remove a task from the list
deleteTask(id: string) {
this.taskService.deleteTask(id).subscribe(() => {
this.tasks = this.tasks.filter(t => t._id !== id);
});
}
}
What does this component do?
- The template section defines the HTML. It uses Angular's @for block syntax to loop through tasks, with @empty providing a fallback message.
- The [(ngModel)] directive creates two-way data binding between the text input and the newTaskTitle property. Typing in the input updates the property, and clearing the property empties the input.
- The [class.completed] binding conditionally adds a CSS class that applies a strikethrough style to completed tasks.
- The styles section is scoped to this component only. No other components will be affected by these rules.
- Save app.ts.
- You won't see a working app in the browser yet. The next step configures provideHttpClient() and runs both servers together.
Editor showing errors in app.ts?
If you see a red underline on FormsModule or on the TaskService import, make sure you saved task.service.ts in the previous substep. The import path './task.service' must resolve to the file you just created.
Help me fix import errors in app.ts
✔️ Awesome, I've got everything!
Great. Double check that both task.service.ts and app.ts are saved before continuing.
ⓧ I'd like to double check the full code
Here is the complete app.ts file:
import { Component, OnInit, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TaskService, Task } from './task.service';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<div class="container">
<h1>Task Manager</h1>
<form (submit)="addTask($event)">
<input
type="text"
[(ngModel)]="newTaskTitle"
name="title"
placeholder="Enter a new task..."
required
/>
<button type="submit">Add Task</button>
</form>
<ul class="task-list">
@for (task of tasks; track task._id) {
<li [class.completed]="task.completed">
<span (click)="toggleTask(task)">{{ task.title }}</span>
<button class="delete-btn" (click)="deleteTask(task._id)">✕</button>
</li>
} @empty {
<li class="empty">No tasks yet. Add one above!</li>
}
</ul>
</div>
`,
styles: [`
.container {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
button {
padding: 0.75rem 1.5rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: #4338ca;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.task-list li span {
cursor: pointer;
}
.task-list li.completed span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
background: #ef4444;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.delete-btn:hover {
background: #dc2626;
}
.empty {
color: #999;
text-align: center;
font-style: italic;
}
`]
})
export class AppComponent implements OnInit {
private taskService = inject(TaskService);
tasks: Task[] = [];
newTaskTitle = '';
ngOnInit() {
this.loadTasks();
}
loadTasks() {
this.taskService.getTasks().subscribe(tasks => {
this.tasks = tasks;
});
}
addTask(event: Event) {
event.preventDefault();
if (!this.newTaskTitle.trim()) return;
this.taskService.createTask(this.newTaskTitle).subscribe(task => {
this.tasks.unshift(task);
this.newTaskTitle = '';
});
}
toggleTask(task: Task) {
this.taskService.updateTask(task._id, { completed: !task.completed }).subscribe(updated => {
const index = this.tasks.findIndex(t => t._id === updated._id);
if (index !== -1) this.tasks[index] = updated;
});
}
deleteTask(id: string) {
this.taskService.deleteTask(id).subscribe(() => {
this.tasks = this.tasks.filter(t => t._id !== id);
});
}
}
Your Angular frontend code is in place. Next up, you will configure the HTTP provider and run both the backend and frontend together to see everything working end to end.
Connect Frontend to Backend
You have all the pieces built. The Express API knows how to talk to MongoDB Atlas, and the Angular frontend knows how to call the API.
The missing link is Angular's HttpClient provider. Without it, the TaskService cannot make HTTP requests. In this step, you'll wire up that provider, start both servers, and watch data flow end-to-end for the first time.
In this step, get ready to:
- Configure Angular's HttpClient provider so the frontend can make API calls.
- Start both the Express API and the Angular dev server.
- Verify the full CRUD flow and data persistence across page refreshes.
Configure Angular's HttpClient provider
Angular's standalone architecture requires you to explicitly register services like HttpClient in the application config. Without this registration, Angular's dependency injection system cannot find HttpClient when your TaskService tries to use it.
- In your code editor, open task-app/src/app/app.config.ts.
- Replace the entire contents of the file with the following code:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
// Batches change detection for better performance
provideZoneChangeDetection({ eventCoalescing: true }),
// Makes HttpClient available to all services and components
provideHttpClient()
]
};
What does this code do?
This file configures Angular's application-wide providers. Two providers are registered here:
- provideZoneChangeDetection({ eventCoalescing: true }) optimizes Angular's change detection by batching multiple events together.
- provideHttpClient() registers the HttpClient service so your TaskService can inject it and make HTTP requests to the Express API.
- Save app.config.ts.
Seeing NullInjectorError: No provider for HttpClient?
This error means provideHttpClient() is missing from your providers array. Double check that your app.config.ts includes it and that the file is saved.
Also confirm that your main.ts imports and uses appConfig from this file. The Angular scaffold sets this up automatically, so you should not need to change it.
Help me fix NullInjectorError for HttpClient in my Angular standalone app.
Start both servers and verify the full flow
You need two servers running at the same time. The Express API serves data on port 3000. The Angular dev server serves your frontend on port 4200.
- Open a terminal window and navigate into your task-api folder on your Desktop by running this command:
cd ~/Desktop/task-api
- Start the Express API by running:
node server.js
- You should see two messages confirming the server started:
What should I see?
Your terminal should display:
Server running on http://localhost:3000
Connected to MongoDB Atlas
If you see a MongoDB connection error instead, double check that the connection string in server.js has your actual username, password, and cluster address from the previous step.
Seeing a MongoDB connection error?
Make sure you replaced the placeholder connection string in server.js with your actual MongoDB Atlas credentials. The string should match the format you copied from Atlas.
Also confirm your current IP address is in the Atlas Network Access list. If your IP changed since you set up the cluster, add your new IP in the Atlas dashboard.
Help me fix my MongoDB Atlas connection error in Express.
- Open a second terminal window (keep the first one running) and navigate into your task-app folder by running:
cd ~/Desktop/task-app
- Start the Angular dev server by running:
npm start
- Wait for the compilation to finish. Once it says the app is available, open your browser to http://localhost:4200.
- You should see the Task Manager heading with an empty task list that says "No tasks yet. Add one above!"
Now test the full CRUD flow:
- Type Buy groceries into the input field and click **Add Task**.
- Confirm the task appears in the list below the form.
- Click the task title text to toggle it as completed. The text should get a strikethrough style.
- Click the **✕** button next to the task to delete it. The task should disappear from the list.
Tasks not appearing after clicking Add Task?
Open your browser's DevTools console (press **F12** or **Cmd+Option+I** on macOS). Look for error messages there.
If you see a CORS error, make sure app.use(cors()) is in your server.js and the Express server is running in the other terminal.
If you see a connection refused error, confirm both terminal windows are still running their respective servers.
Help me debug why my Angular app can't connect to my Express API.
Verify data persistence across page refreshes
The whole point of connecting to MongoDB Atlas is that your tasks survive beyond a single browser session. Let's prove it works.
- Create two or three tasks using the form (for example: Read documentation, Write unit tests).
- Refresh your browser page by pressing **Cmd+R** (macOS) or **Ctrl+R** (Windows).
- Confirm your tasks still appear in the list after the refresh. They are being loaded from MongoDB Atlas each time the page loads.
Why do tasks persist after a refresh?
When you add a task, the Angular app sends a POST request to Express. Express uses Mongoose to save the task document in your MongoDB Atlas cluster.
When the page loads, Angular calls GET /api/tasks which queries MongoDB and returns all saved documents. The data lives in the cloud database, not in the browser's memory.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.config.ts and that both servers are still running.
ⓧ I'd like to double check the full code
Here is the complete app.config.ts file for comparison:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient()
]
};
You just connected all four layers of the MEAN stack into one running application. Your Angular frontend talks to Express, which talks to MongoDB through Mongoose. Next up, take on the Secret Mission and add priority levels with color-coded filtering.
Secret mission
Add Priority Levels with Color-Coded Labels
Your task manager treats every task the same. But in reality, some tasks are urgent and others can wait. Can you add a priority system with color-coded labels and filtering so users can focus on what matters most?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses the MongoDB Atlas M0 free tier and runs locally, so there are no ongoing costs.
Resources you used:
- MongoDB Atlas M0 cluster (cloud database).
- task-api/ project folder (local).
- task-app/ project folder (local).
- Angular CLI globally installed via npm.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment more with your MEAN stack app.
- Your MongoDB Atlas M0 cluster is free forever with 512 MB of storage. It costs nothing to leave running.
- Your local project folders stay on your Desktop until you manually remove them.
- The Express server and Angular dev server stop automatically when you close your terminal windows.
- Angular CLI remains globally installed and available for future Angular projects.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and cloud database stay intact for when you return.
- Switch to the terminal running your Express API and press Ctrl+C to stop the server.
- Switch to the terminal running your Angular dev server and press Ctrl+C to stop it.
- Your MongoDB Atlas M0 cluster remains active at no cost. Your task data stays stored in the cloud.
- To resume later, open two terminal windows, run node server.js in the task-api/ folder and npm start in the task-app/ folder.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your local code, cloud data, and globally installed tools.
- Stop both running servers by pressing Ctrl+C in each terminal window.
- Delete your local project folders by running the following commands.
macOS
cd ~/Desktop
rm -rf task-api
rm -rf task-app
Windows
cd ~\Desktop
rmdir /s /q task-api
rmdir /s /q task-app
- Uninstall Angular CLI globally by running:
npm uninstall -g @angular/cli
- Delete your MongoDB Atlas cluster by logging in to your Atlas account, navigating to the Database Deployments page, opening the options menu on your cluster, and selecting the option to terminate it.
- If you no longer need your MongoDB Atlas account, visit your account settings to find the option to delete it.
Nice Work!
Nice Work!
You connected all four letters of MEAN into one running application. You now have a full-stack Task Manager powered by MongoDB, Express, Angular, and Node.js working together end to end.
You learned how to:
- Built an Express.js REST API with full CRUD operations for creating, reading, updating, and deleting tasks.
- Connected to MongoDB Atlas via Mongoose with schema validation to persist your data in a cloud database.
- Created an Angular frontend with standalone components and HttpClient to consume your REST API.
- Added priority levels with color-coded labels and filtering to extend your task manager.
Ready to quiz yourself?