Node.js: RESTful API Project
Last updated: 2026-08-26
1. Project Background
Alice and Bob were assigned a task: to develop a book management API for an online bookstore. Alice was responsible for designing the routing and middleware architecture, while Bob was responsible for writing the business logic. The two agreed on a standardized response format and error-handling conventions, and within two hours, they completed the entire project—from setting up the project to implementing full CRUD functionality.
(1) You will learn
- Build a complete API by combining Express, routing, and middleware
- Designing the project directory structure (routes / controllers / middleware / models)
- In-Memory Data Storage and Implementation of Full CRUD Operations
- Request Validation and Unified Error Handling
- API Testing Methods (curl / Postman)
- Design of a Unified Response Format
2. Designing the Project Directory Structure
A well-structured directory hierarchy is the foundation of a project’s maintainability. Following community best practices, Alice organizes the project into layers based on responsibilities:
book-api/
├── app.js
├── routes/
│ └── books.js
├── controllers/
│ └── bookController.js
├── middleware/
│ ├── validate.js
│ └── errorHandler.js
└── models/
└── bookModel.js
| Directory/File | Responsibilities | Description |
|---|---|---|
app.js |
Entry File | Create an Express instance, attach routes and middleware |
routes/ |
Route Definition | Defines HTTP methods and paths that point to the corresponding controller |
controllers/ |
Business Logic | Process the request, call the model, and return a response |
middleware/ |
Middleware | Cross-cutting concerns such as request validation and error handling |
models/ |
Data Models | Data Storage and Data Operation Encapsulation |
3. API Endpoint Design
Based on business requirements, Bob compiled a list of all API endpoints:
| HTTP Method | Path | Function | Success Status Code | Failure Status Code |
|---|---|---|---|---|
| GET | /api/books |
Get all books | 200 | — |
| GET | /api/books/:id |
Retrieve a single book | 200 | 404 |
| POST | /api/books |
New Books | 201 | 400 |
| PUT | /api/books/:id |
Update Book | 200 | 404 / 400 |
| DELETE | /api/books/:id |
Delete Book | 200 | 404 |
4. Standardized Response Format
Alice insists on a consistent response format so that the front-end team doesn't have to guess field names:
▶ Example: Successful Response
{
"success": true,
"data": { "id": 1, "title": "Node.js Guide", "author": "Alice" }
}
▶ Example: Error Response
{
"success": false,
"error": { "code": 404, "message": "Book not found" }
}
| Field | Type | Description |
|---|---|---|
success |
boolean | Whether the request was successful |
data |
any | Data returned upon success |
error.code |
number | Error Status Code |
error.message |
string | Error description |
5. Data Model Layer
Bob uses arrays in the models directory to simulate a database and encapsulate all data operations:
▶ Example: bookModel.js
const books = [
{ id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }
];
let nextId = 2;
function findAll() {
return books;
}
function findById(id) {
return books.find(b => b.id === id);
}
function create(data) {
const book = { id: nextId++, ...data };
books.push(book);
return book;
}
function update(id, data) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return null;
books[index] = { ...books[index], ...data };
return books[index];
}
function remove(id) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return false;
books.splice(index, 1);
return true;
}
module.exports = { findAll, findById, create, update, remove };
6. Request Validation Middleware
Alice implements data validation in the middleware layer to ensure that invalid requests do not reach the controller:
▶ Example: validate.js
function validateBook(req, res, next) {
const { title, author, year } = req.body;
const errors = [];
if (!title || typeof title !== "string") {
errors.push("title is required and must be a string");
}
if (!author || typeof author !== "string") {
errors.push("author is required and must be a string");
}
if (year !== undefined && (typeof year !== "number" || year < 0)) {
errors.push("year must be a non-negative number");
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
error: { code: 400, message: errors.join("; ") }
});
}
next();
}
module.exports = { validateBook };
7. Error-Handling Middleware
▶ Example: errorHandler.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
const status = err.status || 500;
res.status(status).json({
success: false,
error: { code: status, message: err.message || "Internal Server Error" }
});
}
function createError(status, message) {
const err = new Error(message);
err.status = status;
return err;
}
module.exports = { errorHandler, createError };
8. Controller Layer
Bob handles the business logic in the controller, calls the model, and returns a response in a standardized format:
▶ Example: bookController.js
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");
function getAllBooks(req, res) {
res.json({ success: true, data: Book.findAll() });
}
function getBookById(req, res, next) {
const book = Book.findById(Number(req.params.id));
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function createBook(req, res) {
const book = Book.create(req.body);
res.status(201).json({ success: true, data: book });
}
function updateBook(req, res, next) {
const book = Book.update(Number(req.params.id), req.body);
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function deleteBook(req, res, next) {
const removed = Book.remove(Number(req.params.id));
if (!removed) return next(createError(404, "Book not found"));
res.json({ success: true, data: { message: "Book deleted" } });
}
module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
9. Route Definitions
Alice decouples routing from the controller; routing is responsible only for mapping:
▶ Example: routes/books.js
const express = require("express");
const router = express.Router();
const controller = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");
router.get("/", controller.getAllBooks);
router.get("/:id", controller.getBookById);
router.post("/", validateBook, controller.createBook);
router.put("/:id", validateBook, controller.updateBook);
router.delete("/:id", controller.deleteBook);
module.exports = router;
10. Entry File app.js
▶ Example: app.js
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/api/books", booksRoute);
app.use(errorHandler);
const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
11. Project Architecture Overview
Alice drew an architecture diagram that clearly illustrates the flow of requests:
graph TD
A[app.js Entrance] --> B[routes/books.js]
B --> C{Needs verification?}
C -->|POST/PUT| D[middleware/validate.js]
C -->|GET/DELETE| E[controllers/bookController.js]
D --> E
E --> F[models/bookModel.js]
F --> E
E --> G[Standardized Response Format]
G --> H[Client]
E -->|Exception| I[middleware/errorHandler.js]
I --> H
12. API Testing
Bob uses curl to verify each endpoint one by one:
| curl command | Description |
|---|---|
curl localhost:3000/api/books |
Get All Books |
curl localhost:3000/api/books/1 |
Get a Specific Book |
curl -X POST -H "Content-Type: application/json" -d '{"title":"New Book","author":"Bob","year":2025}' localhost:3000/api/books |
New Books |
curl -X PUT -H "Content-Type: application/json" -d '{"year":2026}' localhost:3000/api/books/1 |
Update Books |
curl -X DELETE localhost:3000/api/books/1 |
Delete Book |
▶ Example: Testing Insert and Query Operations
# Add a book
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Express in Action","author":"Evan","year":2024}' \
localhost:3000/api/books
{"success":true,"data":{"id":2,"title":"Express in Action","author":"Evan","year":2024}}
# Search All Books
curl localhost:3000/api/books
{"success":true,"data":[{"id":1,"title":"Node.js Guide","author":"Alice","year":2024},{"id":2,"title":"Express in Action","author":"Evan","year":2024}]}
13. Comprehensive Example: Complete Book Management API
Combining all the previous modules, here is the core code flow for the complete project:
// models/bookModel.js
const books = [{ id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }];
let nextId = 2;
function findAll() { return books; }
function findById(id) { return books.find(b => b.id === id); }
function create(data) {
const book = { id: nextId++, ...data };
books.push(book);
return book;
}
function update(id, data) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return null;
books[index] = { ...books[index], ...data };
return books[index];
}
function remove(id) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return false;
books.splice(index, 1);
return true;
}
module.exports = { findAll, findById, create, update, remove };
// middleware/validate.js
function validateBook(req, res, next) {
const { title, author } = req.body;
const errors = [];
if (!title || typeof title !== "string") errors.push("title is required");
if (!author || typeof author !== "string") errors.push("author is required");
if (errors.length > 0) {
return res.status(400).json({
success: false,
error: { code: 400, message: errors.join("; ") }
});
}
next();
}
module.exports = { validateBook };
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const status = err.status || 500;
res.status(status).json({
success: false,
error: { code: status, message: err.message || "Internal Server Error" }
});
}
function createError(status, message) {
const err = new Error(message);
err.status = status;
return err;
}
module.exports = { errorHandler, createError };
// controllers/bookController.js
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");
function getAllBooks(req, res) {
res.json({ success: true, data: Book.findAll() });
}
function getBookById(req, res, next) {
const book = Book.findById(Number(req.params.id));
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function createBook(req, res) {
const book = Book.create(req.body);
res.status(201).json({ success: true, data: book });
}
function updateBook(req, res, next) {
const book = Book.update(Number(req.params.id), req.body);
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function deleteBook(req, res, next) {
const removed = Book.remove(Number(req.params.id));
if (!removed) return next(createError(404, "Book not found"));
res.json({ success: true, data: { message: "Book deleted" } });
}
module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
// routes/books.js
const express = require("express");
const router = express.Router();
const ctrl = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");
router.get("/", ctrl.getAllBooks);
router.get("/:id", ctrl.getBookById);
router.post("/", validateBook, ctrl.createBook);
router.put("/:id", validateBook, ctrl.updateBook);
router.delete("/:id", ctrl.deleteBook);
module.exports = router;
// app.js
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/api/books", booksRoute);
app.use(errorHandler);
app.listen(3000, () => console.log("Server running on port 3000"));
# Start and Test
node app.js
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Clean Code","author":"Robert","year":2008}' \
localhost:3000/api/books
curl localhost:3000/api/books/1
curl -X DELETE localhost:3000/api/books/1
❓ FAQ
curl on the command line, Postman or the Thunder Client plugin for VS Code for a graphical interface, and the supertest library for automated testing.{ success: false, error: { code: 404, message: "Book not found" } }.📖 Summary
- Core Concepts and Usage of the Project Background
- Core Concepts and Usage of Project Directory Structure Design
- Core Concepts and Usage of API Endpoint Design
- Core Concepts and Usage of the Unified Response Format
- Core Concepts and Usage of the Data Model Layer
- Core Concepts and Usage of Request Validation Middleware
- Core Concepts and Usage of Error-Handling Middleware
- Core Concepts and Usage of the Controller Layer
📝 Exercises
- Complete all the code examples in this lesson and make sure each one runs correctly.
- Modify the comprehensive example and add your own extensions
- Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
- Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
- Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.