Build a Book Collection REST API
Build a REST API with Express.js to manage a book collection using CRUD operations.
Introduction
30 Second Summary
Every time you open Spotify, scroll Instagram, or check the weather on your phone, you're talking to an API. These invisible services power the apps you use every day by sending and receiving data behind the scenes.
In this project, you will build a REST API from scratch that manages a personal book collection. You'll create a server that accepts requests, processes them, and sends back structured data, just like the APIs behind your favourite apps.
What You'll Build
A local REST API server built with Express.js that handles all four CRUD operations (Create, Read, Update, Delete) for a book collection.
By the end of this project, you'll have:
- A working Express.js server running on your local machine that responds to HTTP requests with JSON data.
- Hands-on experience with all four HTTP methods (GET, POST, PUT, DELETE) for managing data.
- A solid mental model of how real-world REST APIs work under the hood.
- Secret Mission: Add a search feature that lets you filter books by author or title using query parameters.
Are there any prerequisites?
Nope. This project includes setup instructions for everything you need. You'll just need Node.js and VS Code installed on your machine.
Before We Start
Before We Start...
Set Up Your Project
Every Express.js project starts with the same foundation: a project folder, a package manager configuration, and a few lines of code.
In this step, you'll create the project folder, install Express.js as your server framework, and write the minimum code needed to bring a working server to life.
In this step, get ready to:
- Create your project folder and initialize it with npm.
- Install Express.js.
- Write and test your first server.
Create your project folder and initialize npm
Before writing any code, you need a dedicated folder for your project and a package.json file that tracks your project's dependencies.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- 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
- Initialize your project with npm by running this command:
npm init -y
- Confirm your project was initialized by running:
- You should see package.json in the output.
ls
What is package.json?
The package.json file is your project's configuration file. It tracks metadata like your project's name and, most importantly, which packages (libraries) your project depends on.
The -y flag tells npm to skip all the setup questions and use sensible defaults. You can always edit the file later.
Not seeing package.json?
Make sure you ran npm init -y from inside the book-api folder. If your terminal is in a different directory, the file will be created there instead.
help me figure out why npm init didn't create package.json in my book-api folder
Install Express.js
Express.js is the framework that handles routing, requests, and responses for your API. You need to install it as a project dependency.
- Install Express.js by running this command:
npm install express
- Confirm the installation was successful by running:
- You should see node_modules and package-lock.json in the output alongside your existing package.json.
ls
What did npm install just do?
The node_modules folder contains all the downloaded code for Express and its own dependencies. The package-lock.json file locks the exact versions installed so your project stays reproducible.
If you open package.json, you'll now see express listed under dependencies. This tells npm that your project needs Express to run.
Seeing a network error?
The npm install command downloads packages from the internet. Check that you have an active internet connection and try the command again.
If you see a permissions error, try running the command with administrator privileges or check that your node_modules folder isn't locked by another process.
help me fix an npm install error in my book-api project
Create your server file and test it
Now you'll write the code that starts your server. This is the entry point for your entire API.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the book-api folder, and click Open.
- In the Explorer sidebar on the left, click the new file icon (a page with a plus sign).
- Type app.js as the file name and press Enter.
- Add the following code to app.js:
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
What does this code do?
- const express = require('express') loads the Express library into your file.
- const app = express() creates a new Express application instance. This is the object you'll attach all your routes to.
- const port = 3000 sets which port your server will listen on. Port 3000 is a common choice for local development.
- app.listen(port, ...) starts the server and prints a confirmation message to your terminal.
- Save app.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Switch back to your terminal window.
- Start your server by running this command:
node app.js
- You should see this message in your terminal:
Book API server running at http://localhost:3000
- Open your browser and visit http://localhost:3000.
- You should see Cannot GET / displayed in the browser. This confirms your server is running and receiving requests.
Why does it say Cannot GET /?
Your server is working perfectly. The Cannot GET / message is Express telling you that no route has been defined for the / path yet. You'll add routes in the next step.
Server not starting?
If you see a syntax error, double check that your code in app.js matches the code block above exactly. Pay close attention to backticks around the template string in console.log.
If you see an error about port 3000 being in use, another application is already using that port. Close other running servers or restart your terminal and try again.
help me fix my Express server that won't start on port 3000
✔️ Awesome, I've got everything!
Great. Make sure you've saved app.js and that your server is running in the terminal.
ⓧ I'd like to double check the full code
Here's the complete app.js file you should have at this point:
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Your server is alive and listening for requests. Next up, you'll create routes that actually respond with data when someone asks for your book collection.
Create Your First Routes (GET)
Your Express server is up and running. Right now it responds to requests, but it has no real data to serve.
In this step, you'll create a collection of books stored in memory and build two routes that let anyone retrieve them. This is the "Read" part of CRUD (Create, Read, Update, Delete).
In this step, get ready to:
- Create an in-memory data store with sample books.
- Build GET routes to retrieve all books or a single book by ID.
- Test your routes using curl from the command line.
Add your book data
Real APIs pull data from databases. For this project, you'll store books directly in a JavaScript array. This keeps the focus on the API routing pattern without adding database complexity.
- In app.js, find the line that says const port = 3000;.
- Add the following code below it (and above the app.listen call at the bottom):
// In-memory data store
let books = [
{ id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
{ id: 2, title: '1984', author: 'George Orwell', year: 1949 },
{ id: 3, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
];
let nextId = 4;
What does this code do?
- let books = [...] creates an array of three book objects, each with an id, title, author, and year.
- let nextId = 4 tracks the next available ID for when new books are added later.
- Using let instead of const allows the values to be modified as books are added or removed.
You won't see a change yet. The next chunk builds routes that serve this data.
Build your GET routes
GET requests are how clients ask a server for data. You'll create two routes: one that returns all books, and one that returns a specific book by its ID.
- Below the let nextId = 4; line you just added, add this route:
// GET /books - Get all books
app.get('/books', (req, res) => {
res.json(books);
});
What does this code do?
- app.get('/books', ...) tells Express to listen for GET requests at the /books path.
- (req, res) are the request and response objects that Express passes to your handler function.
- res.json(books) converts the books array to JSON and sends it back to the client with the correct content-type header.
You won't see a change yet. Add the next route, then you'll test both together.
- Below the GET /books route you just added, add this route:
// GET /books/:id - Get a single book by ID
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?
- :id in the path is a route parameter. When someone visits /books/2, Express sets req.params.id to "2".
- parseInt(req.params.id) converts the string to a number so it can match against book IDs.
- books.find(...) searches the array for a book with a matching ID.
- If no book is found, res.status(404).json(...) sends a 404 status code with an error message. This tells the client the resource doesn't exist.
- Save app.js.
- Stop your running server by pressing Ctrl+C in PowerShell, then restart it by running:
node app.js
- You should see Book API server running at http://localhost:3000 printed in your terminal again.
Server won't start?
- Check for typos in your route code. A missing comma, parenthesis, or curly brace will crash the server with a syntax error.
- Make sure your routes are placed above the app.listen(...) call. Express reads routes in order, and app.listen must remain at the bottom.
- Help me debug my Express server startup error.
✔️ Awesome, I've got everything!
Great. Make sure you've saved app.js and your server is running.
ⓧ I'd like to double check the full code
Here's what your complete app.js should look like at this point:
const express = require('express');
const app = express();
const port = 3000;
// In-memory data store
let books = [
{ id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
{ id: 2, title: '1984', author: 'George Orwell', year: 1949 },
{ id: 3, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
];
let nextId = 4;
// GET /books - Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book by ID
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);
});
// Start the server
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Test your routes with curl
Your server is running, but you need a way to send HTTP requests to it. You'll use curl, a command-line tool that sends HTTP requests and shows you the raw response.
Why a second PowerShell window?
Your first PowerShell window is busy running the server process. You need a separate terminal to send requests to it. Think of it like calling a restaurant (the server) from your phone (the second terminal).
- Open a second PowerShell window by pressing the Windows key, typing PowerShell, and pressing Enter.
- Fetch all books by running this command:
curl http://localhost:3000/books
- You should see a JSON array containing all three books, with their id, title, author, and year fields.
- Fetch a single book by its ID by running:
curl http://localhost:3000/books/1
- You should see only The Hobbit's data returned as a single JSON object.
- Test what happens when a book doesn't exist by running:
curl http://localhost:3000/books/99
- You should see {"error":"Book not found"} returned. This confirms your 404 error handling works.
Seeing unexpected output from curl?
- Make sure your server is still running in the other PowerShell window. If you see "connection refused", the server has stopped.
- On Windows PowerShell, curl is sometimes aliased to a different command. If you see HTML-like output instead of JSON, try using curl.exe instead (e.g. curl.exe http://localhost:3000/books).
- Check that you typed the URL correctly. A missing /books path will hit the root route which has no handler.
- Help me troubleshoot my curl requests to localhost.
Your API can now serve data. Next up, you'll teach it how to accept new books from clients using POST requests.
Add Books with POST
Your GET routes can read data from the server, but a real API also needs to accept new data from clients. In this step, you'll build the ability to add books to your collection by sending data to the server.
This is where POST requests come in. A POST request sends data in its body (like a form submission), and the server processes that data and creates a new resource.
In this step, get ready to:
- Add JSON parsing middleware so Express can understand incoming request bodies.
- Build a POST route that creates new books.
- Test the route with curl to confirm books get added.
Add the JSON parsing middleware
When a client sends data to your API (like a new book's title and author), that data arrives in the request body as JSON. By default, Express doesn't know how to read JSON bodies. You need to add middleware that parses the raw data into a JavaScript object your code can use.
- In app.js, find the line const port = 3000; near the top of your file.
- Add the following code directly below it (above the // In-memory data store comment):
// Middleware to parse JSON request bodies
app.use(express.json());
What does express.json() do?
express.json() is built-in middleware that intercepts every incoming request. If the request has a JSON body, it parses it and attaches the result to req.body so your route handlers can access the data as a regular JavaScript object.
Without this line, req.body would be undefined and your POST route wouldn't be able to read any incoming data.
Build the POST route
Now you'll create a route that listens for POST requests and uses the parsed body to add a new book to your collection.
- In app.js, find your GET /books/:id route (the one that ends with res.json(book); and its closing });).
- Add the following code below it, before the // Start the server comment:
// POST /books - Add a new book
app.post('/books', (req, res) => {
const { title, author, year } = req.body;
const newBook = { id: nextId++, title, author, year };
books.push(newBook);
res.status(201).json(newBook);
});
What does this code do?
- app.post('/books', ...) registers a route that only responds to POST requests at /books.
- const { title, author, year } = req.body pulls the book details out of the parsed JSON body.
- nextId++ assigns the next available ID and then increments the counter for the next book.
- books.push(newBook) adds the new book object to your in-memory array.
- res.status(201).json(newBook) sends back the created book with a 201 status code, which means "Created" in HTTP.
- Save app.js.
- Switch to the PowerShell window where your server is running.
- Press Ctrl+C to stop the server.
- Restart it by running:
node app.js
You should see Book API server running at http://localhost:3000 printed again, confirming the server started without errors.
Seeing a syntax error when you restart?
- Check that app.use(express.json()); is placed after const app = express(); but before your routes. Middleware must be registered before the routes that depend on it.
- Make sure the POST route's closing }); is on its own line. A missing bracket or parenthesis causes unexpected token errors.
- Help me debug my Express server syntax error.
Test your POST route with curl
Time to send your first POST request and add a new book to the collection.
- Switch to your second PowerShell window (the one you used for curl testing earlier).
- Send a POST request to add a new book by running this command:
curl -X POST http://localhost:3000/books -H "Content-Type: application/json" -d "{\"title\":\"Dune\",\"author\":\"Frank Herbert\",\"year\":1965}"
What do these curl flags mean?
- -X POST tells curl to send a POST request instead of the default GET.
- -H "Content-Type: application/json" adds a header telling the server that the body contains JSON data.
- -d provides the request body. The escaped quotes are needed for PowerShell to pass the JSON correctly.
You should see a response like this, confirming your book was created with ID 4 and a 201 status:
{"id":4,"title":"Dune","author":"Frank Herbert","year":1965}
- Confirm the book was actually added to the collection by running:
curl http://localhost:3000/books
You should now see an array of 4 books, with "Dune" included at the end.
POST returns an empty or unexpected response?
- If the response body is empty or you see undefined values, check that app.use(express.json()) appears before your routes in app.js. Without it, req.body is undefined.
- If curl shows an error about the connection, make sure your server is still running in the other PowerShell window.
- If you see a JSON parse error, double-check that all quotes in the -d flag are escaped with backslashes.
- Help me debug my POST request.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js and your server is still 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;
// Middleware to parse JSON request bodies
app.use(express.json());
// In-memory data store
let books = [
{ id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
{ id: 2, title: '1984', author: 'George Orwell', year: 1949 },
{ id: 3, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
];
let nextId = 4;
// GET /books - Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book by ID
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);
});
// POST /books - Add a new book
app.post('/books', (req, res) => {
const { title, author, year } = req.body;
const newBook = { id: nextId++, title, author, year };
books.push(newBook);
res.status(201).json(newBook);
});
// Start the server
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Your API can now create new books on demand. Next up, you'll complete the full set of CRUD operations by adding the ability to update and delete books.
Update and Delete Books (PUT and DELETE)
Your API can now create new books with POST requests. Two operations remain to complete the full set of CRUD actions: updating existing books and deleting them.
PUT lets clients modify an existing resource, while DELETE removes one entirely. By the end of this step, your API will handle all four HTTP methods that power real-world web services.
In this step, get ready to:
- Build a PUT route to update a book's details.
- Build a DELETE route to remove a book from the collection.
- Test both routes with curl to confirm they work.
Build the PUT route to update a book
When a client sends a PUT request, they want to change an existing resource. Your route needs to find the correct book, apply the changes, and send back the updated version.
- In app.js in VS Code, find the POST route's closing }); and add the following code below it (above the // Start the server comment):
// PUT /books/:id - Update a book
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, year } = req.body;
if (title) book.title = title;
if (author) book.author = author;
if (year) book.year = year;
res.json(book);
});
What does this code do?
- app.put('/books/:id', ...) registers a handler for PUT requests to a specific book's URL.
- books.find() searches the array for a book whose id matches the URL parameter. If no match is found, it returns a 404 error.
- The three if statements only update a field when the client sends a new value for it. This means clients can update just the title without touching the author or year.
- You won't see a change yet. The next chunk adds the DELETE route, then you'll restart the server and test both together.
Build the DELETE route to remove a book
DELETE requests remove a resource permanently. Your route needs to locate the book and splice it out of the array.
- In app.js, add the following code directly below the PUT route you just wrote (still above // Start the server):
// DELETE /books/:id - Delete a book
app.delete('/books/: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.json({ message: 'Book deleted successfully' });
});
What does this code do?
- app.delete('/books/:id', ...) registers a handler for DELETE requests targeting a specific book.
- findIndex() returns the position of the book in the array, or -1 if it doesn't exist.
- books.splice(index, 1) removes exactly one item at that position, permanently deleting the book from memory.
- Save app.js.
Seeing a syntax error when you save?
- Make sure every route block ends with }); and that the PUT and DELETE routes are placed between the POST route and the app.listen call.
- Check that you haven't accidentally deleted the // Start the server section at the bottom of the file.
Help me find a syntax error in my Express app.js file.
✔️ Awesome, I've got everything!
Great. Make sure you've saved app.js before moving on to testing.
ⓧ I'd like to double check the full code
Here's the complete app.js file at this point:
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// In-memory data store
let books = [
{ id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
{ id: 2, title: '1984', author: 'George Orwell', year: 1949 },
{ id: 3, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
];
let nextId = 4;
// GET /books - Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book by ID
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);
});
// POST /books - Add a new book
app.post('/books', (req, res) => {
const { title, author, year } = req.body;
const newBook = { id: nextId++, title, author, year };
books.push(newBook);
res.status(201).json(newBook);
});
// PUT /books/:id - Update a book
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, year } = req.body;
if (title) book.title = title;
if (author) book.author = author;
if (year) book.year = year;
res.json(book);
});
// DELETE /books/:id - Delete a book
app.delete('/books/: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.json({ message: 'Book deleted successfully' });
});
// Start the server
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Test PUT and DELETE with curl
Your server is still running the old version of your code. You need to restart it to pick up the new routes.
- Switch to the PowerShell window where your server is running.
- Press Ctrl+C to stop the server.
- Restart the server by running:
node app.js
- You should see Book API server running at http://localhost:3000 again.
Now test the PUT route. Open a second PowerShell window (keep the server running in the first one).
- Update the title of book 1 by running this command:
curl -X PUT http://localhost:3000/books/1 -H "Content-Type: application/json" -d "{\"title\":\"Updated Title\"}"
What does this command do?
- -X PUT tells curl to send a PUT request instead of the default GET.
- -H "Content-Type: application/json" sets the header so Express knows to parse the body as JSON.
- -d sends the JSON body containing only the title field. The author and year stay unchanged because the PUT route only updates fields that are provided.
- You should see a response like: {"id":1,"title":"Updated Title","author":"J.R.R. Tolkien","year":1937}.
Now test the DELETE route.
- Delete book 2 by running:
curl -X DELETE http://localhost:3000/books/2
- You should see: {"message":"Book deleted successfully"}.
- Verify the final state of your collection by running:
curl http://localhost:3000/books
- You should see only two books remaining. Book 1 has the updated title, and book 2 is gone.
Getting an empty response or connection error?
- Make sure you restarted the server after saving your changes. Press Ctrl+C in the server window, then run node app.js again.
- Check that you're running curl in a separate PowerShell window, not the one where the server is running.
- If the PUT response shows the book unchanged, make sure you included the -H "Content-Type: application/json" header. Without it, Express won't parse the body.
Help me troubleshoot my PUT or DELETE curl commands.
Secret mission
Add Search with Query Parameters
Your API can return all books, but what if someone only wants books by a specific author? Real-world APIs let users filter results using query parameters in the URL. Your mission: add search functionality to your GET /books route.
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 Express server process running on port 3000.
- The book-api project folder on your Desktop (contains app.js, node_modules, and package.json).
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment with your API further.
- Your server keeps running at http://localhost:3000 until you close the PowerShell window or press Ctrl+C.
- Your book-api folder stays on your Desktop.
Pause - I'll come back to this later
Shut down the running server to free up the port, but keep your project files for later.
- Switch to the PowerShell window where your server is running.
- Press Ctrl+C to stop the server.
- Your book-api folder remains on your Desktop. To restart later, open PowerShell, navigate to the folder, and run node app.js.
Delete - I don't want to use this again
Remove all project files and start fresh. This permanently deletes your code.
- Switch to the PowerShell window where your server is running.
- Press Ctrl+C to stop the server.
- Navigate to your Desktop and remove the project folder by running:
cd ~/Desktop
Remove-Item -Recurse -Force book-api
- Confirm the folder is gone by running:
ls
You should no longer see book-api in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a REST API from scratch using Express.js.
You now understand the universal pattern behind every web service on the internet: clients send HTTP requests, servers process them, and structured JSON responses come back.
Here's what you learned:
- Built a working REST API server with Express.js that implements all four CRUD operations (Create, Read, Update, Delete).
- Used HTTP methods (GET, POST, PUT, DELETE) and status codes (200, 201, 404) to handle requests and communicate outcomes to the client.
- Tested your API with curl by sending raw HTTP requests from the command line, building a clear mental model of the request/response cycle.
- Added search functionality using query parameters to filter books by author or title.
Ready to quiz yourself?