Build a REST API for a Book Collection

Build a Node.js REST API with full CRUD operations using Express.

Introduction

30 Second Summary

Every time you scroll through a social media feed, search for a product online, or check the weather on your phone, you're using a REST API behind the scenes. These invisible connections are how apps send and receive data.

In this project, you will build a REST API server from scratch that manages a book collection. You'll create endpoints that accept requests and respond with JSON data, just like the APIs that power real web and mobile applications.

What You'll Build

A fully functional REST API server built with Node.js and Express that handles Create, Read, Update, and Delete operations for a book collection.

By the end of this project, you'll have:

  • A Node.js project with Express configured as your web framework.
  • Five REST API endpoints covering all CRUD operations: list all books, get a single book, add a book, update a book, and delete a book.
  • Verified and tested endpoints using Thunder Client, a visual REST client inside VS Code.
  • Secret Mission: Add search and filtering so users can find books by title or author.

Are there any prerequisites?

You'll need basic familiarity with JavaScript (variables, functions, objects, and arrays). No prior backend experience is required.

Create Your Project

Your development tools are ready. Now it's time to create the actual project that will become your REST API.

Every Node.js project starts with a folder and a package.json file that tracks its configuration and dependencies. In this step, you'll set up both, install Express as your web framework, and launch a running server.

In this step, get ready to:
  • Initialize a Node.js project with npm.
  • Install Express and configure your project for modern JavaScript.
  • Create and run your first API server.
Initialize the project folder

A Node.js project needs its own folder and a package.json file that stores metadata and dependency information. You'll create both with a few terminal commands.

  • In VS Code, click Terminal in the top menu bar.
  • Click New Terminal.
  • 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 a new Node.js project by running:
npm init -y

What does npm init -y do?

npm init creates a package.json file that tracks your project's name, version, and dependencies.

The -y flag accepts all defaults so you skip the interactive prompts.

  • Confirm the file was created by running:
ls
  • You should see package.json in the output.

Now open the project folder in VS Code so the editor knows where your files live.

  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the book-api folder, and click Open.
  • If VS Code asks whether you trust the authors of the files in this folder, click Yes, I trust the authors.
  • Open a fresh terminal by clicking Terminal in the top menu bar, then New Terminal.

Your terminal now starts inside the book-api folder automatically.

Don't see package.json in the sidebar?

Make sure you opened the book-api folder itself, not your Desktop or a parent folder. The Explorer panel on the left should show package.json at the top level.

Why can't I see my project files in VS Code?

Install Express and configure ES modules

Express is the most popular web framework for Node.js. It gives you a simple way to define routes that respond to HTTP requests.

  • Install Express by running this command in your terminal:
npm install express

What does this command do?

npm install express downloads the Express package from the npm registry and saves it as a dependency in your package.json.

It also creates a node_modules folder that holds all the downloaded code Express needs to run.

Now you need to configure your project to use modern import syntax instead of the older require() style.

  • In the Explorer sidebar, click package.json to open it.
  • Replace the entire contents of the file with this:
{
  "name": "book-api",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^5.2.1"
  }
}

What changed here?

  • "type": "module" tells Node.js to use modern ES module syntax (import/export) instead of the older CommonJS (require()).
  • "start": "node index.js" adds a convenient script so you can run your server with npm start later.
  • The dependencies section confirms Express 5.2.1 is installed.
  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Seeing a red underline or warning in package.json?

Make sure every string is wrapped in double quotes and every comma is in the right place. JSON does not allow trailing commas after the last item in an object.

Help me fix a JSON syntax error in my package.json

Create your entry file and start the server

Your server needs a main file where you define how it responds to incoming requests. This file will create an Express app and set up a single route.

  • Create a new file by clicking the New File icon at the top of the Explorer sidebar.
  • Name the file index.js and press Enter.
  • Add this code to index.js:
// Import the Express framework
import express from 'express'

