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">
© 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:
- Master en JavaScript: Aprender JS, jQuery, Angular 8, NodeJS
- Angular 8 - Complete Essential Guide
- Learn Angular 8 by creating a simple full-stack web App
- Angular 5 Bootcamp FastTrack
- Angular 6 - Soft & Sweet
- Angular 6 with TypeScript
Thanks!