Full Stack
How to Build a MEAN Stack App: From Tech Stack to Deployment
MEAN is four technologies: MongoDB, Express, Angular and Node.js. It's a popular choice for enterprise apps and admin dashboards, because Angular brings structure out of the box: TypeScript, dependency injection, routing, forms and an HTTP client.
This guide walks through building a MEAN app from the first plan to a live URL, using a simple task tracker as the example. At each step you'll see where one technology hands off to the next. (Coming from React? Compare it with the MERN version of this guide - only the frontend changes.)
What each part does
| Technology | Where it runs | Its job |
|---|---|---|
| Angular | Browser | The user interface: components, routing, forms, and services that call the API |
| Express | Server | The API: receives requests, checks them and sends back JSON |
| Node.js | Server | The runtime that runs your Express code (JavaScript outside the browser) |
| MongoDB | Database | Stores your data as JSON-like documents |
The browser never talks to the database. An Angular service calls your Express API over HTTP, Express talks to MongoDB, and data travels back as JSON. Angular adds one thing the other layers don't have: TypeScript interfaces that describe exactly what shape that JSON should be.
Step 1: Plan the data and the API
Decide what you store and which endpoints the UI needs before writing code. A task has a title, a done flag and an owner, and the UI needs to:
| Method | Endpoint | What it does |
|---|---|---|
| GET | /api/tasks | List the current user's tasks |
| POST | /api/tasks | Create a task |
| PATCH | /api/tasks/:id | Update a task (e.g. mark it done) |
| DELETE | /api/tasks/:id | Delete a task |
This list is the contract between frontend and backend. Agree on it first and both sides can be built at the same time.
Step 2: Set up the project
Keep the two apps in one repository. Create the frontend with the Angular CLI: ng new client.
task-tracker/
client/ # Angular app (ng new client)
src/app/tasks/ # task components + service
src/environments/ # API URL per environment
server/ # Node.js + Express API
models/ # Mongoose schemas
middleware/ # auth, error handling
.env # secrets - never commit thisStep 3: MongoDB - define the data
Create a free cluster on MongoDB Atlas and put its connection string in server/.env. Use Mongoose to give tasks a schema with validation:
// server/models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema(
{
title: { type: String, required: true, trim: true },
done: { type: Boolean, default: false },
owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
},
{ timestamps: true } // adds createdAt and updatedAt
);
module.exports = mongoose.model('Task', taskSchema);Step 4: Express on Node.js - build the API
Express receives requests from Angular, runs middleware, and uses the Mongoose model to read or write MongoDB.
// server/index.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const Task = require('./models/Task');
const requireAuth = require('./middleware/requireAuth');
const app = express();
// Allow the Angular app to call us, with its login cookie
app.use(cors({ origin: process.env.CLIENT_URL, credentials: true }));
app.use(express.json());
// Angular -> Express -> MongoDB -> Express -> Angular
app.get('/api/tasks', requireAuth, async (req, res) => {
const tasks = await Task.find({ owner: req.user.id }).sort({ createdAt: -1 });
res.json(tasks);
});
app.post('/api/tasks', requireAuth, async (req, res) => {
const task = await Task.create({ title: req.body.title, owner: req.user.id });
res.status(201).json(task);
});
mongoose.connect(process.env.MONGODB_URI).then(() => {
app.listen(process.env.PORT || 4000, () => console.log('API running'));
});Test every endpoint with Postman or Thunder Client before building the UI. If the API works on its own, later bugs are on the frontend side.
Step 5: Angular - a service for the API
In Angular, components don't call the API themselves. A service does, and components get it through dependency injection. First, describe the JSON with an interface, so TypeScript catches a typo like task.titel at build time:
// client/src/app/tasks/task.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
export interface Task {
_id: string; // created by MongoDB
title: string;
done: boolean;
createdAt: string;
}
@Injectable({ providedIn: 'root' })
export class TaskService {
private http = inject(HttpClient);
private api = `${environment.apiUrl}/api/tasks`;
list() {
return this.http.get<Task[]>(this.api);
}
create(title: string) {
return this.http.post<Task>(this.api, { title });
}
}Register the HTTP client once in app.config.ts, with an interceptor that sends the login cookie on every request:
// client/src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { HttpInterceptorFn, provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
const withCredentials: HttpInterceptorFn = (req, next) =>
next(req.clone({ withCredentials: true }));
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideHttpClient(withInterceptors([withCredentials]))],
};Step 6: Angular - show the data
The component injects the service and turns its response into a signal, so the template updates as soon as the data arrives:
// client/src/app/tasks/task-list.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { TaskService } from './task.service';
@Component({
selector: 'app-task-list',
template: `
<ul>
@for (task of tasks(); track task._id) {
<li>{{ task.title }}</li>
} @empty {
<li>No tasks yet.</li>
}
</ul>
`,
})
export class TaskListComponent {
private taskService = inject(TaskService);
tasks = toSignal(this.taskService.list(), { initialValue: [] });
}Follow task._id: MongoDB created it, Express passed it through untouched, the Task interface typed it, and now track uses it so Angular only re-renders the rows that changed.
Step 7: Authentication, errors and testing
- On sign-up, Express hashes the password with bcrypt and saves the user. On login, it issues a JWT in an
httpOnlycookie. - A
requireAuthmiddleware in Express verifies the token on every protected route. - In Angular, a route guard (
CanActivateFn) keeps logged-out users away from private pages, and an interceptor sends them to the login page when the API answers401. - Use reactive forms with validators for instant feedback - and still validate again on the server.
- Test services with
HttpTestingController, and API endpoints with Jest + Supertest.
Step 8: Deploy
| Part | Where to host it | Settings to remember |
|---|---|---|
| MongoDB | MongoDB Atlas | Allow your server's IP in Network Access |
| Express + Node.js | Render or Railway | MONGODB_URI, JWT_SECRET, CLIENT_URL as environment variables |
| Angular | Netlify, Vercel or Firebase Hosting | Run ng build, publish dist/client/browser, set the live API URL in environment.ts, add an SPA redirect to index.html |
First deployments usually fail on configuration, not code: a CORS origin still set to localhost:4200, the production environment.ts pointing at the wrong API, or a missing redirect that turns every refresh into a 404. Check those three first.
The finished-project checklist
- Secrets live in environment variables, and
.envis in.gitignore. - Every protected route is checked twice: by a guard in Angular and by middleware in Express.
- API responses are typed with interfaces, and the app shows loading, empty and error states.
- The README explains what the app does, the tech stack and how to run it locally.
- It's live, with a link you can put on your resume.
Build it once end to end and you'll be ready for the classic MEAN interview question: "What happens, step by step, when a user clicks Save?"
Want to go deeper on one layer? These free lessons cover each part: MongoDB documents and collections and CRUD operations, Express routing and middleware, how Node.js works including the event loop, and the TypeScript that Angular is written in: basic types and interfaces.
Keep going: Express.js course
Start now