// Create an Express application instance
const app = express()
const port = 3000

// Define a route for the homepage
app.get('/', (req, res) => {
  res.json({ message: 'Welcome to the Book API!' })
})

// Start the server and listen for requests
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`)
})

What does this code do?

  • import express from 'express' loads the Express framework into your file.
  • const app = express() creates your application instance. All routes and settings attach to this object.
  • 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 and prints a confirmation message to your terminal.
  • Save index.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Start your server by running this command in the terminal:
node index.js
  • You should see this message in your terminal:
Server is running on http://localhost:3000

Now test your API using Thunder Client to confirm the server responds correctly.

  • Click the Thunder Client icon in the left sidebar (it looks like a lightning bolt).
  • Click New Request.
  • Make sure the method is set to GET.
  • Enter http://localhost:3000 in the URL field.
  • Click Send.

You should see a JSON response in the Response panel:

{
  "message": "Welcome to the Book API!"
}
  • Return to the terminal and press Ctrl+C to stop the server.

Not seeing the welcome message?

If you see a connection error, make sure your terminal still shows the "Server is running" message. If the terminal shows an error instead, check that you saved index.js before running node index.js.

If you see a syntax error mentioning import, confirm that "type": "module" exists in your package.json.

Help me debug my Express server that won't start

✔️ Awesome, I've got everything!

Great work. Make sure you've saved both files and stopped the server with Ctrl+C before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.js:

import express from 'express'

const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.json({ message: 'Welcome to the Book API!' })
})

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`)
})

And the complete package.json:

{
  "name": "book-api",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^5.2.1"
  }
}

Your server is up and running. Next up, you'll add real book data and build your first API endpoints to serve it.

Build Your Data Store and GET Endpoints

In the previous step, you set up a working Express server and confirmed it responds to requests. Now it's time to give your API something meaningful to serve.

Every API needs data to work with. In this step, you'll create an in-memory data store (a JavaScript array that holds book objects) and build two GET endpoints that let clients read that data.

In this step, get ready to:
  • Create an in-memory book collection with starter data.
  • Build a GET endpoint that returns all books.
  • Build a GET endpoint that returns a single book by its ID.
Create your in-memory book data

Real applications store data in databases, but that adds complexity you don't need right now. A JavaScript array works perfectly as a temporary data store while you focus on learning REST API routing.

  • In index.js, find the line that says const port = 3000.
  • Add your books array below it by copying this code:
// In-memory data store - acts as your database
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 }
]

What does this code do?

This creates a let variable (instead of const) because you'll add and remove books from this array later.

Each book object has four properties: id (a unique number), title, author, and year. This structure mirrors what you'd get from a real database.

You won't see a change yet. The next chunk adds a route that serves this data.

Add the GET /books endpoint

Now that you have data, you need a route that sends it to any client that asks for it.

  • In index.js, find the closing }) of the existing app.get('/') route.
  • Add this new route below it:
// Return all books in the collection
app.get('/books', (req, res) => {
  res.json(books)
})

What does this code do?

When a client sends a GET request to /books, Express calls this function. res.json(books) converts the JavaScript array into JSON and sends it back as the response.

You won't see a change yet. The next chunk adds a second endpoint, then you'll test both together.

Add the GET /books/:id endpoint

Returning all books is useful, but clients often need a specific book. URL parameters let you embed a value (like a book ID) directly in the URL path.

  • Below the app.get('/books') route you just added, add this route:
// Return a single book by its ID
app.get('/books/:id', (req, res) => {
  // Find the book whose id matches the URL parameter
  const book = books.find(b => b.id === parseInt(req.params.id))
  // If no book was found, return a 404 error
  if (!book) {
    return res.status(404).json({ error: 'Book not found' })
  }
  res.json(book)
})

