All posts

Full Stack

How to Build a MEAN Stack App: From Tech Stack to Deployment

Nagaraju Nali5 min read

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

TechnologyWhere it runsIts job
AngularBrowserThe user interface: components, routing, forms, and services that call the API
ExpressServerThe API: receives requests, checks them and sends back JSON
Node.jsServerThe runtime that runs your Express code (JavaScript outside the browser)
MongoDBDatabaseStores your data as JSON-like documents
Angular in the browser sends an HTTP request through HttpClient to Express running on Node.js, which queries MongoDB. MongoDB returns documents, and Express sends them back to Angular as JSON.
Angular never talks to MongoDB directly. Express sits in the middle.

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:

MethodEndpointWhat it does
GET/api/tasksList the current user's tasks
POST/api/tasksCreate a task
PATCH/api/tasks/:idUpdate a task (e.g. mark it done)
DELETE/api/tasks/:idDelete 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 this

Step 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

  1. On sign-up, Express hashes the password with bcrypt and saves the user. On login, it issues a JWT in an httpOnly cookie.
  2. A requireAuth middleware in Express verifies the token on every protected route.
  3. 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 answers 401.
  4. Use reactive forms with validators for instant feedback - and still validate again on the server.
  5. Test services with HttpTestingController, and API endpoints with Jest + Supertest.

Step 8: Deploy

PartWhere to host itSettings to remember
MongoDBMongoDB AtlasAllow your server's IP in Network Access
Express + Node.jsRender or RailwayMONGODB_URI, JWT_SECRET, CLIENT_URL as environment variables
AngularNetlify, Vercel or Firebase HostingRun 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 .env is 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