Build a Blog CMS with MEAN Stack (MongoDB, Express, Angular 20, Node.js)

by Didin J. on Jun 02, 2025 Build a Blog CMS with MEAN Stack (MongoDB, Express, Angular 20, Node.js)

Learn how to build a modern MEAN Stack blog CMS using Angular 20, Node.js, Express, and MongoDB with Tailwind CSS styling. Full CRUD with clean UI

The MEAN Stack remains a powerful and popular full-stack JavaScript solution for building modern web applications. In this updated tutorial, you'll learn how to build a simple yet functional Blog Content Management System (CMS) using the latest MEAN stack, including:

  • MongoDB – NoSQL database for storing blog posts

  • Express.js – Web framework for Node.js to handle API routes

  • Angular 20 – The latest version of Google's frontend framework

  • Node.js (LTS) – Backend runtime with modern ES module support

This crash course covers the full stack from backend API creation to frontend UI — fully updated to use Angular 20 and modern tooling.

What You'll Build

A simple blog CMS that allows you to:

  • Create, read, update, and delete blog posts

  • View a list of posts and a single post detail

  • Manage posts via a clean Angular UI connected to an Express API


Project Setup

Let’s break the setup into two parts: the backend (Node + Express + MongoDB) and the frontend (Angular 20).

Backend: Node.js + Express API

1. Create Backend Folder

mkdir blog-cms-backend
cd blog-cms-backend
npm init -y

2. Install Dependencies

npm install express mongoose cors dotenv
npm install --save-dev nodemon
  • express: Web framework

  • mongoose: MongoDB object modeling

  • cors: Enable cross-origin requests

  • dotenv: Manage environment variables

  • nodemon: Dev tool for auto-restarting the server

3. Folder Structure

blog-cms-backend/
├── models/
│   └── Post.js
├── routes/
│   └── posts.js
├── .env
├── server.js
└── package.json

4. Create server.js

// server.js
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
app.use(cors());
app.use(express.json());

// Routes
import postRoutes from './routes/posts.js';
app.use('/api/posts', postRoutes);

// MongoDB Connection
mongoose.connect(process.env.MONGO_URI)
  .then(() => app.listen(process.env.PORT || 5000, () =>
    console.log('Server running...')))
  .catch(err => console.error(err));

5. Create .env File

PORT=5000
MONGO_URI=mongodb://localhost:27017/blogcms


Frontend: Angular 20

1. Create an Angular App

ng new blog-cms-frontend --standalone --routing --style=css
cd blog-cms-frontend

Use --standalone to take advantage of Angular 20’s new structure.

2. Install Angular HTTP Client (already included

No need to install @angular/common/http separately. Just import HttpClientModule in your app.config.ts.

Next, we can:

  • ✅ Build the MongoDB model and API endpoints

  • ✅ Set up Angular services, components, and views

  • ✅ Add forms and basic styling

  • ✅ Add new features like authentication or Markdown support later


MongoDB Schema and Express API Routes

We'll create a simple Post model with the following fields:

  • title: The blog post title

  • content: The blog content body

  • author: Name of the author (optional)

  • createdAt: Timestamp

1. Create the Mongoose Schema

Create the file: models/Post.js

// models/Post.js
import mongoose from 'mongoose';

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    trim: true,
  },
  content: {
    type: String,
    required: true,
  },
  author: {
    type: String,
    default: 'Admin',
  },
  createdAt: {
    type: Date,
    default: Date.now,
  }
});

export default mongoose.model('Post', postSchema);

2. Create the Express Routes

Create the file: routes/posts.js

// routes/posts.js
import express from 'express';
import Post from '../models/Post.js';

const router = express.Router();