What does this code do?

  • :id in the route path is a URL parameter. When someone visits /books/2, Express stores 2 in req.params.id.
  • parseInt(req.params.id) converts the URL parameter from a string to a number so it can match the numeric id in your books array.
  • If no book matches, the API returns a 404 status code with an error message. This tells the client the resource doesn't exist.
  • Save index.js.
  • Start your server by running this command in the terminal:
node index.js
  • You should see Server is running on http://localhost:3000 in the terminal.
  • Open Thunder Client by clicking the thunder bolt icon in the VS Code sidebar.
  • Click **New Request**.
  • Enter http://localhost:3000/books in the URL field and click **Send**.

You should see a JSON array with all 3 books.

  • Now change the URL to http://localhost:3000/books/1 and click **Send**.

You should see only the book with id 1 ("1984" by George Orwell).

  • Try http://localhost:3000/books/999 to confirm you get a 404 error response.

Not seeing your books?

  • Make sure you saved index.js before starting the server. Node.js reads the file once at startup.
  • Check that the books array is placed between const port = 3000 and the first app.get route. Order matters.
  • Confirm the URL in Thunder Client starts with http:// (not https://).

Why aren't my GET endpoints returning data?

✔️ 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

Here's the complete index.js at this point:

import express from 'express'

const app = express()
const port = 3000

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 }
]

app.get('/', (req, res) => {
  res.json({ message: 'Welcome to the Book API!' })
})

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 is running on http://localhost:${port}`)
})

Your API can now serve data to any client that asks for it. Next up, you'll add the ability to create, update, and delete books.

Add Create, Update, and Delete Endpoints

Your API can read books, but a real book collection grows and changes over time. Users need to add new books, correct mistakes, and remove entries they no longer want.

These operations map directly to three HTTP methods: POST (create), PUT (update), and DELETE (remove). In this step, you will add all three to your API so it supports the full set of CRUD operations.

In this step, get ready to:
  • Enable JSON body parsing so your API can receive data from clients.
  • Add a POST endpoint to create new books.
  • Add PUT and DELETE endpoints to update and remove books.
Enable JSON parsing and add the POST endpoint

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. Express does not parse JSON bodies by default. You need to enable a built-in middleware that reads the incoming JSON and makes it available as req.body.

  • In index.js, find the line const port = 3000.
  • Add this line below it (before the books array):
// Parse incoming JSON request bodies
app.use(express.json())
  • Now scroll down to below your GET /books/:id route.
  • Add the POST endpoint that creates a new book by pasting this code:
app.post('/books', (req, res) => {
  // Build a new book object with an auto-incremented id
  const book = {
    id: books.length > 0 ? Math.max(...books.map(b => b.id)) + 1 : 1,
    title: req.body.title,
    author: req.body.author,
    year: req.body.year
  }
  // Add the new book to our collection
  books.push(book)
  // Respond with 201 Created and the new book
  res.status(201).json(book)
})

What does this code do?

app.use(express.json()) tells Express to automatically parse JSON in every incoming request body. Without this, req.body would be undefined and your POST route would have no data to work with.

The POST route builds a new book object. It calculates the next available id by finding the highest existing id and adding 1. It responds with status code 201 (Created) to signal that a new resource was successfully made.

  • Save index.js.

You will test this endpoint along with PUT and DELETE at the end of this step.

Add the PUT endpoint to update a book

PUT requests let a client change specific fields on an existing book. Your endpoint will look up the book by its id, update only the fields the client sends, and leave the rest unchanged.

  • In index.js, add the PUT route below the POST route you just added:
app.put('/books/:id', (req, res) => {
  // Find the book by its id
  const book = books.find(b => b.id === parseInt(req.params.id))
  if (!book) {
    return res.status(404).json({ error: 'Book not found' })
  }
  // Update only the fields provided in the request body
  book.title = req.body.title || book.title
  book.author = req.body.author || book.author
  book.year = req.body.year || book.year
  // Respond with the updated book
  res.json(book)
})

What does this code do?

