Build a REST API with Node.js and Express
Build a REST API server that handles CRUD operations for a book collection.
Introduction
30 Second Summary
Every time you check the weather on your phone, scroll social media, or search for a restaurant, your app is talking to a server behind the scenes. That conversation happens through something called a REST API.
In this project, you will build your own REST API server that manages a collection of books. You will create, read, update, and delete books using HTTP requests and test every endpoint from your terminal.
What You'll Build
A fully functional REST API server built with Node.js and Express that handles CRUD operations for a book collection.
By the end of this project, you'll have:
- A Node.js project with Express installed and a working server running locally.
- Five API endpoints that create, read, update, and delete books using proper HTTP methods and status codes.
- Hands-on experience testing APIs from the command line using curl commands.
- Secret Mission: Add search and filtering so users can find books by author or title.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need. You do not need prior experience with Node.js or building APIs.
Create Your First Route and Start the Server
Your project folder is ready with Express installed as a dependency.
Now it's time to write your first server code. You'll set up Express, register middleware to parse incoming JSON data, define a route that responds to requests, and start the server so it listens for traffic on your machine.
In this step, get ready to:
- Set up Express and register JSON-parsing middleware.
- Define a GET route that returns a welcome message.
- Start the server and test it with curl.
Import Express and define a welcome route
Every Express app starts by importing the library, creating an application instance, and registering middleware. Middleware are functions that process every incoming request before it reaches your routes.
- Switch back to index.js in your editor. Add the following setup code:
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
What does this code do?
- require('express') loads the Express library you installed in Step 1.
- express() creates your application instance. All routes and middleware attach to this app object.
- app.use(express.json()) tells Express to automatically parse any incoming request body that contains JSON. Without this, your server would not understand POST or PUT data.
You won't see a change yet. The next chunk adds the route that will respond to requests.
Now define your first route. A route maps an HTTP method and URL path to a handler function.
- Below the app.use(express.json()) line in index.js, add this route:
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
What does this route do?
- app.get('/', ...) registers a handler for GET requests to the root path /.
- req is the incoming request object. res is the response object you use to send data back.
- res.json(...) sends a JSON response with the correct Content-Type header set automatically.
You won't see a change yet. The next chunk starts the server so it can receive requests.
Start the server
Your app has a route, but it is not listening for traffic yet. You need to tell Express which port to bind to.
- At the bottom of index.js, add the listen call:
// Start the server
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
What does this code do?
- app.listen(port, callback) starts the HTTP server on port 3000.
- The callback function runs once the server is ready. It logs a confirmation message so you know everything started successfully.
- Save index.js.
- You'll run and test the server in the next substep.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
// Start the server
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Run and test your server
With the code saved, you can start the server and confirm it responds to requests.
- Start your server by running this command in your terminal:
node index.js
- You should see this message printed in your terminal:
Book API server running at http://localhost:3000
Don't see the confirmation message?
- Make sure you saved index.js before running the command.
- Check that your terminal is inside the project folder (the one containing package.json).
- If you see Error: listen EADDRINUSE: address already in use :::3000, another process is using port 3000. Stop it by running npx kill-port 3000 and try again.
help me fix my server startup error
Now test the welcome route. Open a second terminal window (keep the server running in the first one).
- Send a GET request to your server by running this command:
curl http://localhost:3000
- You should see this JSON response:
{"message":"Welcome to the Book API"}
Not seeing the JSON response?
- Make sure the server is still running in your first terminal. If you see the cursor blinking without a prompt, the server is active.
- Confirm you opened a separate terminal window or tab for the curl command. You cannot type in the same terminal that is running the server.
- Check the URL is exactly http://localhost:3000 with no trailing path.
help me fix my curl request
What is curl?
curl is a command-line tool for making HTTP requests. It comes pre-installed on macOS and most Linux distributions. On Windows, it is available in PowerShell and modern Command Prompt. Throughout this project you'll use it to test every endpoint without needing a browser or separate tool.
Your server is live and responding. Next up, you'll build the full set of CRUD endpoints to manage a collection of books.
Build the Full CRUD API
Your server is alive and responding with a welcome message. That's a great start, but a single route doesn't do much.
In this step, you'll build the real functionality: a full set of CRUD routes (Create, Read, Update, Delete) that manage a collection of books stored in memory.
In this step, get ready to:
- Define an in-memory data store with sample books.
- Add GET routes for reading books (all or by ID).
- Add POST, PUT, and DELETE routes for creating, updating, and removing books.
Add the data store and GET routes
A real API needs data to serve. You'll start by creating an in-memory array of books that your routes will read from and write to.
- If your server is still running from the previous step, press Ctrl+C in your terminal to stop it.
- In index.js, find the welcome route:
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
- Add the following data store code above the welcome route (between app.use(express.json()) and the welcome route):
// In-memory data store
let books = [
{ id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien' },
{ id: 2, title: '1984', author: 'George Orwell' }
];
let nextId = 3;
What does this code do?
The books array holds your collection of book objects. Each book has an id, title, and author.
The nextId variable tracks the next available ID. It starts at 3 because the sample books already use 1 and 2.
You won't see a change yet. The GET routes you'll add next will serve this data.
- Now add a GET route that returns all books. Place this code below the welcome route:
// GET all books
app.get('/books', (req, res) => {
res.json(books);
});
What does this code do?
When someone sends a GET request to /books, Express responds with the entire books array as JSON. This is the "Read all" part of CRUD.
You won't see a change yet. Add one more route before testing.
- Add a GET route that returns a single book by its ID. Place this directly below the GET /books route:
// 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?
The :id in the route path is a route parameter. Express captures whatever value appears in that URL segment and stores it in req.params.id.
The books.find() method searches the array for a matching ID. If no book is found, the route returns a 404 status code with an error message. This tells the client the resource doesn't exist.
- Save index.js.
- Restart your server by running:
node index.js
- Open a second terminal tab or window, then test the GET route by running:
curl http://localhost:3000/books
You should see both sample books returned as a JSON array.
Seeing an empty response or error?
- Make sure you stopped the old server process (Ctrl+C) before running node index.js again. Two servers can't share the same port.
- Check that the books array is placed above the welcome route, not inside it.
- help me debug why curl /books returns nothing
Add the POST route
Reading books is useful, but an API also needs to let clients create new entries. A POST route accepts data in the request body and adds it to your collection.
- Stop the server with Ctrl+C in the terminal where it's running.
- In index.js, add the POST route below the GET /books/:id route:
// POST a new book
app.post('/books', (req, res) => {
const { title, author } = req.body;
const newBook = { id: nextId++, title, author };
books.push(newBook);
res.status(201).json(newBook);
});
What does this code do?
This route handles POST requests to /books. It pulls title and author from the request body using destructuring.
The nextId++ expression assigns the current value of nextId to the new book's ID, then increments it for next time. The response uses status 201 (Created) to signal that a new resource was successfully made.
Why status 201 instead of 200?
HTTP status codes communicate specific meanings to clients. Status 200 means "OK, here's what you asked for." Status 201 means "OK, and I created something new for you." Using precise codes makes your API predictable for anyone building on top of it.
- Save index.js and restart your server:
node index.js
- In your second terminal, test the POST route by running:
curl -X POST -H "Content-Type: application/json" -d '{"title":"Dune","author":"Frank Herbert"}' http://localhost:3000/books
You should see a JSON response containing your new book with id: 3 assigned automatically.
Getting an empty body or undefined values?
- Make sure app.use(express.json()) is still near the top of your file, before any routes. This middleware parses incoming JSON.
- Check that your curl command includes -H "Content-Type: application/json". Without this header, Express won't parse the body.
- help me fix my POST route returning undefined values
Add the PUT and DELETE routes
The last two CRUD operations let clients modify existing books and remove them entirely. Both routes need to find a book by ID first, then act on it.
- Stop the server with Ctrl+C.
- In index.js, add the PUT route below the POST route:
// PUT (update) a book by ID
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 } = req.body;
book.title = title;
book.author = author;
res.json(book);
});
What does this code do?
The PUT route finds the book by ID (just like the GET single-book route), then overwrites its title and author properties with the new values from the request body. If no book matches, it returns a 404.
You won't see a change yet. Add the DELETE route next, then test both together.
- Add the DELETE route directly below the PUT route:
// DELETE a book by ID
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' });
}
const deleted = books.splice(index, 1);
res.json(deleted[0]);
});
What does this code do?
The DELETE route uses findIndex() instead of find() because you need the array position to remove the book. If findIndex() returns -1, no match was found.
The splice(index, 1) method removes one element at that position and returns it in an array. The response sends back the deleted book so the client can confirm what was removed.
- Save index.js and restart your server:
node index.js
- In your second terminal, test the DELETE route by running:
curl -X DELETE http://localhost:3000/books/2
You should see the deleted book ("1984") returned as JSON.
Seeing 'Book not found' when deleting?
- Every time you restart the server, the in-memory data resets. If you previously deleted book 2, restart the server and try again.
- Make sure the URL ends with a valid ID (1 or 2 for the sample books).
- help me debug my DELETE route returning 404
✔️ Awesome, I've got everything!
Great. Double check you've saved index.js before moving on.
ⓧ I'd like to double check the full code
Here's what your complete index.js should look like 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' },
{ id: 2, title: '1984', author: 'George Orwell' }
];
let nextId = 3;
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Book API' });
});
// GET all books
app.get('/books', (req, res) => {
res.json(books);
});
// 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 a new book
app.post('/books', (req, res) => {
const { title, author } = req.body;
const newBook = { id: nextId++, title, author };
books.push(newBook);
res.status(201).json(newBook);
});
// PUT (update) a book by ID
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 } = req.body;
book.title = title;
book.author = author;
res.json(book);
});
// DELETE a book by ID
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' });
}
const deleted = books.splice(index, 1);
res.json(deleted[0]);
});
// Start the server
app.listen(port, () => {
console.log(`Book API server running at http://localhost:${port}`);
});
Your API now supports all four CRUD operations. Next up, you'll systematically test every endpoint to make sure they all behave correctly.
Test Every Endpoint
Your API has five endpoints ready to go, but you haven't proven they actually work yet. Sending real HTTP requests is the only way to know your server behaves correctly under real conditions.
In this step, you'll use curl to send requests with every HTTP method your API supports. You'll confirm that each endpoint returns the right status code and the right JSON body.
In this step, get ready to:
- Test all GET routes including error handling for missing books.
- Create a new book with a POST request and verify it gets an ID.
- Update and delete books with PUT and DELETE requests.
Start the server and test the GET routes
Before you can send requests, the server needs to be running. You'll need two terminal windows: one to keep the server running, and one to send curl commands.
- Return to your terminal and start the server by running:
node index.js
- You should see this confirmation message:
Book API server running at http://localhost:3000
- Open a second terminal window by pressing Cmd+Shift+T (macOS) or Ctrl+Shift+T (Windows) in VS Code.
- Fetch all books by running this command in the new terminal:
curl http://localhost:3000/books
What should I see?
You should see a JSON array with the two sample books:
[{"id":1,"title":"The Hobbit","author":"J.R.R. Tolkien"},{"id":2,"title":"1984","author":"George Orwell"}]
- Fetch a single book by its ID by running:
curl http://localhost:3000/books/1
You should see just the first book returned as a single JSON object.
- Test a non-existent ID to confirm error handling works:
curl http://localhost:3000/books/999
What should I see?
Your API returns a 404 status with a JSON error message:
{"error":"Book not found"}
This proves your ID lookup and error handling logic works correctly.
Not seeing any response?
Make sure the server is still running in your first terminal. If you see Connection refused, the server has stopped. Run node index.js again in the first terminal.
On Windows, if curl is not recognized, open Git Bash instead of the default PowerShell terminal. Git Bash includes curl by default.
Help me debug why my curl command isn't getting a response
Create a new book with POST
GET requests only read data. A POST request sends data in the request body to create something new on the server.
- Create a new book by running this command:
curl -X POST -H "Content-Type: application/json" -d '{"title":"Dune","author":"Frank Herbert"}' http://localhost:3000/books
What does this command do?
- -X POST tells curl to use the POST method instead of the default GET.
- -H "Content-Type: application/json" sets a header telling the server the body contains JSON data.
- -d '{...}' is the request body containing the new book's title and author.
What should I see?
Your server responds with the newly created book, including the auto-assigned ID:
{"id":3,"title":"Dune","author":"Frank Herbert"}
The 201 status code means "Created" and confirms a new resource was successfully added.
- Confirm the new book appears in the full list by running:
curl http://localhost:3000/books
You should now see three books in the array, including the Dune entry you just created.
Seeing a syntax error on Windows?
Windows Command Prompt and PowerShell handle quotes differently than macOS or Linux terminals. If you see an error, try replacing the single quotes with escaped double quotes:
curl -X POST -H "Content-Type: application/json" -d "{\"title\":\"Dune\",\"author\":\"Frank Herbert\"}" http://localhost:3000/books
Alternatively, use Git Bash which supports the single-quote syntax directly.
Help me fix my curl POST command on Windows
Update and delete books
PUT replaces a resource's data, and DELETE removes it entirely. These two methods complete the four CRUD operations your API supports.
- Update book 1's title and author by running:
curl -X PUT -H "Content-Type: application/json" -d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien"}' http://localhost:3000/books/1
What should I see?
The server responds with the updated book object:
{"id":1,"title":"The Lord of the Rings","author":"J.R.R. Tolkien"}
The ID stays the same because you updated the existing book, not created a new one.
- Delete book 1 by running:
curl -X DELETE http://localhost:3000/books/1
The server responds with the deleted book object, confirming what was removed.
- Verify the deletion worked by fetching all books again:
curl http://localhost:3000/books
You should now see only two books: 1984 and Dune. The Hobbit (book 1) is gone.
- Test deleting a non-existent book to confirm error handling:
curl -X DELETE http://localhost:3000/books/999
You should see the same 404 error: {"error":"Book not found"}.
Getting unexpected results?
If your data looks wrong, remember the server stores books in memory. Every time you restart the server with node index.js, the data resets to the original two sample books.
If you accidentally deleted books you wanted to keep, stop the server with Ctrl+C and start it again to reset.
Help me understand why my API is returning unexpected data
Why does the data reset on restart?
Your books are stored in a JavaScript array that lives in your server's memory. When the node process stops, that memory is released and the array disappears.
For a production API, you'd connect a database to persist data across restarts. For learning REST patterns, in-memory storage keeps the focus on routing and HTTP methods.
Secret mission
Add Search and Filtering
Your API returns every book every time. What if a user only wants books by a specific author? Add query parameter support to let users search and filter the collection without changing any other endpoints.
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.
- The Node.js server process running on port 3000.
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend your API.
- Your server keeps running as long as the terminal stays open.
- Your project files stay in the book-api folder on your Desktop.
- Node.js remains installed on your machine for future projects.
Pause - I'll come back to this later
Stop the running server to free up the terminal. Your project files stay on disk so you can pick up where you left off.
- In the terminal where your server is running, press Ctrl+C to stop the server.
- When you're ready to resume, open a terminal in your book-api folder and run node index.js again.
Delete - I don't want to use this again
Remove all project files and stop the server. This cannot be undone.
- In the terminal where your server is running, press Ctrl+C to stop the server.
- Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/book-api
On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\book-api
- Confirm the folder is gone by checking your Desktop.
Nice Work!
Nice Work!
Nice work! You just built a fully functional REST API server from scratch using Node.js and Express. Your API can create, read, update, and delete books, all tested and verified from the command line.
You've learned how to:
- Built a REST API server from scratch using Node.js and Express, complete with JSON middleware for parsing request bodies.
- Implemented all four CRUD operations (Create, Read, Update, Delete) with proper HTTP methods and appropriate status codes (200, 201, 404).
- Tested every endpoint using curl from the command line, verifying correct JSON responses for each route.
- Secret Mission: Added query parameter filtering so users can search books by title or author using case-insensitive partial matching.
Ready to quiz yourself?