REST API built with Node.js and Express, refactored to follow the MVC (Model-View-Controller) architectural pattern.
express-books-api/
├── index.js # Entry point: app setup, middleware, server start
├── books.json # In-memory data store (resets on restart)
├── config/
│ └── routes.config.js # Route definitions — maps URLs to controller actions
├── controllers/
│ └── book.controller.js # Request/response logic for the Book resource
└── models/
└── book.model.js # Data access and business logic for books
MVC separates an application into three distinct responsibilities. Each layer has one job and communicates only with the layer directly below it.
Request → Router → Controller → Model → (data)
↓
Response
The model is the only layer that knows about the data. It contains:
- The Joi validation schema that enforces required fields on creation.
- All data access methods (
find,findById,create,findByIdAndUpdate,findByIdAndDelete) — modelled after the Mongoose API so the transition to a real database is straightforward.
The model never imports Express or touches req/res.
The controller sits between the router and the model. It:
- Reads data from the incoming request (
req.params,req.query,req.body). - Calls the appropriate model method.
- Sends the HTTP response (
res.json(...),res.status(...)).
Controllers contain no business logic — if you find yourself filtering an array inside a controller, that code belongs in the model.
The router maps each HTTP method + URL pattern to a controller action. It does nothing else.
Centralising routes here means index.js stays clean and new resource routers can be added with a single app.use(...) line.
Bootstraps Express, registers global middleware (express.json()), mounts the router, and starts the server. Nothing else lives here.
npm install
npm run devThe server starts on http://localhost:8000.
| Method | URL | Description |
|---|---|---|
| GET | /api/books |
Return all books |
| GET | /api/books?genre=sci-fi |
Filter by genre |
| GET | /api/books?sort=year |
Sort by year (ascending) |
| GET | /api/books?genre=sci-fi&sort=year |
Filter + sort combined |
| GET | /api/books/:id |
Return a single book by id |
| POST | /api/books |
Create a new book |
| PUT | /api/books/:id |
Partially update a book |
| DELETE | /api/books/:id |
Delete a book |
{
"title": "Refactoring",
"author": "Martin Fowler",
"genre": "programming",
"year": 1999
}All fields except available are required on creation. available defaults to undefined when omitted.
| Status | When |
|---|---|
| 200 | Successful GET, PUT |
| 204 | Successful DELETE (no body) |
| 400 | Validation error on POST (missing or wrong field type) |
| 404 | Book id not found |
- The
booksarray is in-memory only — all changes reset when the server restarts. - Validation uses Joi. The schema lives in the model, not the controller.
node --watch(Node 18+) provides hot-reload without nodemon.- Method names (
find,findById,findByIdAndUpdate,findByIdAndDelete) intentionally mirror the Mongoose API to ease the migration to MongoDB later.