The || operator provides a fallback. If the client sends a new title, the book gets that new title. If the client does not include title in the request body, the book keeps its existing value.

This means a client can update just one field (like changing only the year) without wiping out the other fields.

  • Save index.js.

One more endpoint to go.

Add the DELETE endpoint

The DELETE endpoint removes a book from the collection entirely. It responds with status code 204 (No Content), which signals success without returning any data in the body.

  • In index.js, add the DELETE route below the PUT route:
app.delete('/books/:id', (req, res) => {
  // Find the index of the book in the array
  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)
  // Respond with 204 No Content (success, nothing to return)
  res.status(204).send()
})

What does this code do?

findIndex returns the position of the matching book in the array, or -1 if no match exists. The route uses splice(index, 1) to remove exactly one element at that position.

A 204 response tells the client the deletion succeeded. Because there is nothing left to show, the response body is empty.

  • Save index.js.
  • Stop the running server by pressing Ctrl+C in the terminal.
  • Restart the server by running:
node index.js
  • In Thunder Client, send a POST request to http://localhost:3000/books.
  • Click the Body tab, select JSON, and enter this data:
{
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965
}
  • Click Send.

You should see a 201 Created response with the new book object, including id: 4.

Seeing an empty body or undefined fields?

Make sure app.use(express.json()) appears above all your routes in index.js. If it is placed below a route, that route will not have access to the parsed body.

Also check that you selected JSON (not Text or Form) in Thunder Client's Body tab. Help me figure out why my POST endpoint returns undefined fields.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.js and that your server is running.

ⓧ I'd like to double check the full code

Here is the complete index.js file at this point:

import express from '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 }
]

