Build a REST API with Node.js

Build a Book Collection API with full CRUD operations using Express.

Introduction

30 Second Summary

Every time you open an app on your phone, search for a product online, or check the weather, your device is sending a request to a server somewhere and getting data back. That invisible conversation between client and server is what makes the internet work.

In this project, you will build a REST API from scratch using Node.js and Express. You will create a server that listens for HTTP requests and responds with JSON data, handling all four CRUD operations for a book collection.

What You'll Build

A Book Collection API that manages books through five HTTP endpoints, complete with custom middleware for logging and error handling.

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

  • A Node.js project with Express installed and a development server that auto-restarts on file changes.
  • A fully functional REST API with five endpoints supporting all CRUD operations (Create, Read, Update, Delete).
  • Custom middleware for logging every incoming request and handling errors gracefully.
  • Secret Mission: Add search and filtering to your API so users can find books by title or author.

Are there any prerequisites?

You should be comfortable with basic JavaScript concepts like variables, functions, loops, objects, and arrays. No prior backend experience is needed.

Set Up Your Project

Before you can build an API, you need a workspace with the right tools installed. This step creates your project folder and gets a basic server running.

By the end of this step, you'll have a live Express server responding to requests on your machine. That's the foundation everything else builds on.

In this step, get ready to:
  • Verify your Node.js version and create a project folder.
  • Install Express and create a basic server.
  • Set up automatic restart so changes take effect instantly.
Open a terminal and verify Node.js

Express 5 requires Node.js version 18 or higher. You already have Node.js installed, so let's confirm the version is correct.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (that's the backtick key, above Tab on your keyboard).
  • Check your Node.js version by running this command:
node --version

You should see a version number like v22.x.x or v24.x.x. Any version 18 or above works.

Seeing a version below 18?

If your version is below 18, visit nodejs.org and download the latest LTS version. Run the installer and restart VS Code before continuing.

If you see "command not found", Node.js is not installed. Download it from nodejs.org and restart VS Code after installation.

Help me troubleshoot my Node.js version

Create your project folder and install Express

Every Node.js project lives in its own folder with a package.json file that tracks its dependencies. You'll create that folder now and install Express into 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
  • Confirm the folder exists by running:
ls

The folder will be empty for now. That's expected.

  • Initialize a new npm project by running:
npm init -y

What does this command do?

The npm init -y command creates a package.json file with default values. This file is the identity card for your project. It tracks the project name, version, and every package it depends on.

You should see a package.json output printed in your terminal confirming the file was created.

  • Install Express by running:
npm install express

What is Express?

Express is a framework that makes it easy to build web servers with Node.js. Without it, you'd need to write dozens of lines of low-level code just to handle a single request.

Express gives you clean patterns for defining routes, reading request data, and sending responses.

You should see output showing packages being added, ending with a confirmation that Express was installed. Your terminal may show a brief progress bar.

Seeing permission errors or EACCES?

Make sure you're inside the book-api folder (run pwd to check). If you see permission errors on macOS, try prefixing the command with sudo.

Help me fix my npm install error

Create your first Express server

With Express installed, you can write a server in just a few lines. This server will listen on port 3000 and respond to anyone who visits the root URL.

  • Open the book-api folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the book-api folder, and click Open.
  • Create a new file called index.js by clicking the new file icon in the Explorer sidebar (or press Ctrl+N then Ctrl+S and name it index.js).
  • Add the following code to index.js:
const express = require('express');
const app = express();
const PORT = 3000;

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

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

