Build a REST API with Node.js
Build a Book Collection API with full CRUD operations using Node.js and Express.
Introduction
30 Second Summary
Every time you check a weather forecast, scroll through social media, or search for a product online, your device is talking to a server through something called a REST API. These invisible conversations power almost every app you use.
In this project, you will build your own REST API from scratch using Node.js and Express. Your API will manage a book collection, handling requests to list, add, update, and delete books.
What You'll Build
A Book Collection API server that stores books in memory and responds to HTTP requests with structured JSON data.
By the end of this project, you'll have:
- A Node.js project with Express installed and a running API server listening on port 3000.
- Four working API endpoints that handle CRUD operations (Create, Read, Update, Delete) on a book collection.
- Tested all endpoints using curl commands in your terminal to confirm everything works.
- Secret Mission: Add search and filtering with query parameters so users can find specific books.
Are there any prerequisites?
Basic JavaScript knowledge is helpful for this project. You should be comfortable with variables, functions, arrays, and objects.
Set Up Your Project
Every web and mobile app you use relies on a server to send and receive data. In this step, you will create the foundation for your own server: a Node.js project powered by Express.
By the end of this step, you will have a running server that responds to browser requests with JSON data. This is the same pattern every REST API uses to deliver information to apps.
In this step, get ready to:
- Create the book-api project folder and initialize it with npm.
- Install Express as a project dependency.
- Create server.js and confirm the server responds in your browser.
Create the project folder and initialize it
Before writing any code, you need a dedicated folder for your project and a package.json file that tracks your project's metadata and dependencies.
- Open the integrated terminal in Visual Studio Code by pressing Ctrl+` (macOS and Windows).
- 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
- Confirm the folder exists by running:
ls
You should see the book-api folder listed in the output.
- Initialize your project by running:
npm init -y
You should see output showing the contents of a new package.json file.
What does npm init -y do?
The npm init command creates a package.json file, which is the identity card for your Node.js project. It stores your project's name, version, and the list of packages it depends on.
The -y flag accepts all the default values so you do not have to answer interactive prompts.
- Install Express by running:
npm install express@5.2.1
You should see output mentioning added packages. A node_modules folder and a package-lock.json file now appear in your project.
What is Express?
Express is a lightweight web framework for Node.js. It gives you simple tools to define routes (URLs your server responds to) and send back data.
The @5.2.1 part pins the exact version so your project behaves the same way regardless of when you install it.
Seeing permission errors or network failures?
If you see EACCES permission errors, try prefixing the command with sudo on macOS/Linux. On Windows, run your terminal as Administrator.
If you see network errors, check your internet connection and try again.
Help me troubleshoot npm install errors in my book-api project.
Create the entry point file
With Express installed, you can write the code that starts your server and defines what happens when someone visits it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the book-api folder on your Desktop, then click Open.
Why open the folder in VS Code?
Opening the project folder gives VS Code full context about your files. You get an Explorer sidebar showing all files, and any new terminal you open starts inside book-api automatically.
- In the Explorer sidebar on the left, hover over the BOOK-API heading.
- Click the New File icon (a page with a plus sign).
- Type server.js and press Enter.
The file opens in the editor. This is where your server code lives.
- Copy the following code into server.js:
// Import the Express framework
const express = require('express');
const app = express();
const port = 3000;
// Define a welcome route at the root URL
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
// Start the server and listen for requests
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
What does this code do?
- require('express') loads the Express library you just installed.
- express() creates your application instance, stored in app.
- app.get('/', ...) tells the server: when someone visits the root URL, respond with a JSON object containing a welcome message.
- app.listen(port, ...) starts the server on port 3000 and prints a confirmation message to your terminal.
- Save server.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open the integrated terminal by pressing Ctrl+`.
- Start your server by running:
node server.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 a JSON response:
{"message":"Welcome to the Book API"}
Not seeing the welcome message?
If your terminal shows an error mentioning Cannot find module, make sure your terminal is inside the book-api folder. Run pwd to check your location.
If the browser shows "This site can't be reached", confirm the terminal still shows the running message and you have not closed it.
Help me debug why my server is not responding at localhost:3000.
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js and your server is still running in the terminal.
ⓧ I'd like to double check the full code
Here is the complete server.js for this step:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
And here is what your package.json should look like (minor differences in description or scripts from npm init -y defaults are fine as long as express appears in dependencies):
{
"name": "book-api",
"version": "1.0.0",
"description": "A simple REST API for managing a book collection",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "5.2.1"
}
}
Your server is alive and responding to requests. Next up, you will fill it with book data and build your first real API endpoint.
Build Your First GET Route
Your server is running and responding to requests. Now it needs actual data to serve.
A REST API is only useful when it can return real information to whoever asks for it. In this step, you will create a collection of books stored in memory and build two routes that let anyone retrieve that data over HTTP.
In this step, get ready to:
- Create an in-memory data store of books.
- Build GET routes to retrieve all books or a single book by ID.
- Test your routes in the browser.
Create the in-memory data store
Before you can build routes that return books, you need books to return. You will store them in a simple JavaScript array that lives in your server's memory.
- In server.js, find the existing welcome route:
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
- Delete those three lines and replace them with the following code:
app.use(express.json());
let books = [
{ id: 1, title: '1984', author: 'George Orwell', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }
];
let nextId = 4;
What does this code do?
app.use(express.json()) is middleware that tells Express to automatically parse incoming JSON request bodies. You will need this for later steps when you send data to the server.
The books array holds three sample book objects, each with an id, title, author, and year property. The nextId counter tracks which ID to assign to the next book that gets created.
- Save server.js.
Seeing a syntax error?
Make sure you deleted the entire original app.get('/', ...) block including its closing });. A leftover brace or parenthesis will cause a parse error.
Help me find my syntax error
Create the GET /books endpoints
With data in place, you can now create routes that respond to GET requests. A GET request is how browsers and other clients ask a server for information.
- In server.js, add the following code directly below the let nextId = 4; line:
// GET /books - List all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book
app.get('/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
What does this code do?
The first route handles GET /books. It returns the entire books array as a JSON response. Any client that visits /books will receive the full list.
The second route handles GET /books/:id. The :id portion is a route parameter. Express extracts whatever value the client puts there and makes it available as req.params.id.
If no book matches the requested ID, the route returns a 404 status code with an error message. This tells the client that the resource they asked for does not exist.
- Save server.js.
Not sure where to place the routes?
Your routes must go after the books array and before the app.listen(...) call at the bottom of the file. Express processes routes in the order they appear, and app.listen must always be last.
Help me figure out where to place my routes
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js before moving on to testing.
ⓧ I'd like to double check the full code
Here is the complete server.js file 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', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }
];
let nextId = 4;
// GET /books - List all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book
app.get('/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Test your GET routes
Your routes are defined, but the running server is still using the old code. You need to restart it so it picks up your changes.
- Switch back to your terminal where the server is running.
- Press Ctrl+C to stop the server.
- Restart the server by running:
node server.js
- Open your browser and visit http://localhost:3000/books.
You should see a JSON array containing three book objects with their IDs, titles, authors, and years.
- Now visit http://localhost:3000/books/1 to get a single book.
You should see only the first book object (1984 by George Orwell).
- Visit http://localhost:3000/books/999 to test the 404 response.
You should see {"error":"Book not found"} in your browser. This confirms your error handling works.
Seeing "Cannot GET /books"?
This means the server is running the old version of your file. Make sure you stopped the server with Ctrl+C and restarted it with node server.js after saving your changes.
Help me debug my routes
Your API can now serve data to anyone who asks for it. Next up, you will build the POST route so clients can add new books to the collection.
Add a POST Route to Create Books
Your API can serve data to anyone who asks for it. Visiting /books or /books/1 returns a JSON response instantly. But a read-only API is only half the story.
In this step, you'll add the ability to create new books by handling POST requests. This is how real apps accept user input and store new data on the server.
In this step, get ready to:
- Understand how JSON body parsing works for incoming requests.
- Create a POST endpoint that adds books to the collection.
- Test the new route with curl.
Understand the JSON parsing middleware
For your server to read data sent in a POST request, it needs middleware that translates raw JSON text into a JavaScript object.
- In server.js, find this line near the top of your file:
app.use(express.json());
What does express.json() do?
When a client sends a POST request with a JSON body, the raw data arrives as a stream of text. express.json() intercepts that text, parses it into a JavaScript object, and attaches it to req.body.
Without this middleware, req.body would be undefined and your POST route would have no data to work with.
You already added this line in the previous step, so your server is ready to receive JSON data.
Create the POST /books endpoint
A POST request tells the server to create a new resource. Your route will read the book details from the request body, assign a unique ID, and add it to the collection.
- In server.js, find the app.listen block at the bottom. Add the following code above it:
// POST /books - Create 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?
- const { title, author, year } = req.body pulls the three fields out of the incoming JSON body using destructuring.
- id: nextId++ assigns the current value of nextId (starting at 4) as the new book's ID, then increments it for next time.
- books.push(newBook) adds the new book object to the in-memory array.
- res.status(201).json(newBook) sends back the created book with a 201 status code, which means "Created".
- Save server.js.
- Switch to the terminal where your server is running. Press Ctrl+C to stop it.
- Restart the server by running:
node server.js
You should see the same startup message confirming the server is running on port 3000.
Seeing a syntax error on startup?
- Check that your new app.post block is placed before app.listen and after the app.get('/books/:id', ...) route.
- Make sure every opening brace { and parenthesis ( has a matching closing one.
- Confirm you have a comma after year inside the newBook object is NOT needed (it's the last property).
Help me fix a syntax error in my Express POST route
Test the POST route with curl
Your server is running, but you cannot test a POST request from a browser address bar. Browsers only send GET requests when you type a URL. You'll use curl, a command-line tool for making HTTP requests of any type.
- Open a new terminal window. Keep your server running in the original terminal.
Why a new terminal?
Your server occupies the first terminal. It needs to stay running to receive requests. The second terminal is where you'll send requests to it.
- Send a POST request to create a new book by running this command:
macOS
curl -X POST http://localhost:3000/books -H "Content-Type: application/json" -d '{"title": "The Hobbit", "author": "J.R.R. Tolkien", "year": 1937}'
Windows
curl -X POST http://localhost:3000/books -H "Content-Type: application/json" -d "{\"title\": \"The Hobbit\", \"author\": \"J.R.R. Tolkien\", \"year\": 1937}"
What does this curl command do?
- -X POST tells curl to use the POST HTTP method instead of the default GET.
- -H "Content-Type: application/json" sets a header telling the server that the body contains JSON data.
- -d '{...}' provides the request body containing your new book's details.
You should see a response like this in your terminal:
{"id":4,"title":"The Hobbit","author":"J.R.R. Tolkien","year":1937}
The id of 4 confirms your nextId counter is working. The 201 status code means the book was successfully created.
- Confirm the new book appears in the full collection by running:
curl http://localhost:3000/books
You should see an array of 4 books, with "The Hobbit" as the last entry.
Getting an empty response or error?
- Make sure your server is still running in the other terminal. If you closed it, restart with node server.js.
- On Windows, check that you used the escaped double-quote syntax shown in the Windows tab above.
- If you see Cannot POST /books, your server may be running old code. Stop it with Ctrl+C and restart.
Help me troubleshoot my curl POST request to my Express API
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and your server is running with the POST route active.
ⓧ 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', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }
];
let nextId = 4;
// GET /books - List all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book
app.get('/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
// POST /books - Create 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);
});
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 add PUT and DELETE routes to update and remove books from the collection.
Add PUT and DELETE Routes
In the last step, you built a POST route that lets users add new books to the collection. Now you'll complete the full set of CRUD operations by adding routes to update and delete books.
Without these routes, your API is one-directional. Users can add books but can never fix a typo in a title or remove a book they no longer want.
In this step, get ready to:
- Create a PUT route that updates an existing book's details.
- Create a DELETE route that removes a book from the collection.
- Test both routes with curl to confirm they work correctly.
Create the PUT /books/:id endpoint
A PUT request tells the server to update an existing resource. Your PUT route will find a book by its ID, apply the changes from the request body, and return the updated book.
- In server.js, find the closing }); of your POST route (just above app.listen).
- Add the PUT route below it by copying this code:
// PUT /books/:id - Update a book
app.put('/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === 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?
This route handles PUT requests to /books/:id, where :id is the book's unique identifier.
- parseInt(req.params.id) converts the URL parameter from a string to a number so it can match your book IDs.
- books.find() searches the array for a book with the matching ID. If none exists, it returns a 404 error.
- The if (title) checks only update fields that the client actually sent. This lets users update just the title without needing to resend the author and year.
You won't see a change yet. The DELETE route comes next, and then you'll restart the server to test both.
Create the DELETE /books/:id endpoint
A DELETE request tells the server to remove a resource entirely. Unlike PUT, a successful DELETE returns no body. The standard response is a 204 No Content status code.
- Directly below the PUT route you just added, copy in the DELETE route:
// DELETE /books/:id - Delete a book
app.delete('/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = books.findIndex(b => b.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Book not found' });
}
books.splice(index, 1);
res.status(204).send();
});
What does this code do?
- books.findIndex() returns the position of the book in the array, or -1 if it doesn't exist. You need the position (not the book itself) because splice() removes items by index.
- books.splice(index, 1) removes exactly one item at the given position.
- res.status(204).send() sends back an empty response. A 204 status means the deletion succeeded but there is nothing to return.
- Save server.js.
- Switch to the terminal where your server is running.
- Press Ctrl+C to stop the server.
- Restart the server by running:
node server.js
You should see Book API server running at http://localhost:3000 in your terminal.
Seeing a syntax error on startup?
- Check that each route ends with a closing brace and parenthesis followed by a semicolon. A missing or extra character between routes is the most common cause.
- Make sure the PUT route sits after the POST route's closing }); and the DELETE route sits after the PUT route's closing });.
- Use the full code tab below to compare your file against the working version.
Help me fix a syntax error in my Express server.js file.
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and your server is running before moving on to test your routes.
ⓧ 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', year: 1949 },
{ id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }
];
let nextId = 4;
// GET /books - List all books
app.get('/books', (req, res) => {
res.json(books);
});
// GET /books/:id - Get a single book
app.get('/books/:id', (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.json(book);
});
// POST /books - Create 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 id = parseInt(req.params.id);
const book = books.find(b => b.id === 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 id = parseInt(req.params.id);
const index = books.findIndex(b => b.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Book not found' });
}
books.splice(index, 1);
res.status(204).send();
});
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Test PUT and DELETE with curl
Time to verify both routes work. You'll use curl to send a PUT request that renames a book, then a DELETE request that removes one.
- Open a second terminal window (keep your server running in the first one).
- Update book 1's title from "1984" to "Nineteen Eighty-Four" by running this curl command:
macOS
curl -X PUT http://localhost:3000/books/1 -H "Content-Type: application/json" -d '{"title": "Nineteen Eighty-Four"}'
Windows
curl -X PUT http://localhost:3000/books/1 -H "Content-Type: application/json" -d "{\"title\": \"Nineteen Eighty-Four\"}"
You should see the updated book returned as JSON with the new title:
{"id":1,"title":"Nineteen Eighty-Four","author":"George Orwell","year":1949}
- Now delete book 3 (The Great Gatsby) by running:
curl -X DELETE http://localhost:3000/books/3
You won't see any output. This is correct. A 204 No Content response has an empty body, which means the deletion succeeded.
- Confirm both changes took effect by fetching all books:
curl http://localhost:3000/books
You should see only two books. Book 1 now has the title "Nineteen Eighty-Four" and book 3 (The Great Gatsby) is gone.
Why did PUT only change the title?
Your PUT route only updates fields that appear in the request body. Since you only sent {"title": "Nineteen Eighty-Four"}, the author and year stayed the same. This makes your API flexible. Clients can update one field without resending everything.
Getting a 404 or no response?
- Make sure your server is still running in the other terminal. If you see no output at all or a connection refused error, restart it.
- If PUT returns Book not found, check that you're using /books/1 (a book that exists). If you restarted the server after a previous DELETE test, the data resets to the original three books.
- On Windows, make sure you're using the Windows curl syntax with escaped double quotes inside the JSON body.
Help me debug my PUT or DELETE curl request.
Secret mission
Add Search and Filtering
Your book collection API works, but real APIs let users search and filter data without scrolling through everything. Can you add query parameter support so users can find books by author or year?
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 running Node.js server process (on port 3000).
- The book-api project folder on your Desktop (contains server.js, package.json, and node_modules).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your API.
- Your server continues listening on port 3000.
- You can send requests to it at any time.
- Node.js and Visual Studio Code remain installed for future projects.
Pause - I'll come back to this later
Stop the running server to free up port 3000. Your project files stay on disk so you can restart later.
- Switch to the terminal where your server is running.
- Press Ctrl+C to stop the server process.
- When you want to resume, open a terminal in the book-api folder and run node server.js again.
Delete - I don't want to use this again
Remove all project files and stop the server. This frees up disk space and port 3000.
- Switch to the terminal where your server is running and press Ctrl+C to stop it.
- Delete the project folder by running the following command.
macOS
cd ~/Desktop
rm -rf book-api
Windows
cd ~\Desktop
rmdir /s /q book-api
- Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see book-api in the listing.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional REST API from scratch using Node.js and Express. Your Book Collection API handles real CRUD operations, returns proper status codes, and responds with structured JSON.
You learned how to:
- Built a REST API server from scratch that handles all four CRUD operations (Create, Read, Update, Delete) with dedicated endpoints.
- Used appropriate HTTP methods (GET, POST, PUT, DELETE) and status codes (200, 201, 204, 404) to communicate outcomes to clients.
- Parsed JSON request bodies with Express built-in middleware and handled error cases with proper 404 responses.
- Secret Mission: Added search and filtering with query parameters so users can find books by author or year without scrolling through the entire collection.
Ready to quiz yourself?