Gensics

Back to Home

Express.js กับ Database: MongoDB และ Mongoose

Express Developer18 ธันวาคม 2567Database
ExpressMongoDBMongooseDatabaseCRUD

การเชื่อมต่ปรอ Express.js กับ MongoDB ผ่าน Mongoose ODM ช่วยให้การจัดการข้อมูลเป็นเรื่องง่ายและมีประสิทธิภาพ

การติดตั้งและตั้งค่า

npm install mongoose npm install dotenv

การเชื่อมต่อ MongoDB

// 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 failed:', error.message) process.exit(1) } } module.exports = connectDB

Mongoose Models

// models/User.js const mongoose = require('mongoose') const bcrypt = require('bcryptjs') const userSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Name is required'], trim: true, maxlength: [50, 'Name cannot be more than 50 characters'] }, email: { type: String, required: [true, 'Email is required'], unique: true, lowercase: true, match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email'] }, password: { type: String, required: [true, 'Password is required'], minlength: [6, 'Password must be at least 6 characters'], select: false // ไม่ส่งกลับมาใน queries โดยปกติ }, role: { type: String, enum: ['user', 'admin'], default: 'user' }, avatar: { type: String, default: 'default-avatar.jpg' }, isActive: { type: Boolean, default: true }, lastLogin: { type: Date } }, { timestamps: true // เพิ่ม createdAt และ updatedAt อัตโนมัติ }) // Pre-save middleware สำหรับ hash password userSchema.pre('save', async function(next) { // หาก password ไม่ได้ถูกแก้ไข ให้ข้ามไป if (!this.isModified('password')) { return next() } try { const salt = await bcrypt.genSalt(10) this.password = await bcrypt.hash(this.password, salt) next() } catch (error) { next(error) } }) // Instance method สำหรับตรวจสอบ password userSchema.methods.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password) } // Static method สำหรับหา active users userSchema.statics.findActiveUsers = function() { return this.find({ isActive: true }) } module.exports = mongoose.model('User', userSchema)
// models/Post.js const mongoose = require('mongoose') const postSchema = new mongoose.Schema({ title: { type: String, required: [true, 'Title is required'], trim: true, maxlength: [100, 'Title cannot exceed 100 characters'] }, content: { type: String, required: [true, 'Content is required'] }, excerpt: { type: String, maxlength: [200, 'Excerpt cannot exceed 200 characters'] }, slug: { type: String, unique: true, lowercase: true }, author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, tags: [{ type: String, trim: true }], category: { type: String, required: [true, 'Category is required'] }, status: { type: String, enum: ['draft', 'published', 'archived'], default: 'draft' }, featured: { type: Boolean, default: false }, viewCount: { type: Number, default: 0 }, publishedAt: { type: Date } }, { timestamps: true }) // Index สำหรับการค้นหา postSchema.index({ title: 'text', content: 'text' }) postSchema.index({ slug: 1 }) postSchema.index({ author: 1, status: 1 }) // Pre-save middleware สำหรับสร้าง slug postSchema.pre('save', function(next) { if (this.isModified('title') && !this.slug) { this.slug = this.title .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .trim('-') } // ตั้งค่า publishedAt เมื่อ status เป็น published if (this.isModified('status') && this.status === 'published' && !this.publishedAt) { this.publishedAt = new Date() } next() }) // Virtual สำหรับ URL postSchema.virtual('url').get(function() { return `/posts/${this.slug}` }) module.exports = mongoose.model('Post', postSchema)

CRUD Controllers