What does this code do?

  • require('express') loads the Express library into your file.
  • express() creates your application. All routes and settings attach to this app object.
  • app.get('/', ...) defines a route. When someone visits the root URL, the server responds with a JSON message.
  • app.listen(PORT, ...) starts the server. It will keep running and wait for incoming requests on port 3000.
  • Save index.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the terminal in VS Code (press Ctrl+` if it closed).
  • Start the server by running:
node index.js

You should see Server is running on http://localhost:3000 printed in your terminal.

  • Open your browser and visit http://localhost:3000.

You should see a JSON response: {"message":"Welcome to the Book Collection API"}.

Not seeing the welcome message?

If your terminal says something about a port already in use, another process is using port 3000. Close other running servers or terminals and try again.

If you see a "Cannot find module" error, make sure you ran npm install express earlier and that your terminal is inside the book-api folder.

Help me debug my Express server

✔️ Awesome, I've got everything!

Great. Make sure you've saved your file before moving on.

ⓧ I'd like to double check the full code

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

const express = require('express');
const app = express();
const PORT = 3000;

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

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});
Set up automatic restart for development

Right now, every time you change your code you'd have to stop the server and restart it manually. Node.js has a built-in --watch flag that restarts your server automatically whenever you save a file.

  • Stop your running server by pressing Ctrl+C in the terminal.
  • Open package.json in VS Code by clicking it in the Explorer sidebar.
  • Find the "scripts" section. It currently looks like this:
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"
}
  • Replace the entire scripts section with this:
"scripts": {
  "dev": "node --watch index.js",
  "start": "node index.js"
}
  • While you're in package.json, find the "description" line and update it from an empty string to:
"description": "A REST API for managing a book collection"

What does the --watch flag do?

The --watch flag tells Node.js to monitor your files for changes. Whenever you save, it automatically restarts the server. This saves you from manually stopping and restarting every time you edit your code.

  • Save package.json.
  • Start the server with auto-restart by running:
npm run dev

You should see Server is running on http://localhost:3000 again in your terminal. The server is now running in watch mode.

  • Visit http://localhost:3000 in your browser again to confirm it still works.

Seeing an "unknown command" error?

Make sure you saved package.json after editing the scripts section. Also check that "dev" is spelled exactly right and the JSON has no trailing commas.

Help me fix my npm run dev error

✔️ Awesome, I've got everything!

Great. Leave the server running in watch mode for the rest of this project.

ⓧ I'd like to double check the full code

Here's what your complete package.json should look like:

{
  "name": "book-api",
  "version": "1.0.0",
  "description": "A REST API for managing a book collection",
  "main": "index.js",
  "scripts": {
    "dev": "node --watch index.js",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^5.2.1"
  }
}

Your project is set up and your server is live. Next, you'll add routes that serve actual book data from your API.

Build Your First Routes

Your server is live and responding to requests. Now it's time to give it something useful to do.

In this step, you'll add book data to your API and create routes that let clients retrieve it. These GET endpoints are how any frontend app or tool fetches information from a server.

In this step, get ready to:
  • Add sample book data and JSON parsing middleware to your server.
  • Create two GET endpoints for retrieving books.
  • Test your routes in Postman with real HTTP requests.
Add sample book data and create the GET /books endpoint

Before your API can return book data, it needs two things: some data to serve and the ability to understand incoming JSON requests.

  • In index.js, find the line const PORT = 3000; and copy the following code below it (above the welcome route):
// Middleware to parse JSON request bodies
app.use(express.json());

// Sample book data
let books = [
  { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
  { id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
  { id: 3, title: '1984', author: 'George Orwell', year: 1949 }
];

What does this code do?

app.use(express.json()) is middleware that runs before every request hits your routes. It parses any incoming JSON body so you can access it later through req.body. You'll need this when you build the POST endpoint in the next step.

The books array acts as a temporary database. Each book object has four properties: id, title, author, and year. Using let instead of const means you can add and remove books later.

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

  • Below the books array (still above the welcome route), add the GET /books route by copying this code:
// GET /books - Get all books
app.get('/books', (req, res) => {
  res.json(books);
});

What does this route do?

app.get('/books', ...) tells Express to listen for GET requests at the path /books. When a request arrives, the callback function runs.

res.json(books) converts your JavaScript array into a JSON response and sends it back to the client. Express automatically sets the Content-Type header to application/json.

  • Save index.js. Your server restarts automatically thanks to the --watch flag.
  • Switch to Postman and send a GET request to http://localhost:3000/books.
  • You should see a JSON array containing all three books.

Not seeing the books array?

  • Make sure you saved index.js. The --watch flag only restarts the server when the file is saved.
  • Check that the new code sits between const PORT = 3000; and the welcome route. If it's below app.listen(), Express won't register the route.
  • Confirm the request method in Postman is set to GET (not POST or another method).

Help me debug why my GET /books endpoint returns nothing

Create the GET /books/:id endpoint to fetch a single book

A list of all books is useful, but clients often need one specific book. Route parameters let you capture a value from the URL path itself.

  • In index.js, find the GET /books route you just added. Copy the following code below it (still above the welcome route):
// GET /books/:id - Get a single book by ID
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 route do?

The :id in the path is a route parameter. Express captures whatever value appears in that URL segment and stores it in req.params.id. For example, a request to /books/2 sets req.params.id to "2".

parseInt() converts the string "2" into the number 2 so it matches the numeric id in your books array. If no book matches, the route returns a 404 status code with a JSON error message.

  • Save index.js.
  • In Postman, send a GET request to http://localhost:3000/books/1.
  • You should see The Hobbit returned as a single JSON object.
  • Now send a GET request to http://localhost:3000/books/999.
  • You should see a 404 response with the message {"error": "Book not found"}.

Getting an empty response or unexpected error?

  • Make sure the GET /books/:id route appears after the GET /books route in your file. Express matches routes top to bottom.
  • Check that parseInt(req.params.id) uses parseInt. Without it, you're comparing a string to a number and find() will never match.
  • Verify you typed /books/1 in the Postman URL bar (not /books/:id literally).

Help me figure out why my GET /books/:id route isn't returning the right book

Why does the data reset when the server restarts?

Your books array lives in memory. Every time the server restarts (which happens on each file save with --watch), the array resets to the original three books.

This is expected. Real applications solve this with a database that persists data to disk. For now, the in-memory approach keeps you focused on learning how routes work.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.js and your server is still running.

ⓧ 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());

// Sample book data
let books = [
  { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
  { id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
  { id: 3, title: '1984', author: 'George Orwell', year: 1949 }
];

// 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 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);
});

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

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 build the endpoints that let clients create, update, and delete books.

Create, Update, and Delete

Your API can read books, but a read-only API is like a library where you can browse the shelves but never check anything out or add new donations.

In this step, you will add the remaining three operations that make your API fully functional: creating new books, updating existing ones, and deleting books you no longer want. These map directly to the POST, PUT, and DELETE HTTP methods.

In this step, get ready to:
  • Build a POST endpoint to add new books to your collection.
  • Build a PUT endpoint to update existing book details.
  • Build a DELETE endpoint to remove books from your collection.
Create the POST /books endpoint

When a client sends a POST request, it includes data in the request body. Your server reads that data, creates a new book object, and adds it to the collection.

  • In index.js, find the welcome route that starts with app.get('/', ...).
  • Add the following code above that welcome route:
// POST /books - Add a new book
app.post('/books', (req, res) => {
  const { title, author, year } = req.body;

  if (!title || !author) {
    return res.status(400).json({ error: 'Title and author are required' });
  }

  const newBook = {
    id: books.length > 0 ? Math.max(...books.map(b => b.id)) + 1 : 1,
    title,
    author,
    year: year || null
  };

  books.push(newBook);
  res.status(201).json(newBook);
});

What does this code do?

This route handles POST requests to /books. Here is how each part works:

  • const { title, author, year } = req.body pulls the book details out of the incoming JSON body.
  • The if (!title || !author) check validates that required fields are present. If they are missing, it returns a 400 status (Bad Request) so the client knows what went wrong.
  • Math.max(...books.map(b => b.id)) + 1 generates a unique ID by finding the highest existing ID and adding 1.
  • res.status(201).json(newBook) sends back the newly created book with a 201 status code, which means "Created".
  • Save index.js.
  • Switch to Postman and create a new request.
  • Change the method dropdown from GET to POST.
  • Enter http://localhost:3000/books as the URL.
  • Click the Body tab below the URL bar.
  • Select raw, then change the dropdown on the right from Text to JSON.
  • Paste this JSON into the body area:
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "year": 1925
}
  • Click Send. You should see a response with your new book, an auto-generated id of 4, and a 201 Created status in the top right of the response panel.

Test the validation too

Try sending a POST request with the title field missing from the JSON body. You should get a 400 status and the error message Title and author are required.

Getting a blank or undefined response?

  • Make sure you selected raw and JSON in the Body tab. If you leave it on Text, Express cannot parse the body.
  • Check that your app.use(express.json()) line is still near the top of index.js, before your routes.
  • Confirm your JSON is valid. Missing quotes or trailing commas will cause a parse error.

Why isn't my POST request working in Postman?

Create the PUT /books/:id endpoint

PUT requests let a client update an existing resource. The client sends the book ID in the URL and the updated fields in the request body.

  • In index.js, add the following code below the POST route you just created (and still above the welcome route):
// PUT /books/:id - Update an existing 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;
  book.title = title || book.title;
  book.author = author || book.author;
  book.year = year || book.year;

  res.json(book);
});

What does this code do?

  • It finds the book by ID the same way the GET /books/:id route does. If no book matches, it returns a 404.
  • title || book.title means "use the new title if one was sent, otherwise keep the existing title." This lets clients update only the fields they care about.
  • res.json(book) sends back the updated book so the client can confirm what changed.
  • Save index.js.
  • In Postman, create a new request and change the method to PUT.
  • Enter http://localhost:3000/books/1 as the URL.
  • In the Body tab, select raw and JSON, then paste:
{
  "title": "The Hobbit: An Unexpected Journey"
}
  • Click Send. You should see book 1 returned with the updated title, while the author and year remain unchanged.

Book not updating?

  • Double-check the URL ends with /books/1 (not /books without an ID).
  • Make sure the method is PUT, not POST or GET.
  • Confirm you selected raw and JSON in the Body tab.

Why isn't my PUT request updating the book?

Create the DELETE /books/:id endpoint

DELETE requests remove a resource entirely. The client only needs to specify the book's ID in the URL.

  • In index.js, add the following code below the PUT route (and still above the welcome 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' });
  }

  const deletedBook = books.splice(index, 1)[0];
  res.json({ message: 'Book deleted successfully', book: deletedBook });
});

What does this code do?

  • books.findIndex(b => b.id === id) returns the position of the book in the array, or -1 if it does not exist.
  • books.splice(index, 1)[0] removes exactly one element at that position and returns it. The [0] grabs the removed item from the array that splice returns.
  • The response includes both a confirmation message and the deleted book's data, so the client knows exactly what was removed.
  • Save index.js.
  • In Postman, create a new request and change the method to DELETE.
  • Enter http://localhost:3000/books/2 as the URL.
  • Click Send. You should see a JSON response with the message Book deleted successfully and the deleted book's details.
  • Verify the deletion by sending a GET request to http://localhost:3000/books. The deleted book should no longer appear in the list.

Getting a 404 when deleting?

  • Remember that your data resets every time the server restarts. If --watch restarted the server after you added a book earlier, that book is gone. Use one of the original IDs (1, 2, or 3).
  • Make sure the ID in your URL is a number, not a word.

Why is my DELETE request returning a 404?

Why does data reset on restart?

Your books are stored in a JavaScript array in memory. Every time the server restarts, the code runs fresh and recreates the original 3 books.

This is expected for now. In a real production API, you would store data in a database so it persists across restarts.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the complete index.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());

// Sample book data
let books = [
  { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
  { id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
  { id: 3, title: '1984', author: 'George Orwell', year: 1949 }
];

// 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 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 - Add a new book
app.post('/books', (req, res) => {
  const { title, author, year } = req.body;

  if (!title || !author) {
    return res.status(400).json({ error: 'Title and author are required' });
  }

  const newBook = {
    id: books.length > 0 ? Math.max(...books.map(b => b.id)) + 1 : 1,
    title,
    author,
    year: year || null
  };

  books.push(newBook);
  res.status(201).json(newBook);
});

// PUT /books/:id - Update an existing 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;
  book.title = title || book.title;
  book.author = author || book.author;
  book.year = year || book.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' });
  }

  const deletedBook = books.splice(index, 1)[0];
  res.json({ message: 'Book deleted successfully', book: deletedBook });
});

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

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

Your API now supports the full set of CRUD operations. Next up, you will add middleware for logging every request and error handling for a more resilient server.

Add Middleware and Error Handling

Your API handles all five CRUD endpoints. But right now, you have zero visibility into what's happening on your server.

Every production API needs two things: logging (so you can see what requests hit your server) and error handling (so clients get clean JSON errors instead of cryptic defaults). In this step, you'll add both using Express middleware.

In this step, get ready to:
  • Create a custom logging middleware that tracks every incoming request.
  • Add error handlers for undefined routes and unexpected server failures.
Create a custom logging middleware

Middleware functions sit between the incoming request and your route handlers. They can inspect, log, or modify requests before passing them along with a call to next().

  • In index.js, find the line that says app.use(express.json());.
  • Add the following code directly below it:
// Custom logging middleware
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.url}`);
  next();
});

