Express CRUD Operations
Introduction
CRUD operations form the backbone of most web applications. The acronym CRUD stands for Create, Read, Update, and Delete - the four basic functions that models should be able to perform on persistent storage. In this tutorial, we'll explore how to implement these operations in an Express.js application with database integration.
By the end of this guide, you'll understand how to:
- Create new records in a database
- Read existing records from a database
- Update records that already exist
- Delete records that are no longer needed
We'll use MongoDB as our database with Mongoose ODM (Object Document Mapper) for this tutorial, but the concepts apply to other databases as well.
Prerequisites
Before starting, make sure you have:
- Basic knowledge of JavaScript and Node.js
- Node.js and npm installed
- MongoDB installed locally or access to a MongoDB Atlas account
- Understanding of basic Express.js concepts
Setting Up Your Project
Let's start by creating a simple Express application with MongoDB integration.
# Create a new directory for your project
mkdir express-crud-app
cd express-crud-app
# Initialize a new Node.js project
npm init -y
# Install required dependencies
npm install express mongoose body-parser
Project Structure
We'll organize our project with the following structure:
express-crud-app/
├── models/
│ └── product.js # Product model definition
├── routes/
│ └── products.js # Product routes for CRUD operations
├── app.js # Main application file
└── package.json # Project dependencies
Database Connection
First, let's create our main application file (app.js
) with a MongoDB connection:
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
// Initialize Express app
const app = express();
// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/crud-demo', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected successfully'))
.catch(err => console.error('MongoDB connection error:', err));
// Import routes
const productRoutes = require('./routes/products');
// Use routes
app.use('/api/products', productRoutes);
// Basic route
app.get('/', (req, res) => {
res.send('Welcome to the CRUD API');
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Creating a Model
Now, let's define our product model in models/product.js
:
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
price: {
type: Number,
required: true,
min: 0
},
description: {
type: String,
trim: true
},
category: {
type: String,
required: true,
trim: true
},
inStock: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Product', ProductSchema);
Implementing CRUD Operations
Now, let's create our routes file (routes/products.js
) to implement the CRUD operations:
const express = require('express');
const router = express.Router();
const Product = require('../models/product');
// CREATE - Add a new product
router.post('/', async (req, res) => {
try {
const newProduct = new Product(req.body);
const savedProduct = await newProduct.save();
res.status(201).json({
success: true,
data: savedProduct
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
});
// READ - Get all products
router.get('/', async (req, res) => {
try {
const products = await Product.find();
res.status(200).json({
success: true,
count: products.length,
data: products
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
// READ - Get a single product by ID
router.get('/:id', async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
res.status(200).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
// UPDATE - Update a product
router.put('/:id', async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
res.status(200).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
// DELETE - Delete a product
router.delete('/:id', async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
res.status(200).json({
success: true,
message: 'Product deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
module.exports = router;
Testing CRUD Operations
Now let's test our CRUD operations using a tool like Postman, curl, or Thunder Client.
Create Operation (POST)
Request:
POST http://localhost:3000/api/products
Content-Type: application/json
{
"name": "Smartphone",
"price": 699.99,
"description": "Latest model with advanced features",
"category": "Electronics",
"inStock": true
}
Response (201 Created):
{
"success": true,
"data": {
"_id": "60f7a9b9e6d3f62b4c2c1234",
"name": "Smartphone",
"price": 699.99,
"description": "Latest model with advanced features",
"category": "Electronics",
"inStock": true,
"createdAt": "2023-06-15T12:34:56.789Z",
"__v": 0
}
}
Read Operations (GET)
Get All Products
Request:
GET http://localhost:3000/api/products
Response (200 OK):
{
"success": true,
"count": 1,
"data": [
{
"_id": "60f7a9b9e6d3f62b4c2c1234",
"name": "Smartphone",
"price": 699.99,
"description": "Latest model with advanced features",
"category": "Electronics",
"inStock": true,
"createdAt": "2023-06-15T12:34:56.789Z",
"__v": 0
}
]
}
Get Single Product
Request:
GET http://localhost:3000/api/products/60f7a9b9e6d3f62b4c2c1234
Response (200 OK):
{
"success": true,
"data": {
"_id": "60f7a9b9e6d3f62b4c2c1234",
"name": "Smartphone",
"price": 699.99,
"description": "Latest model with advanced features",
"category": "Electronics",
"inStock": true,
"createdAt": "2023-06-15T12:34:56.789Z",
"__v": 0
}
}
Update Operation (PUT)
Request:
PUT http://localhost:3000/api/products/60f7a9b9e6d3f62b4c2c1234
Content-Type: application/json
{
"price": 599.99,
"description": "Latest model with advanced features and special discount"
}
Response (200 OK):
{
"success": true,
"data": {
"_id": "60f7a9b9e6d3f62b4c2c1234",
"name": "Smartphone",
"price": 599.99,
"description": "Latest model with advanced features and special discount",
"category": "Electronics",
"inStock": true,
"createdAt": "2023-06-15T12:34:56.789Z",
"__v": 0
}
}
Delete Operation (DELETE)
Request:
DELETE http://localhost:3000/api/products/60f7a9b9e6d3f62b4c2c1234
Response (200 OK):
{
"success": true,
"message": "Product deleted successfully"
}
Real-World Application: E-commerce API
Let's extend our example to create a more practical product management API that could be used in an e-commerce application.
Advanced Features
- Pagination - When dealing with large datasets, pagination becomes essential:
// Get products with pagination
router.get('/', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const total = await Product.countDocuments();
const products = await Product.find().skip(skip).limit(limit);
res.status(200).json({
success: true,
count: products.length,
pagination: {
total,
page,
pages: Math.ceil(total / limit)
},
data: products
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
- Filtering and Searching:
// Get filtered products
router.get('/search', async (req, res) => {
try {
const { category, minPrice, maxPrice, inStock, name } = req.query;
const query = {};
// Build query based on parameters
if (category) query.category = category;
if (minPrice !== undefined || maxPrice !== undefined) {
query.price = {};
if (minPrice !== undefined) query.price.$gte = parseFloat(minPrice);
if (maxPrice !== undefined) query.price.$lte = parseFloat(maxPrice);
}
if (inStock !== undefined) query.inStock = inStock === 'true';
if (name) query.name = new RegExp(name, 'i');
const products = await Product.find(query);
res.status(200).json({
success: true,
count: products.length,
data: products
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
- Bulk Operations:
// Bulk create products
router.post('/bulk', async (req, res) => {
try {
if (!Array.isArray(req.body)) {
return res.status(400).json({
success: false,
message: 'Request body should be an array of products'
});
}
const products = await Product.insertMany(req.body);
res.status(201).json({
success: true,
count: products.length,
data: products
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
});
Best Practices for CRUD Operations
- Validation: Always validate user input before processing database operations.
// Example of validation middleware
const validateProduct = (req, res, next) => {
const { name, price, category } = req.body;
const errors = [];
if (!name || name.trim() === '') {
errors.push('Product name is required');
}
if (!price || isNaN(price) || price < 0) {
errors.push('Product price must be a positive number');
}
if (!category || category.trim() === '') {
errors.push('Product category is required');
}
if (errors.length > 0) {
return res.status(400).json({ success: false, errors });
}
next();
};
// Use the middleware
router.post('/', validateProduct, async (req, res) => {
// Create product logic
});
- Error Handling: Implement proper error handling for better debugging and user experience.
// Global error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: 'Server Error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
- Rate Limiting: Protect your API from abuse by implementing rate limiting.
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later'
});
app.use('/api/', apiLimiter);
Summary
In this tutorial, we've covered how to implement CRUD operations in an Express.js application with MongoDB integration:
- Create: Adding new records to the database using POST requests
- Read: Retrieving records from the database using GET requests
- Update: Modifying existing records using PUT requests
- Delete: Removing records from the database using DELETE requests
We also explored advanced features and best practices that will help you build robust APIs:
- Pagination to handle large datasets
- Filtering and searching for better data retrieval
- Bulk operations for efficiency
- Validation and error handling for reliability
- Rate limiting for security
By mastering CRUD operations, you have the foundational skills needed to build data-driven web applications with Express.js and MongoDB.
Additional Resources
- Express.js Documentation
- Mongoose Documentation
- MongoDB CRUD Operations
- RESTful API Design Best Practices
Exercises
- Extend the product API to include image uploads for product pictures.
- Implement user authentication and ensure only authenticated users can modify products.
- Create a review system where users can add reviews to products (hint: you'll need a new model and relationship).
- Build a front-end interface using React or another framework that interacts with your CRUD API.
- Implement soft delete functionality where products are marked as deleted but not actually removed from the database.
By completing these exercises, you'll gain more experience with Express.js database integration and CRUD operations in real-world scenarios.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)