So far we have driven MongoDB from the shell. Real applications talk to it through a driver, and in the Node.js world most teams add Mongoose, an object-document mapper (ODM) that brings schemas, validation, and middleware to your models. In this CodeYourCraft lesson you will connect to MongoDB from Node.js, define a Mongoose schema, and build the CRUD operations behind a small products API.
mkdir shop-api && cd shop-api
npm init -y
npm install mongoose dotenvPut your connection string in a .env file so it never lands in version control:
MONGODB_URI=mongodb+srv://appuser:secret@cluster0.abc12.mongodb.net/shop// db.js
require('dotenv').config()
const mongoose = require('mongoose')
async function connectDB() {
try {
await mongoose.connect(process.env.MONGODB_URI)
console.log('MongoDB connected')
} catch (err) {
console.error('Connection failed:', err.message)
process.exit(1)
}
}
module.exports = connectDBMongoose manages a connection pool for you, so you connect once at startup and reuse it everywhere.
A Mongoose schema declares the shape, types, defaults, and validation rules of documents in a collection. A model wraps the schema with query methods:
// 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 },
category: { type: String, enum: ['audio', 'video', 'accessories'], index: true },
tags: [String],
stock: { type: Number, default: 0 }
}, { timestamps: true })
module.exports = mongoose.model('Product', productSchema)The timestamps option adds createdAt and updatedAt automatically, and Mongoose pluralizes the model name to use the products collection.
const Product = require('./models/Product')
// Create
const lamp = await Product.create({ name: 'Desk Lamp', price: 1299, category: 'accessories' })
// Read
const cheap = await Product.find({ price: { $lt: 5000 } })
.sort({ price: 1 })
.limit(10)
.lean() // plain objects, faster for read-only use
// Update
await Product.findByIdAndUpdate(lamp._id, { $inc: { stock: 25 } }, { new: true })
// Delete
await Product.findByIdAndDelete(lamp._id)The query syntax is the same MongoDB language you already know from mongosh, which is why we learned the shell first. The lean() call is a favorite performance tip: it skips constructing full Mongoose documents when you only need data to send as JSON.
Schema validation runs before every save, turning bad data into catchable errors instead of corrupt documents:
try {
await Product.create({ name: 'Ghost Item', price: -50 })
} catch (err) {
console.log(err.errors.price.message) // below minimum value
}Middleware (hooks) lets you run logic around operations, for example generating a slug before saving:
productSchema.pre('save', function () {
this.slug = this.name.toLowerCase().replace(/\s+/g, '-')
})The official mongodb driver is lighter and maps one-to-one to shell commands, which suits microservices and scripts. Mongoose adds schemas, casting, validation, population of references, and hooks, which pay off in larger applications. Both share the same query language underneath, so nothing you have learned is wasted either way.
To finish the series, the next lesson covers the best practices and security checklist every production MongoDB deployment needs.