What does this code do?

  • new Date().toISOString() creates a timestamp like 2026-06-28T10:30:00.000Z so you know exactly when each request arrived.
  • req.method and req.url tell you what the client asked for (e.g. GET /books).
  • next() passes control to the next middleware or route handler. Without it, the request would hang forever.

Why does placement matter?

Express runs middleware in the order you register it. Because this middleware comes before your routes, it logs every request that hits your server. If you placed it after the routes, it would never run for matched routes.

  • Save index.js.
  • Switch to Postman and send a GET request to http://localhost:3000/books.
  • Return to your terminal in VS Code. You should see a log line like [2026-06-28T10:30:00.000Z] GET /books printed in the output.

Don't see a log line in your terminal?

  • Make sure you placed the logging middleware above your routes in index.js. If it's below the routes, matched requests skip it entirely.
  • Check that you included next() at the end of the middleware function. Without it, the request never reaches your route.
  • Confirm your server restarted by checking the terminal for the Server is running message. The --watch flag should restart it on save.

Why isn't my Express middleware logging requests?

Add a 404 catch-all handler and a global error handler

Right now, if someone requests a route that doesn't exist, Express returns a plain text response like Cannot GET /invalid. A proper API should return consistent JSON errors so clients can parse them programmatically.

  • In index.js, scroll down to find the app.listen call at the bottom of your file.
  • Add the following code directly above app.listen:
