Integration with Express.js
Integration with Express.js
The integration of Mongoose with Express.js is a common combination for building Node.js backend services. By using Mongoose to manage MongoDB data and Express to handle HTTP requests, you can quickly set up RESTful APIs or server-side rendered applications.
Basic Integration Approach
Integrating Mongoose in an Express project typically starts with connecting to the database. Here is the most basic integration example:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Define a model
const User = mongoose.model('User', new mongoose.Schema({
name: String,
email: String
}));
// Express route
app.get('/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
app.listen(3000);
Project Structure Organization
In actual projects, it is recommended to organize the code in a clearer structure:
project/
├── models/
│ └── User.js
├── routes/
│ └── userRoutes.js
└── app.js
Model definition example (models/User.js
):
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
Route definition example (routes/userRoutes.js
):
const express = require('express');
const router = express.Router();
const User = require('../models/User');
router.get('/', async (req, res) => {
try {
const users = await User.find().limit(10);
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
router.post('/', async (req, res) => {
const user = new User({
name: req.body.name,
email: req.body.email
});
try {
const newUser = await user.save();
res.status(201).json(newUser);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
module.exports = router;
Middleware Integration
Mongoose can be deeply integrated with Express middleware, such as implementing data validation:
// Middleware to validate ObjectId
async function validateUser(req, res, next) {
if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
return res.status(400).json({ message: 'Invalid ID format' });
}
try {
req.user = await User.findById(req.params.id);
if (!req.user) {
return res.status(404).json({ message: 'User not found' });
}
next();
} catch (err) {
res.status(500).json({ message: err.message });
}
}
// Usage in routes
router.get('/:id', validateUser, (req, res) => {
res.json(req.user);
});
Error Handling
A unified error handling mechanism can better manage exceptions in Mongoose operations:
// Error handling middleware
function errorHandler(err, req, res, next) {
if (err instanceof mongoose.Error.ValidationError) {
return res.status(400).json({
message: 'Validation Error',
details: err.errors
});
}
if (err.code === 11000) { // MongoDB duplicate key error
return res.status(409).json({
message: 'Duplicate key error',
field: Object.keys(err.keyPattern)[0]
});
}
console.error(err);
res.status(500).json({ message: 'Server error' });
}
// Register in app.js
app.use(errorHandler);
Advanced Query Integration
Express routes can fully utilize Mongoose's query capabilities:
// Complex query example
router.get('/search', async (req, res) => {
const { name, email, page = 1, limit = 10 } = req.query;
const query = {};
if (name) query.name = new RegExp(name, 'i');
if (email) query.email = email;
try {
const users = await User.find(query)
.skip((page - 1) * limit)
.limit(Number(limit))
.sort({ createdAt: -1 })
.select('name email createdAt');
const count = await User.countDocuments(query);
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total: count
}
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
Transaction Support
For scenarios requiring atomic execution of multiple operations, Mongoose transactions can be used:
router.post('/transfer', async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const { fromUserId, toUserId, amount } = req.body;
const fromUser = await User.findById(fromUserId).session(session);
if (fromUser.balance < amount) {
throw new Error('Insufficient balance');
}
const toUser = await User.findById(toUserId).session(session);
fromUser.balance -= amount;
toUser.balance += amount;
await fromUser.save();
await toUser.save();
await session.commitTransaction();
res.json({ message: 'Transfer successful' });
} catch (err) {
await session.abortTransaction();
res.status(400).json({ message: err.message });
} finally {
session.endSession();
}
});
Performance Optimization
Some performance optimization measures can be taken during integration:
- Index Optimization:
// Add indexes in model definition
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text' });
- Query Optimization:
// Select only required fields
router.get('/minimal', async (req, res) => {
const users = await User.find().select('name email -_id');
res.json(users);
});
// Use lean() to get plain JavaScript objects
router.get('/fast', async (req, res) => {
const users = await User.find().lean();
res.json(users);
});
- Bulk Operations:
router.post('/bulk', async (req, res) => {
try {
const result = await User.insertMany(req.body);
res.status(201).json(result);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
Real-time Application Integration
Combine with Socket.io for real-time data updates:
const http = require('http');
const socketio = require('socket.io');
const server = http.createServer(app);
const io = socketio(server);
// Listen for Mongoose changes
User.watch().on('change', (change) => {
io.emit('user_change', change);
});
// Handle client connections
io.on('connection', (socket) => {
socket.on('get_users', async () => {
const users = await User.find();
socket.emit('users_list', users);
});
});
server.listen(3000);
Testing Strategy
Integration test example (using Jest and SuperTest):
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
beforeAll(async () => {
await mongoose.connect('mongodb://localhost:27017/testdb', {
useNewUrlParser: true,
useUnifiedTopology: true
});
});
afterAll(async () => {
await mongoose.connection.close();
});
describe('User API', () => {
beforeEach(async () => {
await User.deleteMany();
});
test('POST /users - create new user', async () => {
const response = await request(app)
.post('/users')
.send({ name: 'Test', email: 'test@example.com' });
expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty('_id');
expect(response.body.name).toBe('Test');
});
test('GET /users - retrieve all users', async () => {
await User.create([
{ name: 'User1', email: 'user1@example.com' },
{ name: 'User2', email: 'user2@example.com' }
]);
const response = await request(app).get('/users');
expect(response.statusCode).toBe(200);
expect(response.body.length).toBe(2);
});
});
Security Considerations
Security issues to note during integration:
- Data Validation:
// Use express-validator for input validation
const { body, validationResult } = require('express-validator');
router.post(
'/',
[
body('name').trim().isLength({ min: 2 }),
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 })
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process request...
}
);
- Query Injection Protection:
// Unsafe approach
router.get('/unsafe', async (req, res) => {
const users = await User.find(req.query); // Directly use user input
res.json(users);
});
// Safe approach
router.get('/safe', async (req, res) => {
const { name, email } = req.query;
const query = {};
if (name) query.name = { $regex: new RegExp(`^${name}$`, 'i') };
if (email) query.email = email;
const users = await User.find(query);
res.json(users);
});
- Rate Limiting:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // Limit 100 requests per IP
});
app.use('/api/', limiter);
Deployment Considerations
Notes for production environment deployment:
- Connection Pool Configuration:
mongoose.connect(process.env.MONGODB_URI, {
poolSize: 10, // Connection pool size
bufferMaxEntries: 0, // Do not buffer operations on connection error
connectTimeoutMS: 10000, // Connection timeout
socketTimeoutMS: 45000 // Socket timeout
});
- Health Check Endpoint:
router.get('/health', (req, res) => {
const dbStatus = mongoose.connection.readyState;
const status = dbStatus === 1 ? 'healthy' : 'unhealthy';
res.json({
status,
db: {
state: ['disconnected', 'connected', 'connecting', 'disconnecting'][dbStatus],
dbName: mongoose.connection.name
},
uptime: process.uptime()
});
});
- Environment Configuration:
// Use dotenv to manage environment variables
require('dotenv').config();
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
db: 'mongodb://localhost:27017/devdb'
},
test: {
db: 'mongodb://localhost:27017/testdb'
},
production: {
db: process.env.MONGODB_URI
}
};
mongoose.connect(configs[env].db);
Debugging Techniques
Debugging methods during development:
- Enable Debug Logs:
// Enable Mongoose debugging
mongoose.set('debug', true);
// Or customize the debug function
mongoose.set('debug', (collectionName, method, query, doc) => {
console.log(`${collectionName}.${method}`, JSON.stringify(query), doc);
});
- Query Hooks:
// Log the duration of all queries
mongoose.plugin((schema) => {
schema.pre('find', function() {
this._startTime = Date.now();
});
schema.post('find', function(result) {
console.log(`Query ${this.op} took ${Date.now() - this._startTime}ms`);
});
});
- Performance Profiling:
// Use Mongoose's profiling feature
mongoose.set('profile', 2); // Log all operations
// Or for specific operations
const user = await User.findOne().explain('executionStats');
console.log(user);
Version Compatibility
Handling compatibility between different versions:
- Express 4.x vs 5.x:
// Express 5.x handles async errors differently
// In Express 4.x, errors need to be explicitly caught
router.get('/async', async (req, res, next) => {
try {
const data = await User.find();
res.json(data);
} catch (err) {
next(err);
}
});
// Express 5.x catches errors automatically
router.get('/async', async (req, res) => {
const data = await User.find();
res.json(data);
});
- Changes in Mongoose 6.x:
// No longer need useNewUrlParser and useUnifiedTopology options
mongoose.connect('mongodb://localhost:27017/myapp');
// Default strict query
// Older versions required setting strictQuery: false to allow non-schema fields
mongoose.set('strictQuery', false);
Custom Middleware
Creating reusable Mongoose-related middleware:
- Pagination Middleware:
function paginate(model) {
return async (req, res, next) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const startIndex = (page - 1) * limit;
const results = {};
try {
results.total = await model.countDocuments();
results.data = await model.find()
.limit(limit)
.skip(startIndex);
res.paginatedResults = results;
next();
} catch (err) {
res.status(500).json({ message: err.message });
}
};
}
// Usage example
router.get('/', paginate(User), (req, res) => {
res.json(res.paginatedResults);
});
- Caching Middleware:
const cache = require('memory-cache');
function cacheMiddleware(duration) {
return (req, res, next) => {
const key = '__express__' + req.originalUrl;
const cachedBody = cache.get(key);
if (cachedBody) {
return res.json(cachedBody);
} else {
res.sendResponse = res.json;
res.json = (body) => {
cache.put(key, body, duration * 1000);
res.sendResponse(body);
};
next();
}
};
}
// Usage example
router.get('/popular', cacheMiddleware(60), async (req, res) => {
const users = await User.find().sort({ views: -1 }).limit(5);
res.json(users);
});
Advanced Schema Design
Schema design examples for complex scenarios:
- Polymorphic Associations:
// Comments can be associated with posts or products
const commentSchema = new mongoose.Schema({
content: String,
targetType: {
type: String,
enum: ['Post', 'Product'],
required: true
},
targetId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
refPath: 'targetType'
}
});
// Dynamic population during queries
Comment.find().populate('targetId');
- Tree Structures:
// Implement category trees using materialized path pattern
const categorySchema = new mongoose.Schema({
name: String,
path: String
});
categorySchema.pre('save', function(next) {
if (this.isNew) {
if (this.parent) {
this.path = `${this.parent.path}/${this._id}`;
} else {
this.path = this._id;
}
}
next();
});
// Query subtrees
Category.find({ path: new RegExp(`^${parent.path}/`) });
- Version Control:
// Document version history
const docSchema = new mongoose.Schema({
title: String,
content: String,
version: {
number: Number,
timestamp: Date
},
previousVersions: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'DocHistory'
}]
});
const docHistorySchema = new mongoose.Schema({
_originalId: mongoose.Schema.Types.ObjectId,
title: String,
content: String,
version: {
number: Number,
timestamp: Date
}
});
// Save history hook
docSchema.pre('save', function(next) {
if (this.isModified()) {
const history = new DocHistory({
_originalId: this._id,
title: this.title,
content: this.content,
version: this.version
});
history.save();
this.previousVersions.push(history._id);
this.version.number += 1;
this.version.timestamp = new Date();
}
next();
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:RESTful API开发示例
下一篇:用户认证与权限管理