NelsonLabs
Express.js/Project: Bookstore REST API

Project: Bookstore REST API

Build a complete Bookstore REST API with CRUD for books, author management, search, and JWT authentication. This is a production-level API structure you can use as a template.

Project structure

Directory structure
bash
bookstore-api/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ index.js          # entry point
โ”‚   โ”œโ”€โ”€ db.js             # database connection
โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ”œโ”€โ”€ auth.js       # JWT middleware
โ”‚   โ”‚   โ””โ”€โ”€ errorHandler.js
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”œโ”€โ”€ Book.js
โ”‚   โ”‚   โ””โ”€โ”€ User.js
โ”‚   โ””โ”€โ”€ routes/
โ”‚       โ”œโ”€โ”€ auth.js
โ”‚       โ””โ”€โ”€ books.js
โ”œโ”€โ”€ .env
โ”œโ”€โ”€ .gitignore
โ””โ”€โ”€ package.json
src/routes/books.js โ€” complete CRUD
javascript
const router  = require("express").Router();
const Book     = require("../models/Book");
const { auth } = require("../middleware/auth");

// GET /api/books โ€” list with search + pagination
router.get("/", async (req, res, next) => {
  try {
    const { q, genre, page = 1, limit = 20 } = req.query;
    const filter = {};
    if (q)     filter.$or = [{ title: new RegExp(q, "i") }, { author: new RegExp(q, "i") }];
    if (genre) filter.genre = genre;

    const [books, total] = await Promise.all([
      Book.find(filter).skip((page - 1) * limit).limit(Number(limit)).sort("-createdAt"),
      Book.countDocuments(filter),
    ]);

    res.json({ books, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) { next(err); }
});

// GET /api/books/:id
router.get("/:id", async (req, res, next) => {
  try {
    const book = await Book.findById(req.params.id);
    if (!book) return res.status(404).json({ error: "Book not found" });
    res.json(book);
  } catch (err) { next(err); }
});

// POST /api/books โ€” protected
router.post("/", auth, async (req, res, next) => {
  try {
    const book = await Book.create({ ...req.body, addedBy: req.user.id });
    res.status(201).json(book);
  } catch (err) { next(err); }
});

// PUT /api/books/:id โ€” protected
router.put("/:id", auth, async (req, res, next) => {
  try {
    const book = await Book.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
    if (!book) return res.status(404).json({ error: "Book not found" });
    res.json(book);
  } catch (err) { next(err); }
});

// DELETE /api/books/:id โ€” protected
router.delete("/:id", auth, async (req, res, next) => {
  try {
    await Book.findByIdAndDelete(req.params.id);
    res.sendStatus(204);
  } catch (err) { next(err); }
});

module.exports = router;