// 404 catch-all handler
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

// Global error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong' });
});

What does this code do?

The 404 catch-all sits at the bottom of your middleware stack, after all your defined routes. If no route matched the request, Express falls through to this handler and returns a clean JSON error.

The global error handler has a special four-parameter signature: (err, req, res, next). Express recognizes this pattern and sends any unhandled errors here instead of crashing your server. It logs the full error stack for debugging and sends a generic 500 response to the client.

  • Save index.js.
  • In Postman, send a GET request to http://localhost:3000/invalid.
  • You should see a JSON response: {"error": "Route not found"} with a 404 status code.
  • Check your terminal. You should also see a log line for this request from your logging middleware.

Still seeing plain text instead of JSON?

  • Make sure the 404 handler is placed after all your route definitions but before app.listen. If it's above a route, it will intercept valid requests.
  • Confirm the handler uses res.status(404).json(...) and not res.send(). The .json() method sets the correct Content-Type header.
  • Verify your server restarted after saving by checking for the Server is running message in your terminal.

Why is my 404 handler not returning JSON in Express?

✔️ Awesome, I've got everything!

Great. Double check you've saved index.js and that your server restarted.

ⓧ I'd like to double check the full code

Here's the complete index.js file with all middleware and error handling added:

const express = require('express');
const app = express();
const PORT = 3000;

