1. The Project Brief
Build a bookshelf tracker API — readers, the books they own, and the reviews they leave. It's small enough to finish in one sitting, but touches every module in this course: embedding vs. referencing, validation, aggregation, and a real deployment.
The API needs to support:
- Creating, reading, updating and deleting books (title, author, genre, publish year).
- Readers adding a book to their shelf with a status (
want-to-read,reading,finished) and, once finished, a rating and short review. - A reporting endpoint returning the average rating and review count per genre, across every reader.
2. Modeling the Schema
This is Week 3's decision, made for real: a review is tightly coupled to one reader's relationship with one book — it's never queried independently of both — which makes it a strong embedding candidate. Books, on the other hand, are shared across many readers' shelves and queried on their own (browsing by genre), so they stay a separate, referenced collection.
const bookSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
author: { type: String, required: true, trim: true },
genre: {
type: String,
required: true,
enum: ['Fiction', 'Non-Fiction', 'Sci-Fi', 'Mystery', 'Biography']
},
publishYear: { type: Number, min: 1000, max: new Date().getFullYear() }
});
const Book = mongoose.model('Book', bookSchema);
const shelfEntrySchema = new mongoose.Schema({
book: { type: mongoose.Schema.Types.ObjectId, ref: 'Book', required: true },
status: {
type: String,
required: true,
enum: ['want-to-read', 'reading', 'finished'],
default: 'want-to-read'
},
rating: { type: Number, min: 1, max: 5 },
review: { type: String, trim: true, maxlength: 1000 },
updatedAt: { type: Date, default: Date.now }
});
const shelfSchema = new mongoose.Schema({
readerName: { type: String, required: true, trim: true },
entries: [shelfEntrySchema]
});
const Shelf = mongoose.model('Shelf', shelfSchema);
Notice shelfEntrySchema is embedded directly inside Shelf
as an array, while book is only a reference — exactly the "one-to-few"
embedding pattern from Week 3, applied to a domain the syllabus hasn't already
solved for you.
3. Building the API
Standard CRUD routes on top of both models, with the validation habits from Week 4
enforced throughout — every write route validates, every update route opts into
runValidators: true, every failure returns a real status code.
router.post('/:readerId/entries', async (req, res) => {
try {
const shelf = await Shelf.findById(req.params.readerId);
if (!shelf) return res.status(404).json({ error: 'Reader not found' });
shelf.entries.push(req.body); // { book, status } at minimum
await shelf.save(); // runs full schema validation on save
res.status(201).json(shelf);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// PATCH a single entry -- e.g. marking a book "finished" with a rating
router.patch('/:readerId/entries/:entryId', async (req, res) => {
const shelf = await Shelf.findById(req.params.readerId);
if (!shelf) return res.status(404).json({ error: 'Reader not found' });
const entry = shelf.entries.id(req.params.entryId);
if (!entry) return res.status(404).json({ error: 'Entry not found' });
Object.assign(entry, req.body);
entry.updatedAt = new Date();
try {
await shelf.save();
res.json(shelf);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
shelf.entries.id(entryId) is a Mongoose convenience for finding one
subdocument inside an embedded array by its own auto-generated _id —
worth knowing, since embedded array elements get an _id by default
just like top-level documents do.
4. The Reporting Endpoint
This is Week 5's material, applied to the finished schema: one aggregation pipeline, run across every shelf, producing average rating and review count per genre.
router.get('/by-genre', async (req, res) => {
const report = await Shelf.aggregate([
{ $unwind: '$entries' },
{ $match: { 'entries.rating': { $exists: true } } },
{
$lookup: {
from: 'books',
localField: 'entries.book',
foreignField: '_id',
as: 'bookInfo'
}
},
{ $unwind: '$bookInfo' },
{
$group: {
_id: '$bookInfo.genre',
avgRating: { $avg: '$entries.rating' },
reviewCount: { $sum: 1 }
}
},
{ $sort: { avgRating: -1 } }
]);
res.json(report);
});
Every stage here traces back to a specific week: $unwind and
$lookup from Week 5, $match filtering out entries with no
rating yet, and a final $group/$sort — the exact shape
from Week 2's first pipeline, just against real, self-designed data.
5. Deploying Against Atlas
Point the finished API at the Atlas cluster from Week 9 instead of a local database — the only change is the connection string, sourced from an environment variable rather than hardcoded.
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
app.use('/api/books', require('./routes/books'));
app.use('/api/shelf', require('./routes/shelf'));
app.use('/api/reports', require('./routes/reports'));
mongoose.connect(process.env.MONGODB_URI)
.then(() => app.listen(process.env.PORT || 3000, () => console.log('API running')))
.catch(err => console.error('Failed to connect to MongoDB:', err));
Deploy the Express app itself to any Node-friendly host (Render, Railway, Fly.io
all have generous free tiers), set MONGODB_URI as an environment
variable there rather than committing it, and confirm the deployed API's routes
work against the live Atlas cluster before calling the project done.
6. Capstone Project
Ship the full bookshelf tracker API, deployed to Atlas
Everything from Sections 1–5, built out completely and deployed.
Requirements:
- Implement
BookandShelfmodels exactly as modeled in Section 2, with full Mongoose validation on every field. - Build every CRUD route: books (full CRUD) and shelf entries (add, update status/rating/review, remove).
- Implement the
/api/reports/by-genreaggregation endpoint from Section 4, and add a second report of your own design (e.g., a reader's personal reading stats, or the 5 highest-rated books overall). - Write a short
READMEexplaining your embedding/referencing decision forShelf, in your own words, tied back to the criteria from Week 3. - Deploy the API against a real Atlas cluster and confirm every route works end to end against the live deployment, not just locally.
A deployed URL you can hit with curl or Postman right now, a README that explains your schema decisions rather than just listing routes, and a reporting endpoint that returns a real, computed answer — not a stub. That combination is what makes this a portfolio piece rather than a tutorial exercise.
7. Course Recap
Ten weeks, ten ideas that build on each other:
- Weeks 1–2 — the document model, CRUD, querying, indexing, and a first aggregation pipeline.
- Weeks 3–4 — schema design decisions, and wiring them into real application code with the driver and Mongoose.
- Week 5 — joining, flattening and summarizing data with advanced aggregation.
- Week 6 — atomicity, transactions, and designing them away where possible.
- Weeks 7–8 — replication for availability, sharding for scale, and knowing which one a given problem actually needs.
- Week 9 — running all of it on Atlas instead of a laptop.
- Week 10 — all of it, together, in one project you built and can explain.
That's the full MongoDB path. From here, the Node.js & Express course goes deeper on the API layer this capstone only sketched, and the schema-design instincts from Week 3 carry directly into any document database you touch next.