Week 10: Capstone — Build a Full CRUD API Backed by MongoDB

No new concepts this week — this is where the previous nine come together. You'll model a real schema, expose it through a validated Express + Mongoose API, add an aggregation-powered reporting endpoint, and deploy the whole thing against a live Atlas cluster. The finished project is a portfolio piece: something you built end to end and can defend every decision behind.

Module 10 of 10 Week 10 of 10 ~4–5 Hours Capstone Project

By the end of this week, you'll be able to

  • Justify an embedding/referencing decision for a real domain, not just a textbook example
  • Ship a validated, fully-tested CRUD API on top of that schema
  • Add a reporting endpoint powered by a real aggregation pipeline
  • Deploy the whole project against Atlas, ready to link from a resume or portfolio

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.

models/Book.js
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);
models/Shelf.js — one reader's shelf, with embedded reviews
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.

routes/shelf.js — adding a book to a reader's shelf
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.

routes/reports.js — GET /api/reports/by-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.

server.js
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

Capstone

Ship the full bookshelf tracker API, deployed to Atlas

Everything from Sections 1–5, built out completely and deployed.

Requirements:

  1. Implement Book and Shelf models exactly as modeled in Section 2, with full Mongoose validation on every field.
  2. Build every CRUD route: books (full CRUD) and shelf entries (add, update status/rating/review, remove).
  3. Implement the /api/reports/by-genre aggregation 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).
  4. Write a short README explaining your embedding/referencing decision for Shelf, in your own words, tied back to the criteria from Week 3.
  5. Deploy the API against a real Atlas cluster and confirm every route works end to end against the live deployment, not just locally.
What "done" looks like

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.