// Create a new post
router.post('/', async (req, res) => {
  try {
    const post = await Post.create(req.body);
    res.status(201).json(post);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

// Get all posts
router.get('/', async (req, res) => {
  try {
    const posts = await Post.find().sort({ createdAt: -1 });
    res.json(posts);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

// Get a single post by ID
router.get('/:id', async (req, res) => {
  try {
    const post = await Post.findById(req.params.id);
    if (!post) return res.status(404).json({ message: 'Post not found' });
    res.json(post);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

// Update a post by ID
router.put('/:id', async (req, res) => {
  try {
    const updated = await Post.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
      runValidators: true,
    });
    if (!updated) return res.status(404).json({ message: 'Post not found' });
    res.json(updated);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

// Delete a post by ID
router.delete('/:id', async (req, res) => {
  try {
    const deleted = await Post.findByIdAndDelete(req.params.id);
    if (!deleted) return res.status(404).json({ message: 'Post not found' });
    res.json({ message: 'Post deleted' });
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

export default router;

Test the API

Run the backend server:

nodemon server.js

Don't forget to add this to package.json:

"type": "module"

Test your API endpoints (e.g., using Postman or curl):

  • GET http://localhost:5000/api/posts – List all posts

  • POST http://localhost:5000/api/posts – Create a post

  • GET/PUT/DELETE http://localhost:5000/api/posts/:id – CRUD by ID


Angular 20 frontend service and components

Step 1: Angular HTTP Service

Generate the service:

ng generate service services/post

Edit src/app/services/post.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Post {
  _id?: string;
  title: string;
  content: string;
  author?: string;
  createdAt?: string;
}

@Injectable({
  providedIn: 'root'
})
export class PostService {
  private apiUrl = 'http://localhost:5000/api/posts';

  constructor(private http: HttpClient) {}

  getPosts(): Observable<Post[]> {
    return this.http.get<Post[]>(this.apiUrl);
  }

  getPost(id: string): Observable<Post> {
    return this.http.get<Post>(`${this.apiUrl}/${id}`);
  }

  createPost(post: Post): Observable<Post> {
    return this.http.post<Post>(this.apiUrl, post);
  }

  updatePost(id: string, post: Post): Observable<Post> {
    return this.http.put<Post>(`${this.apiUrl}/${id}`, post);
  }

  deletePost(id: string): Observable<any> {
    return this.http.delete(`${this.apiUrl}/${id}`);
  }
}

Step 2: Create Angular Components

Generate components:

ng generate component pages/home
ng generate component pages/view-post
ng generate component pages/create-post
ng generate component pages/edit-post

Set up Angular Routes

In src/app/app.routes.ts:

import { Routes } from '@angular/router';
import { Home } from './pages/home/home';
import { ViewPost } from './pages/view-post/view-post';
import { CreatePost } from './pages/create-post/create-post';
import { EditPost } from './pages/edit-post/edit-post';

export const routes: Routes = [
    { path: '', component: Home },
    { path: 'post/:id', component: ViewPost },
    { path: 'create', component: CreatePost },
    { path: 'edit/:id', component: EditPost },
];

Step 3: Home Page - List Posts

In home.ts:

import { Component, OnInit } from '@angular/core';
import { PostService, Post } from '../../services/post';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-home',
  templateUrl: './home.html',
  imports: [RouterModule, CommonModule]
})
export class Home implements OnInit {
  posts: Post[] = [];

  constructor(private postService: PostService) { }

  ngOnInit() {
    this.postService.getPosts().subscribe(data => this.posts = data);
  }
}

In home.html:

<h2>All Posts</h2>
<a routerLink="/create">Create New Post</a>
<ul>
  <li *ngFor="let post of posts">
    <h3><a [routerLink]="['/post', post._id]">{{ post.title }}</a></h3>
    <p>{{ post.content | slice:0:100 }}...</p>
    <small>By {{ post.author }} on {{ post.createdAt | date }}</small>
  </li>
</ul>

Step 4: View Post Page

In view-post.ts:

import { Component, OnInit } from '@angular/core';
import { Post, PostService } from '../../services/post';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-view-post',
  imports: [RouterModule, CommonModule],
  templateUrl: './view-post.html',
  styleUrl: './view-post.css'
})
export class ViewPost implements OnInit {
  post: Post | undefined;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private postService: PostService
  ) { }

  ngOnInit() {
    const id = this.route.snapshot.paramMap.get('id');
    if (id) {
      this.postService.getPost(id).subscribe(data => this.post = data);
    }
  }

  deletePost() {
    if (this.post?._id && confirm('Delete this post?')) {
      this.postService.deletePost(this.post._id).subscribe(() => {
        this.router.navigate(['/']);
      });
    }
  }
}

In view-post.html:

<h2>{{ post?.title }}</h2>
<p>{{ post?.content }}</p>
<small>By {{ post?.author }} on {{ post?.createdAt | date }}</small>
<br />
<a [routerLink]="['/edit', post?._id]">Edit</a>
<button (click)="deletePost()">Delete</button>

Step 5: Create and Edit Post Pages

Use a shared template for both components (optional).

create-post..ts:

import { Component } from '@angular/core';
import { Post, PostService } from '../../services/post';
import { Router, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-create-post',
  imports: [RouterModule, CommonModule, FormsModule],
  templateUrl: './create-post.html',
  styleUrl: './create-post.css'
})
export class CreatePost {
  post: Post = { title: '', content: '' };

  constructor(private postService: PostService, private router: Router) { }

  submit() {
    this.postService.createPost(this.post).subscribe(() => {
      this.router.navigate(['/']);
    });
  }
}

edit-post.ts:

import { Component, OnInit } from '@angular/core';
import { Post, PostService } from '../../services/post';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-edit-post',
  imports: [RouterModule, CommonModule, FormsModule],
  templateUrl: './edit-post.html',
  styleUrl: './edit-post.css'
})
export class EditPost implements OnInit {
  post: Post = { title: '', content: '' };

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private postService: PostService
  ) { }

  ngOnInit() {
    const id = this.route.snapshot.paramMap.get('id');
    if (id) {
      this.postService.getPost(id).subscribe(data => this.post = data);
    }
  }

  submit() {
    if (this.post._id) {
      this.postService.updatePost(this.post._id, this.post).subscribe(() => {
        this.router.navigate(['/']);
      });
    }
  }
}

Shared form template (for both):

<form (ngSubmit)="submit()">
  <input type="text" [(ngModel)]="post.title" name="title" placeholder="Title" required />
  <textarea [(ngModel)]="post.content" name="content" placeholder="Content" required></textarea>
  <button type="submit">Save</button>
</form>

✅ With this setup, your Angular 20 frontend can now:

  • List posts

  • View full post

  • Create new posts

  • Edit existing posts

  • Delete posts


Adding basic styling with Tailwind

1. Layout & Navigation

In app.html:

<header class="bg-blue-600 text-white px-6 py-4">
  <h1 class="text-2xl font-bold">Simple Blog CMS</h1>
</header>

<nav class="bg-blue-500 text-white px-6 py-2 flex space-x-4">
  <a routerLink="/" class="hover:underline">Home</a>
  <a routerLink="/create" class="hover:underline">Create Post</a>
</nav>

<main class="p-6 max-w-3xl mx-auto">
  <router-outlet></router-outlet>
</main>

<footer class="text-center text-sm text-gray-500 py-6">
  &copy; 2025 Simple Blog CMS. All rights reserved.
</footer>

2. Create Post Page

In create-post.html:

<h2 class="text-xl font-semibold mb-4">Create New Post</h2>

<form (ngSubmit)="submit()" class="space-y-4">
  <div>
    <label class="block mb-1 font-medium">Title</label>
    <input [(ngModel)]="post.title" name="title" required
      class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
  </div>

  <div>
    <label class="block mb-1 font-medium">Content</label>
    <textarea [(ngModel)]="post.content" name="content" rows="6" required
      class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
  </div>

  <button type="submit"
    class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">Submit</button>
</form>

3. Home Page (Post List)

In home.html:

<h2 class="text-xl font-semibold mb-4">All Posts</h2>

<div *ngFor="let post of posts" class="mb-6 p-4 border rounded shadow-sm">
  <h3 class="text-lg font-bold text-blue-600">{{ post.title }}</h3>
  <p class="text-gray-700 mt-2">{{ post.content | slice:0:150 }}...</p>
  <div class="mt-4 space-x-3">
    <a [routerLink]="['/post', post._id]" class="text-sm text-blue-500 hover:underline">Read More</a>
    <a [routerLink]="['/edit', post._id]" class="text-sm text-green-500 hover:underline">Edit</a>
  </div>
</div>

Add Tailwind Utilities

You can now freely use Tailwind’s utility classes across your templates.

Let me know if you'd like to:

  • Add a responsive mobile menu

  • Create a custom PostCard component

  • Style edit/view pages similarly

view-post.html

Displays a single blog post (read-only).

<div *ngIf="post" class="p-6 border rounded shadow-sm">
  <h2 class="text-2xl font-bold text-blue-700 mb-2">{{ post.title }}</h2>
  <p class="text-gray-800 whitespace-pre-line">{{ post.content }}</p>

  <div class="mt-4">
    <a [routerLink]="['/edit', post._id]" class="text-sm text-green-600 hover:underline">
      Edit this post
    </a>
  </div>
</div>

<div *ngIf="!post" class="text-center text-gray-500">
  Loading post...
</div>

edit-post.html

<h2 class="text-xl font-semibold mb-4">Edit Post</h2>

<form *ngIf="post" (ngSubmit)="submit()" class="space-y-4">
  <div>
    <label class="block mb-1 font-medium">Title</label>
    <input [(ngModel)]="post.title" name="title" required
      class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
  </div>

  <div>
    <label class="block mb-1 font-medium">Content</label>
    <textarea [(ngModel)]="post.content" name="content" rows="6" required
      class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
  </div>

  <button type="submit"
    class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition">Update</button>
</form>

<div *ngIf="!post" class="text-center text-gray-500">
  Loading post for editing...
</div>

✅ What's Next?

You now have:

  • 📝 Post creation form

  • 🏠 Post listing page

  • 👁 View post page

  • ✏️ Edit post form


Delete Post feature with Tailwind styling and confirmation

 

Step 1: Add Delete Method to Service

In your post.ts:

deletePost(id: string): Observable<any> {
  return this.http.delete(`${this.apiUrl}/${id}`);
}

Step 2: Add a Delete Button in view-post.html

Below the “Edit this post” link:

<div class="mt-4 flex space-x-4">
  <a [routerLink]="['/edit', post._id]" class="text-sm text-green-600 hover:underline">
    Edit this post
  </a>

  <button (click)="confirmDelete()" class="text-sm text-red-600 hover:underline">
    Delete this post
  </button>
</div>

Step 3: Add confirmDelete() in view-post.ts

confirmDelete() {
  if (confirm('Are you sure you want to delete this post?')) {
    this.postService.deletePost(this.post._id).subscribe(() => {
      this.router.navigate(['/']);
    });
  }
}

Optional: Add a success message or toast

You can later enhance it with Angular animations, a toast service, or a modal confirmation using Tailwind UI.


Conclusion

In this tutorial, you’ve learned how to build a full-featured Blog CMS using the MEAN Stack—MongoDB, Express, Angular 20, and Node.js—with a modern development approach using standalone Angular components and Tailwind CSS for styling.

We covered:

  • Setting up the backend with Node.js, Express, and MongoDB

  • Creating RESTful API routes and schemas

  • Building a clean Angular frontend with standalone components

  • Integrating Tailwind CSS for a responsive and attractive UI

  • Implementing full CRUD functionality: Create, Read, Update, Delete

This modern MEAN Stack setup is scalable, modular, and beginner-friendly, perfect for building more advanced web apps. You can take this further by:

  • Adding authentication and role-based access

  • Supporting image uploads for posts

  • Integrating rich text editing for content

  • Deploying to platforms like Vercel, Netlify, or Render

You can find the full working source code on our GitHub.

If you don’t want to waste your time designing your front-end or your budget to spend by hiring a web designer, then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That's just the basics. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:

Thanks!