Node.js MongoDB CRUD Operations: Complete Guide with Best Practices
Learn to build RESTful APIs with Node.js and MongoDB. Complete guide covering CRUD operations, Mongoose ODM, proper project structure, and error handling.
StalkTechie
Author
Node.js MongoDB CRUD Operations: Complete Guide with Best Practices
Learn to build efficient RESTful APIs with Node.js and MongoDB. This comprehensive guide covers CRUD operations, Mongoose ODM, proper project structure, error handling, and industry best practices for building scalable applications.
Table of Contents
Project Setup and Structure
Building a well-structured Node.js application is crucial for maintainability and scalability. We'll use the MVC pattern with a service layer for separation of concerns.
Project Structure
node-crud-app/
├── src/
│ ├── config/
│ │ └── database.js
│ ├── models/
│ │ └── User.js
│ ├── controllers/
│ │ └── userController.js
│ ├── services/
│ │ └── userService.js
│ └── app.js
├── server.js
├── package.json
└── .env
Package.json Dependencies
{
"name": "node-mongodb-crud",
"version": "1.0.0",
"description": "CRUD API with Node.js and MongoDB",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.5.0",
"dotenv": "^16.3.1",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
Initialize the project and install dependencies:
mkdir node-crud-app
cd node-crud-app
npm init -y
npm install express mongoose dotenv cors
npm install -D nodemon
MongoDB Database Configuration
Set up MongoDB connection using Mongoose ODM with proper error handling and connection management.
Environment Configuration (.env)
MONGODB_URI=mongodb://localhost:27017/node_crud_db
PORT=3000
NODE_ENV=development
Database Configuration (src/config/database.js)
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error('Database connection error:', error.message);
process.exit(1);
}
};
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected');
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB connection error:', err);
});
module.exports = connectDB;
Mongoose Data Models
Define the User schema with validation, timestamps, and data sanitization.
User Model (src/models/User.js)
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: [2, 'Name must be at least 2 characters long'],
maxlength: [50, 'Name cannot exceed 50 characters']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email']
},
age: {
type: Number,
min: [18, 'Age must be at least 18'],
max: [120, 'Age cannot exceed 120']
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
userSchema.methods.toJSON = function() {
const user = this.toObject();
delete user.__v;
return user;
};
module.exports = mongoose.model('User', userSchema);
Business Logic Service Layer
The service layer contains business logic and database operations, separating concerns from controllers.
User Service (src/services/userService.js)
const User = require('../models/User');
class UserService {
async createUser(userData) {
try {
const user = new User(userData);
return await user.save();
} catch (error) {
if (error.code === 11000) {
throw new Error('Email already exists');
}
throw new Error(`User creation failed: ${error.message}`);
}
}
async getAllUsers(page = 1, limit = 10) {
try {
const skip = (page - 1) * limit;
const users = await User.find({ isActive: true })
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await User.countDocuments({ isActive: true });
return {
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
};
} catch (error) {
throw new Error(`Failed to fetch users: ${error.message}`);
}
}
async getUserById(id) {
try {
const user = await User.findOne({ _id: id, isActive: true });
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
throw new Error(`Failed to fetch user: ${error.message}`);
}
}
async updateUser(id, updateData) {
try {
const user = await User.findOneAndUpdate(
{ _id: id, isActive: true },
updateData,
{ new: true, runValidators: true }
);
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
if (error.code === 11000) {
throw new Error('Email already exists');
}
throw new Error(`User update failed: ${error.message}`);
}
}
async deleteUser(id) {
try {
const user = await User.findOneAndUpdate(
{ _id: id, isActive: true },
{ isActive: false },
{ new: true }
);
if (!user) {
throw new Error('User not found');
}
return { message: 'User deleted successfully' };
} catch (error) {
throw new Error(`User deletion failed: ${error.message}`);
}
}
}
module.exports = new UserService();
Controller Layer Implementation
Controllers handle HTTP requests, response formatting, and error handling.
User Controller (src/controllers/userController.js)
const userService = require('../services/userService');
const userController = {
async createUser(req, res) {
try {
const user = await userService.createUser(req.body);
res.status(201).json({
success: true,
message: 'User created successfully',
data: user
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
},
async getUsers(req, res) {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const result = await userService.getAllUsers(page, limit);
res.status(200).json({
success: true,
data: result.users,
pagination: result.pagination
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
},
async getUserById(req, res) {
try {
const user = await userService.getUserById(req.params.id);
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(404).json({
success: false,
message: error.message
});
}
},
async updateUser(req, res) {
try {
const user = await userService.updateUser(req.params.id, req.body);
res.status(200).json({
success: true,
message: 'User updated successfully',
data: user
});
} catch (error) {
const statusCode = error.message.includes('not found') ? 404 : 400;
res.status(statusCode).json({
success: false,
message: error.message
});
}
},
async deleteUser(req, res) {
try {
const result = await userService.deleteUser(req.params.id);
res.status(200).json({
success: true,
message: result.message
});
} catch (error) {
res.status(404).json({
success: false,
message: error.message
});
}
}
};
module.exports = userController;
Express Application Setup
Configure Express application with middleware, routes, and error handling.
Express App (src/app.js)
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const connectDB = require('./config/database');
const userRoutes = require('./routes/userRoutes');
const app = express();
connectDB();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/api/users', userRoutes);
app.get('/health', (req, res) => {
res.status(200).json({
success: true,
message: 'Server is running',
timestamp: new Date().toISOString()
});
});
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Route not found'
});
});
app.use((error, req, res, next) => {
console.error('Unhandled error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
});
module.exports = app;
Routes (src/routes/userRoutes.js)
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router.post('/', userController.createUser);
router.get('/', userController.getUsers);
router.get('/:id', userController.getUserById);
router.put('/:id', userController.updateUser);
router.delete('/:id', userController.deleteUser);
module.exports = router;
Server Entry Point (server.js)
const app = require('./src/app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});
Testing API Endpoints
Test your CRUD operations using tools like Postman, Thunder Client, or curl commands.
API Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/users | Create new user |
| GET | /api/users | Get all users (with pagination) |
| GET | /api/users/:id | Get user by ID |
| PUT | /api/users/:id | Update user |
| DELETE | /api/users/:id | Delete user (soft delete) |
Example API Requests
# Create User
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com","age":25}'
# Get All Users
curl -X GET "http://localhost:3000/api/users?page=1&limit=10"
# Get User by ID
curl -X GET http://localhost:3000/api/users/USER_ID_HERE
# Update User
curl -X PUT http://localhost:3000/api/users/USER_ID_HERE \
-H "Content-Type: application/json" \
-d '{"name":"John Smith","age":26}'
# Delete User
curl -X DELETE http://localhost:3000/api/users/USER_ID_HERE
Best Practices and Security Considerations
Security Measures
- Input validation and sanitization
- Environment variables for sensitive data
- Proper error handling without exposing sensitive information
- CORS configuration for cross-origin requests
- Rate limiting to prevent abuse
Performance Optimization
- Database indexing for frequently queried fields
- Pagination for large datasets
- Selective field projection in queries
- Connection pooling for database connections
Code Quality
- Separation of concerns (MVC + Service layer)
- Consistent error handling patterns
- Proper HTTP status codes
- Input validation at multiple levels
- Comprehensive logging
FAQs: Node.js MongoDB CRUD Operations
Why use Mongoose instead of native MongoDB driver?
Mongoose provides schema validation, middleware, type casting, and relationship management out of the box, making it easier to build robust applications with structured data models.
What is the purpose of the service layer?
The service layer separates business logic from controller logic, making code more testable, reusable, and maintainable. It also allows for better error handling and data transformation.
How to handle database transactions in MongoDB?
MongoDB supports multi-document transactions. Use session objects with startTransaction(), commitTransaction(), and abortTransaction() methods for atomic operations across multiple documents.
What are the best practices for error handling?
Use try-catch blocks, centralized error handling middleware, consistent error response formats, and appropriate HTTP status codes. Always handle both operational and programmer errors.
Conclusion
This comprehensive guide demonstrates how to build a robust CRUD API with Node.js and MongoDB using proper project structure and industry best practices. The layered architecture with controllers, services, and models ensures maintainability and scalability.
Key takeaways include proper error handling, input validation, security considerations, and performance optimization techniques. This foundation can be extended with authentication, caching, and more advanced features for production applications.