// Middleware to parse JSON request bodies
app.use(express.json());

// Custom logging middleware
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.url}`);
  next();
});

// Sample book data
let books = [
  { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
  { id: 2, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
  { id: 3, title: '1984', author: 'George Orwell', year: 1949 }
];

// 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 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 - Add a new book
app.post('/books', (req, res) => {
  const { title, author, year } = req.body;

  if (!title || !author) {
    return res.status(400).json({ error: 'Title and author are required' });
  }

  const newBook = {
    id: books.length > 0 ? Math.max(...books.map(b => b.id)) + 1 : 1,
    title,
    author,
    year: year || null
  };

  books.push(newBook);
  res.status(201).json(newBook);
});

// PUT /books/:id - Update an existing 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;
  book.title = title || book.title;
  book.author = author || book.author;
  book.year = year || book.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' });
  }

  const deletedBook = books.splice(index, 1)[0];
  res.json({ message: 'Book deleted successfully', book: deletedBook });
});

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

// 404 catch-all handler
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

// Global error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong' });
});

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

Secret mission

Add Search and Filtering

Your API returns every book every time. Real-world APIs let users narrow down results without knowing exact IDs. Can you add search and filtering using query parameters?

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 Node.js server process running on port 3000.
  • The book-api project folder on your Desktop.

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with your API.

  • Your server keeps running on port 3000 until you close the terminal or stop it manually.
  • Your book-api folder stays on your Desktop, ready for you to modify or extend.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your project files stay on your Desktop for when you return.

  • Switch to the terminal in VS Code where your server is running.
  • Press Ctrl+C to stop the server.
  • When you come back, open the book-api folder in VS Code and run npm run dev to start the server again.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your code permanently.

  • Switch to the terminal in VS Code where your server is running.
  • Press Ctrl+C to stop the server.
  • Delete the project folder by running this command (macOS/Linux):
cd ~/Desktop
rm -rf book-api

On Windows, run this instead:

cd ~/Desktop
rmdir /s /q book-api
  • Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see book-api in the output.

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.

You've learned how to:

  • Built a REST API with five endpoints that handle full CRUD operations (Create, Read, Update, Delete) using Express routing and HTTP methods.
  • Created custom middleware for request logging and added error handling for undefined routes and server errors.
  • Tested every endpoint with real HTTP requests in Postman, sending JSON bodies and verifying correct status codes in the responses.
  • Secret Mission: Added search and filtering with query parameters so users can find books by title or author.

Ready to quiz yourself?