Creating a well-structured backend application is crucial for scalability, maintainability, and ease of testing. In this guide, we will walk through building a blog backend using Node.js, Express, and MongoDB with a focus on separation of concerns by moving business logic into dedicated classes. We will follow a three-layer architecture and avoid placing business logic inside the controllers.
Overview
Our application will include user registration and login functionalities, an admin section for managing users and blog posts, and various features like search, filtering, and pagination. We will also incorporate error handlers and validations to ensure the robustness of our application.
Modules and Features
Installation and Setup
- Installation of necessary apps, servers, and dependencies.
- Connection to GitHub via Visual Studio Code.
- Documentation and application schema/diagram creation.
User Management
- User registration and login.
- JWT-based authentication.
Admin Panel
- Viewing all users.
- Viewing all blog posts.
- Statistics and graphs.
Blog Management
- CRUD operations for blog posts.
- Search, filtering, and pagination.
Error Handling and Validation
- Centralized error handling.
- Input validation.
Advanced Features for Later Updates
Role-Based Access Control (RBAC)
- Implement roles and permissions for different user types.
Email Notifications
- Send email notifications for various events like registration, password reset, etc.
Caching
- Implement caching for frequently accessed data to improve performance.
Rate Limiting
- Prevent abuse by limiting the number of requests a user can make.
Testing
- Unit and integration testing using tools like Jest and Supertest.
Module 1: Initial Setup and Configuration
Step 1: Install Node.js and Express
First, ensure you have Node.js and npm installed. Then, create a new project directory and initialize it:
mkdir notes-app
cd notes-app
npm init -y
Install Express and other necessary dependencies:
npm install express body-parser mongoose bcryptjs jsonwebtoken
Step 2: Directory Structure
Create a structured directory layout:
blog-backend/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── utils/
│ ├── app.js
│ └── server.js
├── .env
├── .gitignore
├── package.json
└── README.md
Step 3: Database Configuration
Create a config
folder and a database.js
file for MongoDB configuration:
// src/config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB Connected');
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;
Step 4: Environment Variables
Create a .env
file in the root of your project to store environment variables:
MONGO_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret_key
PORT=5000
Step 5: Initialize Git and Connect to GitHub
Initialize a Git repository and connect it to GitHub:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin <your-github-repo-url>
git push -u origin main
Module 2: User Management
In this module, we will implement the user management features, including user registration, login, and JWT-based authentication. We will also set up the necessary models, controllers, and services.
Step 1: Create the User Model
- Create a
User
model insrc/models/userModel.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user',
},
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
const User = mongoose.model('User', userSchema);
module.exports = User;
Step 2: Create the User Service
- Create a
UserService
class insrc/services/userService.js
:
const User = require('../models/userModel');
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
class UserService {
async registerUser(username, email, password) {
const userExists = await User.findOne({ email });
if (userExists) {
throw new Error('User already exists');
}
const user = await User.create({
username,
email,
password,
});
return this.generateToken(user._id);
}
async loginUser(email, password) {
const user = await User.findOne({ email });
if (!user || !(await user.matchPassword(password))) {
throw new Error('Invalid email or password');
}
return this.generateToken(user._id);
}
generateToken(id) {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d',
});
}
}
module.exports = new UserService();
Step 3: Create the User Controller
- Create a
UserController
class insrc/controllers/userController.js
const userService = require('../services/userService');
class UserController {
async register(req, res) {
const { username, email, password } = req.body;
try {
const token = await userService.registerUser(username, email, password);
res.status(201).json({ token });
} catch (error) {
res.status(400).json({ message: error.message });
}
}
async login(req, res) {
const { email, password } = req.body;
try {
const token = await userService.loginUser(email, password);
res.status(200).json({ token });
} catch (error) {
res.status(401).json({ message: error.message });
}
}
}
module.exports = new UserController();
Step 4: Create User Routes
- Create user routes in
src/routes/userRoutes.js
const express = require('express');
const UserController = require('../controllers/userController');
const router = express.Router();
router.post('/register', (req, res) => UserController.register(req, res));
router.post('/login', (req, res) => UserController.login(req, res));
module.exports = router;
Step 5: Middleware for Authentication
- Create middleware for authentication in
src/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const User = require('../models/userModel');
const dotenv = require('dotenv');
dotenv.config();
const protect = async (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
try {
token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch (error) {
res.status(401).json({ message: 'Not authorized, token failed' });
}
}
if (!token) {
res.status(401).json({ message: 'Not authorized, no token' });
}
};
module.exports = { protect };
Step 6: Integrate Routes into App
- Update
app.js
to include user routes:
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => {
console.log('Connected to MongoDB');
}).catch(err => {
console.error('Failed to connect to MongoDB', err);
});
app.use(express.json());
// Import routes
const userRoutes = require('./routes/userRoutes');
const blogRoutes = require('./routes/blogRoutes'); // Placeholder for blog routes
// Use routes
app.use('/api/users', userRoutes);
app.use('/api/blogs', blogRoutes); // Placeholder for blog routes
module.exports = app;
Step 7: Testing the User Management Module
Start the server and use a tool like Postman to test the user registration and login endpoints:
Start the server:
npm run dev
Test the registration endpoint:
- URL:
http://localhost:3000/api/users/register
- Method:
POST
- Body
{
"username": "testuser",
"email": "testuser@example.com",
"password": "password123"
}
Test the login endpoint:
- URL:
http://localhost:3000/api/users/login
- Method:
POST
- Body
{
"email": "testuser@example.com",
"password": "password123"
}
With the user management module implemented, we have set up a foundation for user registration and authentication. Next, we will move on to implementing the admin panel and blog management features.
Module 3: Admin panel
In this module, we will implement the admin panel features, including viewing all users, viewing all blog posts, and displaying statistics and graphs. We will ensure that only users with the admin role can access these features.
Step 1: Create Admin Middleware
- Create middleware to check for admin role in
src/middleware/adminMiddleware.js
const User = require('../models/userModel');
const admin = async (req, res, next) => {
if (req.user && req.user.role === 'admin') {
next();
} else {
res.status(403).json({ message: 'Not authorized as an admin' });
}
};
module.exports = { admin };
Step 2: Admin Controller
- Create an
AdminController
class insrc/controllers/adminController.js
const User = require('../models/userModel');
const BlogPost = require('../models/blogPostModel'); // Placeholder for blog post model
class AdminController {
async getAllUsers(req, res) {
try {
const users = await User.find({});
res.status(200).json(users);
} catch (error) {
res.status(500).json({ message: error.message });
}
}
async getAllBlogPosts(req, res) {
try {
const blogPosts = await BlogPost.find({});
res.status(200).json(blogPosts);
} catch (error) {
res.status(500).json({ message: error.message });
}
}
async getStats(req, res) {
try {
const userCount = await User.countDocuments({});
const blogPostCount = await BlogPost.countDocuments({});
// Add more stats as needed
res.status(200).json({
userCount,
blogPostCount,
// Add more stats as needed
});
} catch (error) {
res.status(500).json({ message: error.message });
}
}
}
module.exports = new AdminController();
Step 3: Admin Routes
- Create admin routes in
src/routes/adminRoutes.js
const express = require('express');
const AdminController = require('../controllers/adminController');
const { protect } = require('../middleware/authMiddleware');
const { admin } = require('../middleware/adminMiddleware');
const router = express.Router();
router.get('/users', protect, admin, (req, res) => AdminController.getAllUsers(req, res));
router.get('/blogs', protect, admin, (req, res) => AdminController.getAllBlogPosts(req, res));
router.get('/stats', protect, admin, (req, res) => AdminController.getStats(req, res));
module.exports = router;
Step 4: Integrate Admin Routes into App
- Update
app.js
to include admin routes
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => {
console.log('Connected to MongoDB');
}).catch(err => {
console.error('Failed to connect to MongoDB', err);
});
app.use(express.json());
// Import routes
const userRoutes = require('./routes/userRoutes');
const blogRoutes = require('./routes/blogRoutes'); // Placeholder for blog routes
const adminRoutes = require('./routes/adminRoutes');
// Use routes
app.use('/api/users', userRoutes);
app.use('/api/blogs', blogRoutes); // Placeholder for blog routes
app.use('/api/admin', adminRoutes);
module.exports = app;
Step 5: Create Blog Post Model
- Create a
BlogPost
model insrc/models/blogPostModel.js
const mongoose = require('mongoose');
const blogPostSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
const BlogPost = mongoose.model('BlogPost', blogPostSchema);
module.exports = BlogPost;
Step 6: Test Admin Features
Start the server and use a tool like Postman to test the admin endpoints:
Start the server
npm run dev
Test viewing all users:
- URL:
http://localhost:3000/api/admin/users
- Method:
GET
- Headers:
- Authorization:
Bearer <admin_token>
- Authorization:
- URL:
Test viewing all blog posts:
- URL:
http://localhost:3000/api/admin/blogs
- Method:
GET
- Headers:
- Authorization:
Bearer <admin_token>
- Authorization:
- URL:
Test viewing statistics:
- URL:
http://localhost:3000/api/admin/stats
- Method:
GET
- Headers:
- Authorization:
Bearer <admin_token>
- Authorization:
- URL:
With the admin panel implemented, we now have features that allow an admin to view all users, view all blog posts, and get statistics about the application. Next, we will move on to implementing the blog management features, including CRUD operations for blog posts.
Module 4: Blog Management
In this module, we will implement the features for managing blog posts, including creating, reading, updating, and deleting blog posts. We will also add functionalities for search, filtering, and pagination.
Step 1: Blog Post Model
We already created the BlogPost
model in the previous module. Here it is again for reference:
const mongoose = require('mongoose');
const blogPostSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
const BlogPost = mongoose.model('BlogPost', blogPostSchema);
module.exports = BlogPost;
Step 2: Blog Service
- Create a
BlogService
class insrc/services/blogService.js
:
const BlogPost = require('../models/blogPostModel');
class BlogService {
async createBlogPost(title, content, author) {
const blogPost = await BlogPost.create({
title,
content,
author,
});
return blogPost;
}
async getBlogPostById(id) {
const blogPost = await BlogPost.findById(id).populate('author', 'username email');
return blogPost;
}
async updateBlogPost(id, title, content) {
const blogPost = await BlogPost.findByIdAndUpdate(
id,
{ title, content },
{ new: true }
);
return blogPost;
}
async deleteBlogPost(id) {
await BlogPost.findByIdAndDelete(id);
}
async getAllBlogPosts(page, limit, search) {
const query = search
? { title: { $regex: search, $options: 'i' } }
: {};
const blogPosts = await BlogPost.find(query)
.populate('author', 'username email')
.skip((page - 1) * limit)
.limit(limit);
return blogPosts;
}
async countBlogPosts(search) {
const query = search
? { title: { $regex: search, $options: 'i' } }
: {};
const count = await BlogPost.countDocuments(query);
return count;
}
}
module.exports = new BlogService();
Step 3: Blog Controller
- Create a
BlogController
class insrc/controllers/blogController.js
:
const blogService = require('../services/blogService');
class BlogController {
async create(req, res) {
const { title, content } = req.body;
const author = req.user._id;
try {
const blogPost = await blogService.createBlogPost(title, content, author);
res.status(201).json(blogPost);
} catch (error) {
res.status(400).json({ message: error.message });
}
}
async get(req, res) {
const { id } = req.params;
try {
const blogPost = await blogService.getBlogPostById(id);
if (!blogPost) {
return res.status(404).json({ message: 'Blog post not found' });
}
res.status(200).json(blogPost);
} catch (error) {
res.status(400).json({ message: error.message });
}
}
async update(req, res) {
const { id } = req.params;
const { title, content } = req.body;
try {
const blogPost = await blogService.updateBlogPost(id, title, content);
if (!blogPost) {
return res.status(404).json({ message: 'Blog post not found' });
}
res.status(200).json(blogPost);
} catch (error) {
res.status(400).json({ message: error.message });
}
}
async delete(req, res) {
const { id } = req.params;
try {
await blogService.deleteBlogPost(id);
res.status(204).end();
} catch (error) {
res.status(400).json({ message: error.message });
}
}
async list(req, res) {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const search = req.query.search || '';
try {
const blogPosts = await blogService.getAllBlogPosts(page, limit, search);
const total = await blogService.countBlogPosts(search);
res.status(200).json({
blogPosts,
total,
page,
pages: Math.ceil(total / limit),
});
} catch (error) {
res.status(400).json({ message: error.message });
}
}
}
module.exports = new BlogController();
Step 4: Blog Routes
- Create blog routes in
src/routes/blogRoutes.js
:
const express = require('express');
const BlogController = require('../controllers/blogController');
const { protect } = require('../middleware/authMiddleware');
const router = express.Router();
router.post('/', protect, (req, res) => BlogController.create(req, res));
router.get('/:id', (req, res) => BlogController.get(req, res));
router.put('/:id', protect, (req, res) => BlogController.update(req, res));
router.delete('/:id', protect, (req, res) => BlogController.delete(req, res));
router.get('/', (req, res) => BlogController.list(req, res));
module.exports = router;
Step 5: Integrate Blog Routes into App
- Update
app.js
to include blog routes:
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => {
console.log('Connected to MongoDB');
}).catch(err => {
console.error('Failed to connect to MongoDB', err);
});
app.use(express.json());
// Import routes
const userRoutes = require('./routes/userRoutes');
const blogRoutes = require('./routes/blogRoutes');
const adminRoutes = require('./routes/adminRoutes');
// Use routes
app.use('/api/users', userRoutes);
app.use('/api/blogs', blogRoutes);
app.use('/api/admin', adminRoutes);
module.exports = app;
Step 6: Test Blog Management Features
Start the server and use a tool like Postman to test the blog management endpoints:
Start the server:
npm run dev
Test creating a blog post:
- URL:
http://localhost:3000/api/blogs
- Method:
POST
- Headers:
- Authorization:
Bearer <user_token>
- Authorization:
- Body
{
"title": "My First Blog Post",
"content": "This is the content of my first blog post."
}
Test reading a blog post:
- URL:
http://localhost:3000/api/blogs/<blog_id>
- Method:
GET
- URL:
Test updating a blog post:
- URL:
http://localhost:3000/api/blogs/<blog_id>
- Method:
PUT
- Headers:
- Authorization:
Bearer <user_token>
- Authorization:
- Body
- URL:
{
"title": "Updated Blog Post Title",
"content": "Updated content of the blog post."
}
Test deleting a blog post:
- URL:
http://localhost:3000/api/blogs/<blog_id>
- Method:
DELETE
- Headers:
- Authorization:
Bearer <user_token>
- Authorization:
- URL:
Test listing blog posts with pagination and search:
- URL:
http://localhost:3000/api/blogs?page=1&limit=10&search=First
- Method:
GET
- URL:
With the blog management module implemented, we now have features that allow users to create, read, update, and delete blog posts, along with search, filtering, and pagination. Next, we will move on to implementing error handling and validation to ensure our application is robust and secure.
Module 5: Error Handling and Validation
In this module, we will implement centralized error handling and input validation to ensure our application is robust and secure. This will help us manage errors consistently and validate user inputs to prevent invalid data from entering our system.
Step 1: Error Handling Middleware
- Create an error handling middleware in
src/middleware/errorMiddleware.js
:
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
});
};
module.exports = { errorHandler };
Step 2: Validation Middleware
- Create validation middleware using
express-validator
insrc/middleware/validationMiddleware.js
:
const { validationResult } = require('express-validator');
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
module.exports = { validate };
Step 3: Apply Validation Rules
- Apply validation rules to the user and blog routes in
src/routes/userRoutes.js
andsrc/routes/blogRoutes.js
:
User Routes with Validation
const express = require('express');
const { check } = require('express-validator');
const UserController = require('../controllers/userController');
const { validate } = require('../middleware/validationMiddleware');
const router = express.Router();
router.post(
'/register',
[
check('username', 'Username is required').not().isEmpty(),
check('email', 'Please include a valid email').isEmail(),
check('password', 'Password must be 6 or more characters').isLength({ min: 6 }),
],
validate,
(req, res) => UserController.register(req, res)
);
router.post(
'/login',
[
check('email', 'Please include a valid email').isEmail(),
check('password', 'Password is required').exists(),
],
validate,
(req, res) => UserController.login(req, res)
);
module.exports = router;
Blog Routes with Validation
const express = require('express');
const { check } = require('express-validator');
const BlogController = require('../controllers/blogController');
const { protect } = require('../middleware/authMiddleware');
const { validate } = require('../middleware/validationMiddleware');
const router = express.Router();
router.post(
'/',
protect,
[
check('title', 'Title is required').not().isEmpty(),
check('content', 'Content is required').not().isEmpty(),
],
validate,
(req, res) => BlogController.create(req, res)
);
router.put(
'/:id',
protect,
[
check('title', 'Title is required').not().isEmpty(),
check('content', 'Content is required').not().isEmpty(),
],
validate,
(req, res) => BlogController.update(req, res)
);
router.delete('/:id', protect, (req, res) => BlogController.delete(req, res));
router.get('/:id', (req, res) => BlogController.get(req, res));
router.get('/', (req, res) => BlogController.list(req, res));
module.exports = router;
Step 4: Integrate Error Handling Middleware
- Update
app.js
to use the error handling middleware:
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const { errorHandler } = require('./middleware/errorMiddleware');
dotenv.config();
const app = express();
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => {
console.log('Connected to MongoDB');
}).catch(err => {
console.error('Failed to connect to MongoDB', err);
});
app.use(express.json());
// Import routes
const userRoutes = require('./routes/userRoutes');
const blogRoutes = require('./routes/blogRoutes');
const adminRoutes = require('./routes/adminRoutes');
// Use routes
app.use('/api/users', userRoutes);
app.use('/api/blogs', blogRoutes);
app.use('/api/admin', adminRoutes);
// Error handling middleware
app.use(errorHandler);
module.exports = app;
Step 5: Test Error Handling and Validation
Start the server and use a tool like Postman to test the validation and error handling:
Start the server:
npm run dev
Test validation on user registration:
- URL:
http://localhost:3000/api/users/register
- Method:
POST
- Body with missing or invalid fields to trigger validation errors
{
"username": "",
"email": "invalidemail",
"password": "short"
}
Test error handling by making an invalid request:
- URL:
http://localhost:3000/api/users/login
- Method:
POST
- Body with missing fields:
{
"email": "testuser@example.com"
}
Test validation on creating a blog post:
- URL:
http://localhost:3000/api/blogs
- Method:
POST
- Headers:
- Authorization:
Bearer <user_token>
- Authorization:
- Body with missing fields to trigger validation errors
{
"title": "",
"content": ""
}
With the error handling and validation module implemented, we now have a robust system for managing errors consistently and validating user inputs to ensure data integrity. This completes the core functionalities of our blog backend. You can further enhance the application by adding advanced features like role-based access control, email notifications, caching, rate limiting, and comprehensive testing.
Welcome to DevTechTutor.com, your ultimate resource for mastering web development and technology! Whether you're a beginner eager to dive into coding or an experienced developer looking to sharpen your skills, DevTechTutor.com is here to guide you every step of the way. Our mission is to make learning web development accessible, engaging, and effective.