Build a Book Collection REST API
Build a REST API with Node.js and Express to manage a book collection.
Introduction
30 Second Summary
Every time you open a weather app, scroll social media, or shop online, your device is talking to a REST API behind the scenes. These APIs are the invisible backbone that connects apps to their data.
In this project, you will build a REST API from scratch using Node.js and Express that manages a collection of books with full create, read, update, and delete operations.
What You'll Build
A Book Collection REST API that stores books in memory and responds with proper JSON and HTTP status codes.
By the end of this project, you'll have:
- An Express server with 5 RESTful endpoints that handle GET, POST, PUT, and DELETE operations on a book collection.
- A router module that organizes book-related routes into a clean, separate file.
- Custom error handling middleware that catches and formats all errors with proper status codes.
- An .http file with test requests that prove every endpoint works, tested directly inside VS Code.
- Secret Mission: Add query parameter filtering to search books by author or genre.
Do I need JavaScript experience?
Nope. If you know basic programming concepts like variables, functions, and arrays (from Java or any other language), you have everything you need. You will pick up JavaScript along the way.
Set Up Your Project
Before you can build an API, you need a working development environment with the right tools in place.
In this step, you will create a project folder, install Express (the framework that powers your API), set up a tool for testing HTTP requests, and confirm everything works by running a basic server.
In this step, get ready to:
- Create your project folder and install Express.
- Install the REST Client extension for testing your API.
- Create and run your first Express server.
Create your project and install Express
You need a dedicated folder for your project and a couple of packages installed before writing any code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir book-api
cd book-api
You are now inside the empty book-api folder on your Desktop.
- Initialize your Node.js project and install Express by running these commands:
npm init -y
npm install express
What do these commands do?
- npm init -y creates a package.json file that tracks your project's name, version, and dependencies.
- npm install express downloads the Express framework and saves it as a dependency in package.json.
- Confirm everything is set up by running:
ls
- You should see node_modules, package.json, and package-lock.json in the output.
Seeing an error from npm?
- Make sure you ran cd book-api before running npm init -y. Running npm commands outside a project folder can cause unexpected behavior.
- If you see a permissions error, try running the command with sudo on macOS/Linux or open VS Code as Administrator on Windows.
Help me troubleshoot my npm install error
Install the REST Client extension
You will test your API by sending HTTP requests directly from VS Code. The REST Client extension lets you write and send requests from a simple text file without leaving your editor.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type REST Client in the search bar.
- Find the result by Huachao Mao (extension ID: humao.rest-client).
- Click Install.
Why REST Client instead of Postman?
REST Client keeps your test requests as .http files right inside your project folder. You can version-control them alongside your code and run them without switching to an external app.
- Verify the extension is active by checking that it appears under the Installed section of the Extensions panel.
Create your entry point file
Every Express application needs an entry point file that creates the server and tells it how to handle incoming requests.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the book-api folder, and click Open.
Seeing a trust dialog?
VS Code may ask "Do you trust the authors of the files in this folder?" Click Yes, I trust the authors to continue. This lets VS Code run code and extensions in your project.
- In the Explorer sidebar on the left, click the New File icon (a page with a plus sign) next to the folder name.
- Name the file app.js and press Enter.
- Add the following starter code to app.js to create a basic Express server:
// Import Express and create an app instance
const express = require('express');
const app = express();
const PORT = 3000;
// Enable JSON parsing for incoming request bodies
app.use(express.json());
// Define a test route to confirm the server works
app.get('/', (req, res) => {
res.json({ message: 'Book API is running' });
});
// Start the server and listen for requests
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
- require('express') loads the Express framework you just installed.
- express() creates your application object, which you use to define routes and settings.
- app.use(express.json()) tells Express to automatically parse JSON in incoming request bodies. You will need this when creating books later.
- app.get('/', ...) defines a route that responds to GET requests at the root URL with a JSON message.
- app.listen(PORT, ...) starts the server on port 3000 so it can receive requests.
- Save app.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Start your server by running this command in the terminal:
node app.js
- You should see this message in your terminal:
Server running on http://localhost:3000
- Open your browser and visit http://localhost:3000.
- You should see {"message":"Book API is running"} displayed in the browser window.
Not seeing the JSON message?
- Check that your terminal shows "Server running on http://localhost:3000" without any errors. If you see a syntax error, compare your app.js against the code above character by character.
- Make sure you saved the file before running node app.js. Unsaved changes are not picked up.
- If you see "address already in use," another process is using port 3000. Stop it by pressing Ctrl+C in the terminal and try again.
Help me fix my Express server startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js and your server is running.
ⓧ I'd like to double check the full code
Here is the complete app.js file at this point:
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
app.get('/', (req, res) => {
res.json({ message: 'Book API is running' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Your development environment is ready and Express is responding to requests. Next up, you will build the GET endpoints that return book data from your collection.
Build the GET Endpoints
Your Express server is up and running, responding to requests at http://localhost:3000. Now it's time to give it real data to serve.
In this step, you'll add book data to your server and create endpoints that return it. You'll also learn how route parameters let clients request a specific book by its ID.
In this step, get ready to:
- Add sample book data to your server's memory.
- Build GET endpoints that return all books or a single book by ID.
- Test your endpoints using the REST Client extension.
Add in-memory book data
Your API needs data to serve. Instead of connecting to a database, you'll store books in a JavaScript array. This keeps the focus on learning how endpoints work.
- In app.js, find the line that says app.use(express.json()); and add the following code below it:
// Sample book data stored in memory
let books = [
{ id: 1, title: '1984', author: 'George Orwell', genre: 'Dystopian', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Fiction', year: 1925 }
];
// Tracks the next available ID for new books
let nextId = 4;
What does this code do?
The books array holds three book objects, each with an id, title, author, genre, and year. This acts as your data store for the entire project.
The nextId variable keeps track of the next ID to assign when a new book is created. It starts at 4 because you already have books with IDs 1, 2, and 3.
You won't see a change yet. The next chunk adds routes that serve this data.
Create GET /books and GET /books/:id routes
A REST API uses HTTP methods to perform actions on resources. The GET method retrieves data without modifying it.
- In app.js, find the closing bracket of the existing app.get('/', ...) route and add the following code below it:
// Return all books in the collection
app.get('/books', (req, res) => {
res.json(books);
});
// Return a single book by its ID from the URL
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
What does this code do?
- GET /books responds with the entire books array as JSON. Any client that hits this URL gets back every book.
- GET /books/:id uses a route parameter (:id) to capture the book's ID from the URL. Express stores this value in req.params.id.
- The .find() method searches the array for a matching book. If none is found, the route returns a 404 status code with an error message.
What is a route parameter?
The :id part of /books/:id is a route parameter. It's a placeholder that matches any value in that URL position. When a client sends GET /books/2, Express captures 2 and makes it available as req.params.id.
Since URL values always arrive as strings, you need parseInt() to convert the string "2" into the number 2 before comparing it to the book IDs in your array.
- Save app.js.
- If your server is still running, press Ctrl+C in the terminal to stop it.
- Restart your server by running:
node app.js
- Open your browser and visit http://localhost:3000/books. You should see a JSON array containing all three books.
Don't see the book data?
- Make sure you placed the books array above the route that uses it. JavaScript reads top to bottom, so the data must exist before the route tries to reference it.
- Check that you stopped the old server with Ctrl+C before running node app.js again. The old process won't pick up file changes.
- If you see Cannot GET /books, double check that app.get('/books', ...) is spelled exactly right with the forward slash.
Still stuck? Help me debug why my GET /books endpoint isn't returning data.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
let books = [
{ id: 1, title: '1984', author: 'George Orwell', genre: 'Dystopian', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Fiction', year: 1925 }
];
let nextId = 4;
app.get('/', (req, res) => {
res.json({ message: 'Book API is running' });
});
app.get('/books', (req, res) => {
res.json(books);
});
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Test with REST Client
Checking endpoints in a browser works for GET requests, but you'll need a better tool for POST, PUT, and DELETE later. The REST Client extension lets you send any HTTP request directly from VS Code.
- Create a new file called requests.http in your book-api folder.
- Add the following test requests to requests.http:
### Get all books
GET http://localhost:3000/books
### Get a single book
GET http://localhost:3000/books/1
### Get a non-existent book (should return 404)
GET http://localhost:3000/books/99
What does this file do?
- Each ### line is a separator that divides individual requests. The text after ### is a label describing what the request tests.
- Each request starts with the HTTP method (GET) followed by the full URL. REST Client reads this and sends the request to your running server.
- The third request intentionally asks for a book that doesn't exist. This confirms your 404 error handling works correctly.
- Save requests.http.
- Make sure your server is still running in the terminal. If you stopped it, run node app.js again.
- Click the Send Request link that appears above the first GET http://localhost:3000/books line.
- A response panel opens on the right showing the JSON array of 3 books with status 200 OK.
- Click Send Request above the second request (GET /books/1). You should see a single book object for "1984".
- Click Send Request above the third request (GET /books/99). You should see status 404 Not Found with the message {"error":"Book not found"}.
Not seeing the Send Request link?
- Make sure your file is saved with the .http extension. REST Client only activates for .http or .rest files.
- Check that VS Code's CodeLens feature is enabled. Open Settings (Ctrl+,) and search for editor.codeLens. Make sure it's checked.
- If you see a connection error, confirm your server is running. You should see Server running on http://localhost:3000 in the terminal.
Still stuck? Help me get REST Client working in VS Code.
✔️ Awesome, I've got everything!
Great. Double check you've saved requests.http before moving on.
ⓧ I'd like to double check the full code
### Get all books
GET http://localhost:3000/books
### Get a single book
GET http://localhost:3000/books/1
### Get a non-existent book (should return 404)
GET http://localhost:3000/books/99
Your API can now serve book data to any client that asks for it. Next up, you'll build the endpoints that let clients create and update books.
Build POST and PUT Endpoints
Your GET endpoints let clients read data from the API. But a real application needs to create and update data too.
In this step, you will build two new endpoints that accept data from the client. The POST endpoint creates new books, and the PUT endpoint updates existing ones. Both will validate incoming data before making changes.
In this step, get ready to:
- Create a POST endpoint that adds new books with input validation.
- Create a PUT endpoint that updates existing books by ID.
- Test both endpoints using REST Client.
Create the POST /books endpoint
When a client sends a POST request, it includes data in the request body. Your endpoint needs to read that data, check it is valid, and then add it to the collection.
- In app.js, find the closing }); of your GET /books/:id route.
- Add the following POST route below it, above app.listen():
app.post('/books', (req, res) => {
// Pull title, author, genre, year from the request body
const { title, author, genre, year } = req.body;
// Reject the request if required fields are missing
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
// Build the new book object with an auto-incrementing ID
const newBook = {
id: nextId++,
title,
author,
genre: genre || 'Unknown',
year: year || null
};
// Add to the collection and respond with 201 Created
books.push(newBook);
res.status(201).json(newBook);
});
What does this code do?
This route handles POST requests to /books. It uses destructuring to pull fields out of req.body (the JSON the client sent).
- If title or author is missing, the API responds with status 400 (Bad Request) and stops processing.
- nextId++ assigns the current ID and then increments for the next book.
- genre || 'Unknown' provides a default value if the client did not supply one.
- Status 201 means "Created" and tells the client the resource was successfully added.
- Save app.js.
- You won't see a change yet. The next substep adds the PUT route, then you will test both together.
Seeing a syntax error when you save?
- Make sure the new app.post() block is placed after the closing }); of the GET /books/:id route, not inside it.
- Check that every opening { has a matching closing }. VS Code highlights mismatched brackets in red.
Help me fix a syntax error in my Express POST route.
Create the PUT /books/:id endpoint
PUT requests replace an existing resource with new data. Your PUT endpoint combines the route parameter pattern from GET /books/:id with the body-reading pattern from POST.
- In app.js, add the following PUT route directly below the POST route you just added, still above app.listen():
app.put('/books/:id', (req, res) => {
// Find the book by ID from the URL parameter
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
// Pull updated fields from the request body
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
// Update the book's properties
book.title = title;
book.author = author;
book.genre = genre || book.genre;
book.year = year || book.year;
// Respond with the updated book
res.json(book);
});
What does this code do?
- It first looks up the book by ID using the same books.find() pattern as your GET route. If the book does not exist, it returns 404.
- It validates that title and author are present, just like POST.
- genre || book.genre keeps the existing genre if the client did not send a new one.
- The response uses the default status 200 (OK), which is what res.json() sends automatically.
- Save app.js.
- You won't see a change yet. The next substep tests both new endpoints.
Red squiggly lines in VS Code?
- Confirm the PUT route sits outside the POST route's closing });. Nesting one route inside another causes errors.
- Check that app.listen() is still the very last block in your file.
Help me debug my Express route placement.
Test POST and PUT with REST Client
POST and PUT requests include a JSON body. In your .http file, you specify the body by adding a Content-Type header and then the JSON data after a blank line.
- Open requests.http and add these test requests at the bottom of the file:
### Create a new book
POST http://localhost:3000/books
Content-Type: application/json
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Science Fiction",
"year": 1965
}
### Create with missing fields (should return 400)
POST http://localhost:3000/books
Content-Type: application/json
{
"genre": "Mystery"
}
### Update a book
PUT http://localhost:3000/books/1
Content-Type: application/json
{
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian Fiction",
"year": 1949
}
What does each request test?
- The first POST sends valid data. You should get back the new book with id: 4 and status 201.
- The second POST is deliberately missing title and author. You should see status 400 with the error message.
- The PUT updates book 1's genre from "Dystopian" to "Dystopian Fiction". You should get the updated book back with status 200.
- Save requests.http.
- Switch back to your terminal. If your server is still running from before, press Ctrl+C to stop it.
- Restart the server by running:
node app.js
- Return to requests.http and click Send Request above the "Create a new book" POST request.
- Verify the response panel shows status 201 Created and the new book JSON with "id": 4.
- Click Send Request above the "Create with missing fields" request.
- Verify the response shows status 400 Bad Request and the error message Title and author are required.
- Click Send Request above the "Update a book" PUT request.
- Verify the response shows status 200 OK and the book now has "genre": "Dystopian Fiction".
Getting no response or a connection error?
- Make sure your server is running. You should see Server running on http://localhost:3000 in the terminal.
- Check that you restarted the server after saving your code changes. The old running server does not pick up new routes automatically.
- Make sure there is a blank line between the Content-Type header and the JSON body in your .http file. REST Client uses this blank line to separate headers from the body.
Help me troubleshoot my REST Client POST request.
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.js and requests.http.
ⓧ I'd like to double check the full code
Here is the full app.js at this point:
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
let books = [
{ id: 1, title: '1984', author: 'George Orwell', genre: 'Dystopian', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Fiction', year: 1925 }
];
let nextId = 4;
app.get('/', (req, res) => {
res.json({ message: 'Book API is running' });
});
app.get('/books', (req, res) => {
res.json(books);
});
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
app.post('/books', (req, res) => {
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
const newBook = {
id: nextId++,
title,
author,
genre: genre || 'Unknown',
year: year || null
};
books.push(newBook);
res.status(201).json(newBook);
});
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
book.title = title;
book.author = author;
book.genre = genre || book.genre;
book.year = year || book.year;
res.json(book);
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
And here is the full requests.http:
### Get all books
GET http://localhost:3000/books
### Get a single book
GET http://localhost:3000/books/1
### Get a non-existent book (should return 404)
GET http://localhost:3000/books/99
### Create a new book
POST http://localhost:3000/books
Content-Type: application/json
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Science Fiction",
"year": 1965
}
### Create with missing fields (should return 400)
POST http://localhost:3000/books
Content-Type: application/json
{
"genre": "Mystery"
}
### Update a book
PUT http://localhost:3000/books/1
Content-Type: application/json
{
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian Fiction",
"year": 1949
}
Your API can now create and update books. Next up, you will add the DELETE endpoint and then reorganize your routes into a separate module for cleaner code.
Build DELETE and Extract Routes
Your API can now create, read, and update books. The last CRUD operation to complete is DELETE, which lets users remove a book from the collection.
After adding DELETE, you will reorganize all your route handlers into a separate module using Express Router. This keeps app.js focused on server configuration and makes your project easier to maintain as it grows.
In this step, get ready to:
- Create the DELETE endpoint to remove books.
- Extract all routes into a dedicated router module.
- Verify all endpoints still work after refactoring.
Add the DELETE endpoint
A DELETE request tells the server to remove a specific resource. When the deletion succeeds, the API returns status 204 (No Content) with an empty response body, since there is nothing left to send back.
- In app.js, find the closing }); of your PUT route handler.
- Add the following DELETE route below it, before app.listen:
app.delete('/books/:id', (req, res) => {
// Find the index of the book to remove
const index = books.findIndex(b => b.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Book not found' });
}
// Remove the book from the array
books.splice(index, 1);
res.status(204).send();
});
What does this code do?
- books.findIndex() searches the array and returns the position of the matching book, or -1 if no match exists.
- books.splice(index, 1) removes exactly one element at that position from the array.
- res.status(204).send() returns an empty response with a 204 status code, signaling successful deletion with no body.
- Now add the DELETE test requests to requests.http. Scroll to the bottom of the file and paste this below your existing requests:
### Delete a book
DELETE http://localhost:3000/books/1
### Verify deleted book returns 404
GET http://localhost:3000/books/1
What do these requests test?
The first request deletes book 1. The second request tries to fetch the same book afterward. Since it was just deleted, the API should return a 404 error.
- Save both app.js and requests.http.
- Stop your server if it is running by pressing Ctrl+C in the terminal.
- Restart it by running:
node app.js
- In requests.http, click Send Request above the DELETE http://localhost:3000/books/1 line. You should see a 204 response with no body.
- Click Send Request above GET http://localhost:3000/books/1. You should see a 404 response with the message Book not found.
Still seeing 200 on the GET request?
- Make sure you sent the DELETE request first. The data is in-memory, so the order matters.
- If you restarted the server between the two requests, the data resets. Send both requests without restarting in between.
- Check that your DELETE handler uses books.splice(index, 1) and not books.slice (splice modifies the array, slice does not).
Help me debug my DELETE endpoint returning unexpected results.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full requests.http file so far:
### Get all books
GET http://localhost:3000/books
### Get a single book
GET http://localhost:3000/books/1
### Get a non-existent book (should return 404)
GET http://localhost:3000/books/99
### Create a new book
POST http://localhost:3000/books
Content-Type: application/json
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Science Fiction",
"year": 1965
}
### Create with missing fields (should return 400)
POST http://localhost:3000/books
Content-Type: application/json
{
"genre": "Mystery"
}
### Update a book
PUT http://localhost:3000/books/1
Content-Type: application/json
{
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian Fiction",
"year": 1949
}
### Delete a book
DELETE http://localhost:3000/books/1
### Verify deleted book returns 404
GET http://localhost:3000/books/1
Extract routes into a separate module
Right now app.js handles everything: server configuration, data storage, and route logic. As your API grows, this single file becomes harder to read and maintain.
Express Router lets you group related routes into their own file. Each router acts as a mini-application that handles requests for a specific path.
- Create a routes folder inside your book-api directory by running:
mkdir routes
- Create a new file called books.js inside the routes folder.
- Paste the following code into routes/books.js:
const express = require('express');
const router = express.Router();
// In-memory book data
let books = [
{ id: 1, title: '1984', author: 'George Orwell', genre: 'Dystopian', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Fiction', year: 1925 }
];
let nextId = 4;
// GET all books
router.get('/', (req, res) => {
res.json(books);
});
// GET a single book by ID
router.get('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
// CREATE a new book
router.post('/', (req, res) => {
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
const newBook = {
id: nextId++,
title,
author,
genre: genre || 'Unknown',
year: year || null
};
books.push(newBook);
res.status(201).json(newBook);
});
// UPDATE an existing book
router.put('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
book.title = title;
book.author = author;
book.genre = genre || book.genre;
book.year = year || book.year;
res.json(book);
});
// DELETE a book
router.delete('/:id', (req, res) => {
const index = books.findIndex(b => b.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Book not found' });
}
books.splice(index, 1);
res.status(204).send();
});
module.exports = router;
What changed from app.js?
- express.Router() creates a modular, mountable set of route handlers. It works like a mini-application that only handles routing.
- Every app.get/app.post/etc. became router.get/router.post/etc. The route logic is identical.
- Paths changed from /books and /books/:id to just / and /:id. The /books prefix will be added when we mount the router in app.js.
- module.exports = router makes this file importable by other files.
- Save routes/books.js.
You will not see a change yet. The next substep connects this router to your main app file.
✔️ Awesome, I've got everything!
Great. Make sure routes/books.js is saved before continuing.
ⓧ I'd like to double check the full code
Here is the full routes/books.js file:
const express = require('express');
const router = express.Router();
let books = [
{ id: 1, title: '1984', author: 'George Orwell', genre: 'Dystopian', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Fiction', year: 1925 }
];
let nextId = 4;
router.get('/', (req, res) => {
res.json(books);
});
router.get('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
router.post('/', (req, res) => {
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
const newBook = {
id: nextId++,
title,
author,
genre: genre || 'Unknown',
year: year || null
};
books.push(newBook);
res.status(201).json(newBook);
});
router.put('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
const { title, author, genre, year } = req.body;
if (!title || !author) {
return res.status(400).json({ error: 'Title and author are required' });
}
book.title = title;
book.author = author;
book.genre = genre || book.genre;
book.year = year || book.year;
res.json(book);
});
router.delete('/:id', (req, res) => {
const index = books.findIndex(b => b.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Book not found' });
}
books.splice(index, 1);
res.status(204).send();
});
module.exports = router;
Update app.js to use the router
Now that the routes live in their own file, app.js only needs to import the router and mount it at the /books path. This makes the entry point clean and focused.
- Replace the entire contents of app.js with the following:
const express = require('express');
const app = express();
const PORT = 3000;
// Import the books router module
const booksRouter = require('./routes/books');
// Parse JSON request bodies
app.use(express.json());
// Mount all book routes at /books
app.use('/books', booksRouter);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does app.use('/books', booksRouter) do?
This line mounts the entire router at the /books path. Every route defined in the router (/, /:id) gets prefixed with /books automatically.
So router.get('/') becomes accessible at GET /books, and router.get('/:id') becomes GET /books/:id. The URLs your clients use stay exactly the same.
- Save app.js.
- Stop your server (Ctrl+C) and restart it by running:
node app.js
- In requests.http, click Send Request on each of your requests from top to bottom. Every request should return the same results as before.
- The DELETE request should return status 204 with no body.
- The verify GET request (GET /books/1 after the delete) should return status 404.
Seeing "Cannot find module ./routes/books"?
- Make sure you created a routes folder (not route without the 's') inside book-api.
- Confirm the file is named exactly books.js inside that routes folder.
- Check that the require path in app.js starts with ./ (a relative path). Without it, Node looks for a package instead of a local file.
Help me fix my router import error.
✔️ Awesome, I've got everything!
Great. All three files are saved and every endpoint works as expected.
ⓧ I'd like to double check the full code
Here is the full app.js after refactoring:
const express = require('express');
const app = express();
const PORT = 3000;
const booksRouter = require('./routes/books');
app.use(express.json());
app.use('/books', booksRouter);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Your API now supports all four CRUD operations and the code is cleanly organized. Next up, you will add error handling middleware to catch unexpected failures gracefully.
Add Error Handling Middleware
Your routes are cleanly organized in a separate module, and every CRUD operation works. But what happens when someone sends a request to a URL your API doesn't recognize?
Right now, Express responds with a generic HTML error page. That's confusing for API clients expecting JSON. In this step, you'll add middleware that catches unmatched routes and unexpected errors, returning consistent JSON responses with proper status codes.
In this step, get ready to:
- Add a catch-all 404 handler for undefined routes.
- Add centralized error handling middleware for unexpected server errors.
- Test error responses with REST Client.
Add a catch-all 404 handler
Express processes middleware in the order you define it. If a request doesn't match any of your routes, it falls through to whatever middleware comes next. By placing a 404 handler after all your route definitions, you catch any request that didn't match.
- In app.js, find this line:
app.listen(PORT, () => {
- Add the following 404 handler directly above that line:
// Catch any request that didn't match a defined route
app.use((req, res) => {
res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
});
What does this code do?
This middleware has no path argument, so it matches every request that reaches it. Since it sits below your /books router mount, only unmatched requests arrive here.
The response includes req.method and req.url so the client knows exactly which request failed. This is far more helpful than a generic "Not Found" page.
You won't test this change yet. The next chunk adds the error handler, and then you'll restart and verify both together.
Add centralized error handling middleware
Express has a special type of middleware for handling errors. It looks like regular middleware but has four parameters instead of three. Express uses that extra parameter to recognize it as an error handler.
- In app.js, add the error handler directly below the 404 handler you just added (still above app.listen):
// Centralized error handler (4 params tells Express this is for errors)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal server error'
});
});
Why does this need four parameters?
Express identifies error-handling middleware by its function signature. The four parameters (err, req, res, next) signal to Express that this middleware should only run when an error is thrown or passed to next().
The handler logs the full error stack trace to your terminal, then sends a JSON response. It uses the error's status code if one exists, or defaults to 500 (Internal Server Error).
- Save app.js.
- Restart your server by pressing Ctrl+C in the terminal, then running:
node app.js
- Open your browser and visit http://localhost:3000/unknown. You should see a JSON response like {"error":"Route GET /unknown not found"}.
Still seeing an HTML error page?
Make sure the 404 handler is placed after app.use('/books', booksRouter) but before app.listen(). Order matters. If it's above the router mount, it will intercept all requests.
Also confirm you saved the file and restarted the server. Changes only take effect after a restart.
Help me debug my Express 404 middleware
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const app = express();
const PORT = 3000;
const booksRouter = require('./routes/books');
app.use(express.json());
app.use('/books', booksRouter);
app.use((req, res) => {
res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal server error'
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Test error handling with REST Client
Your error middleware is working in the browser, but let's add a proper test request to your .http file so you can re-run it any time.
- In requests.http, add the following request at the very bottom of the file:
### Test 404 for unknown route
GET http://localhost:3000/unknown
What does this test?
This request hits a URL that no route handles. Your catch-all 404 middleware should intercept it and return a JSON error with the method and path included in the message.
- Save requests.http.
- Click Send Request above the "Test 404 for unknown route" request. You should see a response with status 404 and a JSON body showing Route GET /unknown not found.
- Click Send Request above the "Delete a book" request (DELETE http://localhost:3000/books/1). You should see status 204 with no body.
- Click Send Request above the "Verify deleted book returns 404" request (GET http://localhost:3000/books/1). You should see status 404 with Book not found.
Getting "connection refused" errors?
Your server might not be running. Check the terminal for the Server running on http://localhost:3000 message. If it's not there, run node app.js again.
Help me fix REST Client connection issues
✔️ Awesome, I've got everything!
Great. Double check you've saved requests.http before moving on.
ⓧ I'd like to double check the full code
### Get all books
GET http://localhost:3000/books
### Get a single book
GET http://localhost:3000/books/1
### Get a non-existent book (should return 404)
GET http://localhost:3000/books/99
### Create a new book
POST http://localhost:3000/books
Content-Type: application/json
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Science Fiction",
"year": 1965
}
### Create with missing fields (should return 400)
POST http://localhost:3000/books
Content-Type: application/json
{
"genre": "Mystery"
}
### Update a book
PUT http://localhost:3000/books/1
Content-Type: application/json
{
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian Fiction",
"year": 1949
}
### Delete a book
DELETE http://localhost:3000/books/1
### Verify deleted book returns 404
GET http://localhost:3000/books/1
### Test 404 for unknown route
GET http://localhost:3000/unknown
Secret mission
Add Filtering and Search
Your API returns all books every time, but real-world APIs let users search for exactly what they need. Can you add query parameter filtering so users can find books by author or genre?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The book-api project folder on your Desktop (including node_modules, app.js, routes/books.js, and requests.http).
- The REST Client VS Code extension.
- The Node.js server process (if still running in your terminal).
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment further with your API.
- Your book-api folder stays on your Desktop.
- The REST Client extension remains available in VS Code for future projects.
- If the server is still running, you can keep sending requests to it.
Pause - I'll come back to this later
Stop the running server to free up your terminal, but keep all your files for later.
- Switch to the terminal where your server is running.
- Press Ctrl+C to stop the server.
- Your files stay on your Desktop. When you want to resume, open the book-api folder in VS Code and run node app.js again.
Delete - I don't want to use this again
Remove all project files and the extension to start completely fresh.
- If the server is still running, press Ctrl+C in the terminal to stop it.
- Delete the project folder by running this command on macOS/Linux:
rm -rf ~/Desktop/book-api
On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\book-api
- To remove the REST Client extension, open the Extensions panel in VS Code (Ctrl+Shift+X), find REST Client in your installed list, right-click it, and select Uninstall.
Nice Work!
Nice Work!
You just built a complete REST API from scratch using Node.js and Express. Your API handles full CRUD operations on a book collection, responds with proper HTTP status codes, and is organized with clean routing and centralized error handling.
You've learned how to:
- Build a REST API with Express that maps HTTP methods (GET, POST, PUT, DELETE) to CRUD operations, using route parameters, request validation, and proper status codes.
- Organize an Express project with Express Router for modular route files and error handling middleware using the 4-parameter pattern for centralized error responses.
- Test API endpoints directly in VS Code using the REST Client extension with .http files that document and verify every route.
- Secret Mission: Added query parameter filtering with case-insensitive search to find books by author or genre.
Ready to quiz yourself?