app.get('/', (req, res) => {
  res.json({ message: 'Welcome to the Book API!' })
})

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 book = {
    id: books.length > 0 ? Math.max(...books.map(b => b.id)) + 1 : 1,
    title: req.body.title,
    author: req.body.author,
    year: req.body.year
  }
  books.push(book)
  res.status(201).json(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' })
  }
  book.title = req.body.title || book.title
  book.author = req.body.author || book.author
  book.year = req.body.year || book.year
  res.json(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.status(204).send()
})

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`)
})

Your API now supports every CRUD operation. Next, you will run through the full create-read-update-delete cycle to verify everything works together.

Test Your Complete API

Your API has all five endpoints built and ready to go. Now it's time to prove they work together as a complete system by running a full lifecycle test.

A lifecycle test walks through the entire journey of a resource: creating it, reading it, updating it, deleting it, and confirming it's gone. This is how professional developers verify that an API behaves correctly before shipping it to production.

In this step, get ready to:
  • Test the full CRUD cycle (Create, Read, Update, Delete) in Thunder Client.
  • Verify your API returns the correct HTTP status codes for each operation.
  • Confirm error handling works for requests targeting non-existent resources.
Test the full CRUD cycle

CRUD stands for Create, Read, Update, and Delete. You'll send requests that exercise this entire cycle on a single book, proving every endpoint cooperates.

  • In Thunder Client, select GET from the method dropdown next to the URL bar.
  • Enter http://localhost:3000/books in the URL field.
  • Click Send.

You should see a JSON array containing all 3 starter books (1984, To Kill a Mockingbird, and The Great Gatsby) with a 200 status code.

  • Change the method dropdown to POST (keep the same URL http://localhost:3000/books).
  • Click the Body tab below the URL field.
  • Select JSON from the format dropdown.
  • Enter this JSON body:
{
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965
}
  • Click Send.

You should see a 201 Created status and a response containing your new book with id: 4.

  • Change the method back to GET.
  • Update the URL to http://localhost:3000/books/4.
  • Click Send.

You should see only the Dune book you just created, confirming it was stored correctly.

What do these status codes mean?

200 means the request succeeded and the server is returning data. 201 means a new resource was successfully created.

These codes tell the client (Thunder Client, a web browser, or a mobile app) exactly what happened without needing to parse the response body.

Seeing a connection error?

Your server may have stopped if you closed the terminal or restarted your computer. Switch to the VS Code terminal and run node index.js again to restart it.

help me debug a connection error in Thunder Client

Test update and delete

Now you'll modify the book you just created, remove it, and confirm the deletion stuck.

  • Change the method to PUT (keep the URL as http://localhost:3000/books/4).
  • In the Body tab, replace the previous JSON with:
{
  "year": 1966
}
  • Click Send.

You should see the Dune book with its year updated to 1966, while the title and author remain unchanged.

Why only send the year?

Your PUT endpoint uses a pattern where each field only updates if a new value is provided. Sending just {"year": 1966} leaves the title and author untouched, which is a common approach for partial updates.

  • Change the method to DELETE (keep the URL as http://localhost:3000/books/4).
  • Click Send.

You should see a 204 No Content status with an empty response body. This means the deletion succeeded, and the server has nothing to send back.

  • Change the method back to GET (keep the URL as http://localhost:3000/books/4).
  • Click Send.

You should see a 404 Not Found status with the error message Book not found. This confirms the book was permanently removed.

Test error handling

A production-ready API needs to handle bad requests gracefully. You'll verify that requesting a book that doesn't exist returns a clear error instead of crashing the server.

  • Set the method to GET and update the URL to http://localhost:3000/books/999.
  • Click Send.

You should see a 404 status with the message {"error": "Book not found"}. There is no book with ID 999, so your API correctly reports the problem.

  • Change the method to DELETE (keep the URL as http://localhost:3000/books/999).
  • Click Send.

You should see the same 404 status and error message. Your DELETE endpoint handles missing resources the same way your GET endpoint does.

Why does consistent error handling matter?

When every endpoint returns the same error structure (a JSON object with an error key), frontend developers can write one piece of error-handling code that works across all requests.

Without this consistency, clients would need custom logic for every endpoint, making the frontend harder to maintain.

Secret mission

Add Search and Filtering

Your API returns all books every time. Real APIs let users search and filter results. Can you add query parameters so users can find books by title or author?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The book-api project folder on your Desktop (includes index.js, package.json, and node_modules).
  • Thunder Client VS Code extension.
  • The running Node.js server process.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your API.

  • Your server stays running at http://localhost:3000 until you close the terminal or press Ctrl+C.
  • You can restart it anytime by running node index.js from inside the book-api folder.
  • Thunder Client stays installed in VS Code for future API testing.

Pause - I'll come back to this later

Shut down the running server process to free up the terminal. Your project files stay on disk for when you return.

  • In the VS Code terminal where your server is running, press Ctrl+C to stop the server.
  • Your book-api folder and all its files remain on your Desktop. To resume later, open the folder in VS Code and run node index.js.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cannot be undone.

  • Stop the server by pressing Ctrl+C in the terminal.
  • Delete the project folder by running this command on macOS/Linux:
rm -rf ~/Desktop/book-api

On Windows, run this in PowerShell:

Remove-Item -Recurse -Force ~\Desktop\book-api
  • To uninstall Thunder Client, click the Extensions icon in the VS Code sidebar.
  • Find Thunder Client in your installed extensions list, right-click it, and select Uninstall.

Nice Work!

Nice Work!

You just built a fully functional REST API from scratch. Your server accepts HTTP requests, manages data, and responds with proper status codes, exactly like the backends that power real web and mobile applications.

Here's what you accomplished:

  • Built a Node.js project with Express 5 and created 5 REST API endpoints covering all four CRUD operations.
  • Used proper HTTP status codes (200, 201, 204, 404) and parsed JSON request bodies and URL parameters.
  • Tested every endpoint with Thunder Client to verify the full request/response cycle.
  • Secret Mission: Added search and filtering with query parameters so users can find books by title or author.

Ready to quiz yourself?