// controllers/userController.js const User = require('../models/User') const jwt = require('jsonwebtoken') // Helper function สำหรับสร้าง JWT const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '30d' }) } // @desc Register user // @route POST /api/users/register // @access Public const registerUser = async (req, res) => { try { const { name, email, password } = req.body // ตรวจสอบว่า user มีอยู่แล้วหรือไม่ const existingUser = await User.findOne({ email }) if (existingUser) { return res.status(400).json({ success: false, message: 'User already exists' }) } // สร้าง user ใหม่ const user = await User.create({ name, email, password }) const token = generateToken(user._id) res.status(201).json({ success: true, message: 'User registered successfully', token, data: { id: user._id, name: user.name, email: user.email, role: user.role } }) } catch (error) { if (error.name === 'ValidationError') { const messages = Object.values(error.errors).map(err => err.message) return res.status(400).json({ success: false, message: messages.join(', ') }) } res.status(500).json({ success: false, message: 'Server error' }) } } // @desc Login user // @route POST /api/users/login // @access Public const loginUser = async (req, res) => { try { const { email, password } = req.body // ตรวจสอบ email และ password if (!email || !password) { return res.status(400).json({ success: false, message: 'Please provide email and password' }) } // หา user และรวม password field const user = await User.findOne({ email }).select('+password') if (!user || !(await user.comparePassword(password))) { return res.status(401).json({ success: false, message: 'Invalid credentials' }) } // อัปเดต lastLogin user.lastLogin = new Date() await user.save() const token = generateToken(user._id) res.json({ success: true, message: 'Login successful', token, data: { id: user._id, name: user.name, email: user.email, role: user.role } }) } catch (error) { res.status(500).json({ success: false, message: 'Server error' }) } } // @desc Get all users // @route GET /api/users // @access Private/Admin const getUsers = async (req, res) => { try { const page = parseInt(req.query.page) || 1 const limit = parseInt(req.query.limit) || 10 const skip = (page - 1) * limit const users = await User.find({ isActive: true }) .select('-password') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) const total = await User.countDocuments({ isActive: true }) res.json({ success: true, data: users, pagination: { page, limit, total, pages: Math.ceil(total / limit) } }) } catch (error) { res.status(500).json({ success: false, message: 'Server error' }) } } module.exports = { registerUser, loginUser, getUsers }
// controllers/postController.js const Post = require('../models/Post') const User = require('../models/User') // @desc Create new post // @route POST /api/posts // @access Private const createPost = async (req, res) => { try { const postData = { ...req.body, author: req.user.id } const post = await Post.create(postData) // Populate author information await post.populate('author', 'name email') res.status(201).json({ success: true, data: post }) } catch (error) { if (error.name === 'ValidationError') { const messages = Object.values(error.errors).map(err => err.message) return res.status(400).json({ success: false, message: messages.join(', ') }) } res.status(500).json({ success: false, message: 'Server error' }) } } // @desc Get all posts // @route GET /api/posts // @access Public const getPosts = async (req, res) => { try { const { page = 1, limit = 10, status = 'published', category, tag, search, author } = req.query const query = { status } // Filter by category if (category) { query.category = category } // Filter by tag if (tag) { query.tags = { $in: [tag] } } // Filter by author if (author) { query.author = author } // Text search if (search) { query.$text = { $search: search } } const posts = await Post.find(query) .populate('author', 'name email avatar') .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)) const total = await Post.countDocuments(query) res.json({ success: true, data: posts, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / limit) } }) } catch (error) { res.status(500).json({ success: false, message: 'Server error' }) } } // @desc Get single post // @route GET /api/posts/:id // @access Public const getPost = async (req, res) => { try { const post = await Post.findById(req.params.id) .populate('author', 'name email avatar') if (!post) { return res.status(404).json({ success: false, message: 'Post not found' }) } // เพิ่ม view count post.viewCount += 1 await post.save() res.json({ success: true, data: post }) } catch (error) { if (error.name === 'CastError') { return res.status(404).json({ success: false, message: 'Post not found' }) } res.status(500).json({ success: false, message: 'Server error' }) } } module.exports = { createPost, getPosts, getPost }

Routes Integration

// routes/users.js const express = require('express') const { registerUser, loginUser, getUsers } = require('../controllers/userController') const { protect, authorize } = require('../middleware/auth') const router = express.Router() router.post('/register', registerUser) router.post('/login', loginUser) router.get('/', protect, authorize('admin'), getUsers) module.exports = router
// routes/posts.js const express = require('express') const { createPost, getPosts, getPost } = require('../controllers/postController') const { protect } = require('../middleware/auth') const router = express.Router() router.route('/') .get(getPosts) .post(protect, createPost) router.route('/:id') .get(getPost) module.exports = router

Main App Setup

// app.js require('dotenv').config() const express = require('express') const connectDB = require('./config/database') const errorHandler = require('./middleware/errorHandler') // Connect to database connectDB() const app = express() // Middleware app.use(express.json()) app.use(express.urlencoded({ extended: true })) // Routes app.use('/api/users', require('./routes/users')) app.use('/api/posts', require('./routes/posts')) // Error handler app.use(errorHandler) const PORT = process.env.PORT || 5000 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) })

Environment Variables

# .env NODE_ENV=development PORT=5000 MONGODB_URI=mongodb://localhost:27017/express-blog JWT_SECRET=your-super-secret-jwt-key JWT_EXPIRES_IN=30d

Best Practices

  1. Data Validation - ใช้ Mongoose schema validation
  2. Indexing - สร้าง indexes สำหรับ queries ที่ใช้บ่อย
  3. Pagination - implement pagination สำหรับ large datasets
  4. Error Handling - จัดการ errors อย่างครอบคลุม
  5. Security - validate input และ sanitize data
  6. Performance - ใช้ populate อย่างระมัดระวัง

สรุป

การรวม Express.js กับ MongoDB ผ่าน Mongoose ช่วยให้การสร้าง web applications ที่มี database backend เป็นเรื่องง่าย ด้วย schema validation, middleware, และ query builder ที่ทรงพลัง