Build a REST API with Express.js

Build a CRUD REST API server for a book collection using Express.js.

Introduction

30 Second Summary

Every time you open an app, scroll a feed, or add something to a cart, your device is sending a request to a server behind the scenes. That conversation follows a set of rules called REST.

In this project, you will build a Book Collection API server that handles requests to create, read, update, and delete books. You'll send requests from your terminal and see exactly how clients and servers communicate over HTTP.

What You'll Build

A REST API server built with Express.js that manages a collection of books through five endpoints you can test entirely from the terminal.

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

  • A fully functional Express.js server with 5 API endpoints for managing a book collection.
  • Proper HTTP status codes (200, 201, 404, 400, 204) that communicate success and errors to clients.
  • Input validation and error handling that returns structured JSON responses.
  • Secret Mission: Add a search endpoint that filters books by author or title using query parameters.

Are there any prerequisites?

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

Set Up Your Development Environment

Every REST API needs a home. Before you can write any server code, you need a project folder, a package manager configuration, and a framework to handle HTTP requests.

With Node.js already installed on your machine, you'll use npm to initialize a new project and pull in Express.js as your web framework.

In this step, get ready to:
  • Create the project folder and initialize it with npm.
  • Install Express as a dependency.
  • Open the project in VS Code and create the entry file server.js.
Create your project folder and install Express

You need a dedicated folder for your API project. This folder will hold your code, your dependencies, and your configuration files.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir book-collection-api
cd book-collection-api
  • Confirm the folder exists by running:
ls ~/Desktop

You should see book-collection-api in the output.

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

What does this command do?

The npm init -y command creates a package.json file with default values. The -y flag accepts all defaults so you skip the interactive questionnaire.

This file tracks your project's name, version, dependencies, and scripts. Think of it as the blueprint for your project.

You should see the generated JSON printed to your terminal, with "name": "book-collection-api" at the top.

  • Install Express by running:
npm install express --save

What does this command do?

This downloads Express and all its dependencies into a node_modules folder. The --save flag records Express in your package.json under dependencies so anyone else can install the same packages later.

You should see a message about added packages in the terminal output.

Seeing permission errors?

If you see EACCES permission errors, your npm installation may need a prefix fix. Try running the command with sudo in front, or check that your Node.js installation completed correctly.

help me fix npm permission errors when installing Express

Open the project in VS Code and create server.js

Now that your project folder has its dependencies installed, open it in your code editor to start building files.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the book-collection-api folder, then click Open.

You should see the Explorer sidebar on the left showing package.json and a node_modules folder.

Next, update your package.json to describe your project correctly and set up a start script.

  • Click on package.json in the Explorer sidebar to open it.
  • Select all the content in the file (Cmd+A on macOS or Ctrl+A on Windows).
  • Replace it with the following:
{
  "name": "book-collection-api",
  "version": "1.0.0",
  "description": "A REST API for managing a book collection",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "5.2.1"
  }
}

What changed from the defaults?

  • description now describes what your API does.
  • main changed from index.js to server.js to match the file you are about to create.
  • scripts.start is set to node server.js so you can launch your server with npm start later.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

Now create the entry file for your server.

  • In the Explorer sidebar, hover over the BOOK-COLLECTION-API heading area.
  • Click the New File icon (it looks like a page with a plus sign).
  • Type server.js and press Enter.

VS Code creates the file and opens it in a new tab. It will be empty for now.

Don't see the New File icon?

Make sure you hover over the project name in the sidebar, not over an individual file. The icon row (New File, New Folder, Refresh, Collapse) appears when you hover over the folder heading.

help me create a new file in VS Code's Explorer sidebar

✔️ Awesome, I've got everything!

Great. Double check you have both package.json and server.js visible in your Explorer sidebar, and that you have saved package.json.

ⓧ I'd like to double check the full code

Your package.json should look exactly like this:

{
  "name": "book-collection-api",
  "version": "1.0.0",
  "description": "A 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.js file should exist and be empty.

Your project scaffold is ready. Next up, you'll write your first Express server and see it respond to requests.

Create Your First Express Server

Your project folder is set up with Express installed and an empty server.js waiting for code. Now it's time to bring your API to life.

Every web API starts with a server that listens for incoming requests and sends back responses. In this step, you'll write your first Express.js server, start it up, and see a real JSON response come back from it.

In this step, get ready to:
  • Write a minimal Express server with a welcome route.
  • Start the server and test it with a curl request.
  • Add JSON parsing middleware for incoming request bodies.
Write your Express server code

An Express server needs three things: a reference to the Express library, an app instance created from it, and a port to listen on. You'll also define a route that tells the server how to respond when someone visits a specific URL.

  • Switch back to VS Code and open server.js from the sidebar.
  • Add the following code to the file:
const express = require('express');
const app = express();
const port = 3000;

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

// Start the server
app.listen(port, () => {
  console.log(`Book Collection API running at http://localhost:${port}`);
});

What does this code do?

  • require('express') loads the Express library you installed in Step 1.
  • express() creates your application instance. All routes and settings attach to this app object.
  • app.get('/', ...) defines a route. When a GET request hits the root URL, the callback sends back a JSON response.
  • app.listen(port, ...) starts the server and keeps it running, waiting for incoming requests on port 3000.
  • Save server.js.
Start the server and test it with curl

Your server code is ready, but nothing happens until you run the file. Starting the server tells Node.js to execute your code and begin listening for requests.

  • In your terminal, start the server by running this command:
node server.js

You should see this output in the terminal:

Book Collection API running at http://localhost:3000

Why is the terminal stuck?

The terminal isn't stuck. Your server is running and waiting for requests. It stays active until you stop it manually. This is expected behavior for any web server.

Now send your first request to the server. You'll need a second terminal window because the first one is busy running the server.

  • Open a new terminal tab or window by pressing Cmd+T (macOS) or Ctrl+Shift+T (Windows) in your terminal application.
  • Test the server by running this command:
curl http://localhost:3000

You should see this JSON response:

{"message":"Welcome to the Book Collection API"}

What just happened?

curl sent an HTTP GET request to your server at localhost:3000. Your server received the request, matched it to the GET / route, and sent back the JSON message.

This is the request/response cycle in action. Every REST API works this way: a client sends a request, and the server sends back a response.

Don't see the JSON response?

  • Make sure the server is still running in your first terminal. You should see the Book Collection API running at... message there.
  • Check that you're running curl in a separate terminal, not the one where the server is running.
  • Verify the URL is exactly http://localhost:3000 with no trailing slash or extra characters.

Ask for help: Why isn't my Express server responding to curl?

Add JSON parsing middleware

Your server can send JSON responses, but it can't yet understand JSON data sent TO it. When clients send data in a POST or PUT request, that data arrives as raw text in the request body.

Express middleware is code that runs between receiving a request and executing a route handler. The express.json() middleware automatically parses incoming JSON text into a JavaScript object you can work with.

  • Return to the terminal running your server.
  • Stop the server by pressing Ctrl+C.
  • In VS Code, find this line in server.js:
const port = 3000;
  • Add the following code directly below it:
// Middleware to parse JSON request bodies
app.use(express.json());

What does express.json() do?

app.use() registers middleware that runs on every incoming request. By placing express.json() before your routes, any request with a JSON body gets parsed automatically.

The parsed data becomes available as req.body inside your route handlers. You'll use this in the next step when you build the POST endpoint to create new books.

  • Save server.js.
  • Restart the server by running:
node server.js
  • Switch to your second terminal and test again by running:
curl http://localhost:3000

You should see the same JSON response as before. The middleware works silently in the background and doesn't change the output for GET requests.

Seeing an error on restart?

  • If you see SyntaxError, check that the app.use(express.json()) line has a semicolon at the end and is placed after const port = 3000; but before the app.get route.
  • If you see EADDRINUSE, the previous server is still running. Make sure you pressed Ctrl+C in the original terminal before restarting.

Ask for help: My Express server won't restart after adding middleware

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and your server is running without errors.

ⓧ I'd like to double check the full code

Here's the complete server.js file at the end of this step:

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

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

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

// Start the server
app.listen(port, () => {
  console.log(`Book Collection API running at http://localhost:${port}`);
});

Your Express server is running and ready to handle JSON data. Next up, you'll add real book data and build the GET endpoints that let clients retrieve it.

Build the GET Endpoints

Your server is running and responding to requests. Now it's time to give it real data to serve.

A REST API's primary job is responding to GET requests with data. In this step, you'll add a collection of books to your server and build two endpoints that return that data as JSON.

In this step, get ready to:
  • Add seed data to your server as an in-memory book collection.
  • Build two GET endpoints that return book data as JSON.
  • Test both endpoints with curl to see different response patterns.
Create an in-memory array of book objects as seed data

APIs need data to serve. Instead of connecting a database, you'll start with a simple array of book objects stored in memory. This keeps the focus on the API itself.

  • In VS Code, open server.js.
  • Find this line near the top of the file:
app.use(express.json());
  • Add the following code below that line to create your book collection:
// In-memory book collection (seed data)
let books = [
  { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
  { id: 2, title: '1984', author: 'George Orwell', year: 1949 },
  { id: 3, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
];

let nextId = 4;

What does this code do?

This creates a let variable called books holding three book objects. Each book has an id, title, author, and year.

The nextId variable tracks what ID to assign to the next new book. Since you already have books with IDs 1, 2, and 3, it starts at 4.

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

Implement GET /books to return all books

The first real data endpoint returns the full collection. When a client sends a GET request to /books, your server responds with the entire array as JSON.

  • In server.js, find the closing }); of the GET / route (the welcome message handler).
  • Add this code below it to create the GET /books route:
// GET /books - Get all books
app.get('/books', (req, res) => {
  res.status(200).json(books);
});

What does this code do?

  • app.get('/books', ...) registers a handler for GET requests to the /books path.
  • res.status(200) sets the HTTP status code to 200, which means "OK" (the request succeeded).
  • .json(books) converts the books array to JSON and sends it as the response body.
  • Save server.js.
  • Switch back to your terminal and stop the running server by pressing Ctrl+C.
  • Restart the server by running:
node server.js
  • Test the new endpoint by running this in a second terminal window:
curl http://localhost:3000/books

You should see a JSON array containing all three books with their id, title, author, and year fields.

Don't see the book data?

  • Make sure you saved server.js before restarting the server.
  • Check that you stopped the old server with Ctrl+C before running node server.js again. If the port is still in use, the new server won't start.
  • Verify the books array is defined above the route, not below it.

Still stuck? Why isn't my GET /books endpoint returning data?

Implement GET /books/:id to return a single book

Clients often need a single resource, not the whole collection. Route parameters let you capture a value from the URL path. Here, :id tells Express to grab whatever the client puts in that position and make it available as req.params.id.

  • In server.js, find the closing }); of the GET /books route you just added.
  • Add this code below it to create the GET /books/:id 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.status(200).json(book);
});

What does this code do?

  • parseInt(req.params.id) converts the URL parameter from a string to a number so it matches the numeric id in your book objects.
  • books.find(b => b.id === id) searches the array for a book with a matching ID. If none is found, book will be undefined.
  • The if (!book) check returns a 404 status ("Not Found") with an error message if the ID doesn't match any book.
  • The return keyword is important. It stops the function so res.status(200).json(book) doesn't also run.
  • Save server.js.
  • Stop the server with Ctrl+C and restart it by running:
node server.js
  • Test fetching a single book by running:
curl http://localhost:3000/books/1

You should see only the first book object returned: The Hobbit by J.R.R. Tolkien.

  • Now test with an ID that doesn't exist by running:
curl http://localhost:3000/books/99

You should see the error response: {"error":"Book not found"}. This is your 404 in action.

Getting an empty response or an error?

  • If you see Cannot GET /books/1, the route wasn't registered. Make sure the code is placed before the app.listen call at the bottom of the file.
  • If /books/1 returns null, check that you used parseInt() to convert the string parameter to a number. Without it, === won't match.
  • If both requests return the same thing, make sure the /books route and /books/:id route are separate app.get() calls.

Still stuck? Help me debug my GET /books/:id endpoint

✔️ Awesome, I've got everything!

Great. Double check that you've saved server.js and that both /books and /books/1 respond correctly.

ⓧ I'd like to double check the full code

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

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

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

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

let nextId = 4;

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

// GET /books - Get all books
app.get('/books', (req, res) => {
  res.status(200).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.status(200).json(book);
});

// Start the server
app.listen(port, () => {
  console.log(`Book Collection API running at http://localhost:${port}`);
});

Your API now serves data. Next up, you'll build the POST endpoint so clients can add new books to the collection.

Build the POST Endpoint

Your GET endpoints let clients read data from the server. But reading is only half the story.

A real API also needs to accept new data from clients. In this step, you'll build a POST endpoint that receives a JSON body, validates it, and adds a new book to your collection.

In this step, get ready to:
  • Implement a POST endpoint that creates new books.
  • Add input validation that returns proper error codes.
  • Test the endpoint with curl using POST-specific flags.
Implement the POST /books route

When a client sends a POST request, they include data in the request body. Your endpoint needs to read that data, check it's valid, and store the new book.

  • In server.js, find the GET /books/:id route. Add the following code below it, but above the app.listen block:
// POST /books - Create 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: nextId++,
    title,
    author,
    year: year || null
  };

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

What does this code do?

  • const { title, author, year } = req.body pulls the fields out of the JSON body the client sends. This is called destructuring.
  • The if (!title || !author) check ensures both required fields are present. If either is missing, the server responds with status 400 (Bad Request) and stops processing.
  • nextId++ assigns a unique ID and then increments the counter for the next book.
  • year: year || null makes the year field optional. If the client doesn't send one, it defaults to null.
  • Status 201 means "Created." It tells the client that a new resource was successfully made.

Why 400 and not 404?

Status 400 means the client sent a bad request. The server understood the request format, but the content was invalid or incomplete. Status 404 means the requested resource doesn't exist.

Choosing the right status code helps API consumers debug problems faster without reading your source code.

  • Save server.js.
  • Switch to your terminal and stop the running server by pressing Ctrl+C.
  • Restart the server by running:
node server.js
  • You should see Book Collection API running at http://localhost:3000 printed again with no errors.

Seeing a syntax error on startup?

  • Check that your new route is placed after the closing }); of the GET /books/:id route and before app.listen. A missing bracket will break the file.
  • Make sure every opening brace { has a matching close } and every opening parenthesis ( has a matching ).
  • help me fix a syntax error in my Express POST route

✔️ Awesome, I've got everything!

Great. Make sure you've saved server.js and your server restarted without errors.

ⓧ I'd like to double check the full code

Here is the full server.js file up to this point:

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

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

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

let nextId = 4;

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

// GET /books - Get all books
app.get('/books', (req, res) => {
  res.status(200).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.status(200).json(book);
});

// POST /books - Create 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: nextId++,
    title,
    author,
    year: year || null
  };

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

// Start the server
app.listen(port, () => {
  console.log(`Book Collection API running at http://localhost:${port}`);
});
Test the endpoint with curl

POST requests are different from GET requests. You need to tell curl three things: the HTTP method, the content type, and the data you're sending.

  • Open a new terminal tab (keep the server running in the first one).
  • Create a new book by running this curl command:
curl -X POST http://localhost:3000/books -H "Content-Type: application/json" -d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'

What do these flags mean?

  • -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 '{...}' sends the JSON string as the request body. This is the data your server reads from req.body.
  • You should see a response like this:
{"id":4,"title":"Dune","author":"Frank Herbert","year":1965}

The server responded with status 201 and returned the new book object with its assigned ID.

  • Now test the validation by sending a request missing the author field:
curl -X POST http://localhost:3000/books -H "Content-Type: application/json" -d '{"title": "Incomplete Book"}'
  • You should see the error response:
{"error":"Title and author are required"}

Your validation caught the missing field and returned a 400 status code. The book was not added to the collection.

  • Confirm the new book was actually stored by running:
curl http://localhost:3000/books
  • You should see 4 books in the array. "Dune" is now in your collection, but "Incomplete Book" is not.

Getting an empty body or unexpected results?

  • Make sure app.use(express.json()) is near the top of your file, before any routes. Without it, req.body will be undefined.
  • Check that you included the -H "Content-Type: application/json" flag. Without this header, Express won't parse the body as JSON.
  • help me debug why my POST request body is empty in Express

Your API can now create new books. Next up, you'll add the ability to update and delete books, completing the full set of CRUD operations.

Build the PUT and DELETE Endpoints

Your API can now create new books and retrieve them. That covers the "C" and "R" in CRUD. Two operations remain: updating existing books and removing them entirely.

In this step, you'll implement PUT and DELETE routes to complete the full set of REST operations. By the end, your API will handle every standard action a client might request on a book resource.

In this step, get ready to:
  • Implement a PUT endpoint to update book fields.
  • Implement a DELETE endpoint to remove a book.
  • Test both endpoints with curl and verify changes persist.
Implement the PUT endpoint

A PUT request tells the server to update an existing resource with new data. Your endpoint needs to find the book, check it exists, then apply the changes.

  • In server.js, find the comment // Start the server near the bottom of the file.
  • Add the following PUT route above that comment:
// PUT /books/:id - Update a book
app.put('/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === id);

  if (bookIndex === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  const { title, author, year } = req.body;

  if (title) books[bookIndex].title = title;
  if (author) books[bookIndex].author = author;
  if (year) books[bookIndex].year = year;

  res.status(200).json(books[bookIndex]);
});

What does this code do?

  • findIndex returns the position of the matching book in the array, or -1 if no book has that ID.
  • The if (bookIndex === -1) check returns a 404 status to tell the client the book does not exist.
  • Each if (title) conditional only updates a field if the client sent a new value for it. This lets clients update one field without erasing the others.
  • The response returns status 200 with the full updated book object so the client can confirm what changed.

Why findIndex instead of find?

In the GET route, you used find() to retrieve the book object directly. Here you need findIndex() because you need to modify the book in-place inside the array. The index lets you target the exact position with books[bookIndex].

  • You won't see a change yet. The next chunk adds the DELETE route, then you'll test both together.
Implement the DELETE endpoint

A DELETE request removes a resource permanently. The server should confirm the book exists, remove it, and respond with no body.

  • Still in server.js, add the following DELETE route directly below the PUT route you just added (and still above // Start the server):
// DELETE /books/:id - Delete a book
app.delete('/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === id);

  if (bookIndex === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  books.splice(bookIndex, 1);
  res.status(204).send();
});

What does this code do?

  • The route uses the same findIndex pattern to locate the book and return 404 if it does not exist.
  • splice(bookIndex, 1) removes exactly one element from the array at that position.
  • Status 204 means "No Content". It tells the client the deletion succeeded, but there is nothing to send back. The .send() call sends the response with an empty body.
  • Save server.js.

Seeing a syntax error on save?

  • Check that each route handler has matching opening and closing curly braces {} and parentheses ().
  • Make sure the PUT and DELETE routes sit between the POST route and the app.listen call. They should not be nested inside another route.
  • Verify you have a closing }); for each app.put and app.delete call.

Help me debug a syntax error in my Express route handlers.

✔️ Awesome, I've got everything!

Great. Make sure 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;

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

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

let nextId = 4;

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

// GET /books - Get all books
app.get('/books', (req, res) => {
  res.status(200).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.status(200).json(book);
});

// POST /books - Create 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: nextId++,
    title,
    author,
    year: year || null
  };

  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 bookIndex = books.findIndex(b => b.id === id);

  if (bookIndex === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  const { title, author, year } = req.body;

  if (title) books[bookIndex].title = title;
  if (author) books[bookIndex].author = author;
  if (year) books[bookIndex].year = year;

  res.status(200).json(books[bookIndex]);
});

// DELETE /books/:id - Delete a book
app.delete('/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === id);

  if (bookIndex === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  books.splice(bookIndex, 1);
  res.status(204).send();
});

// Start the server
app.listen(port, () => {
  console.log(`Book Collection API running at http://localhost:${port}`);
});
Test both endpoints with curl

Time to confirm your new endpoints work. You will update a book's title with PUT, delete a different book with DELETE, then verify both changes with a GET request.

  • Switch to your terminal. If your server is still running, press Ctrl+C to stop it.
  • Restart the server by running:
node server.js
  • Test the PUT endpoint by updating book 1's title. Run this command:
curl -X PUT -H "Content-Type: application/json" -d '{"title":"The Lord of the Rings"}' http://localhost:3000/books/1

You should see the updated book returned as JSON with the new title:

{"id":1,"title":"The Lord of the Rings","author":"J.R.R. Tolkien","year":1937}

What do the curl flags mean?

  • -X PUT sets the HTTP method to PUT instead of the default GET.
  • -H "Content-Type: application/json" tells the server the request body is JSON.
  • -d '{...}' attaches the JSON data as the request body. You only sent title, so only the title changed.
  • Now test the DELETE endpoint by removing book 2. Run this command:
curl -X DELETE http://localhost:3000/books/2

You will see no output. A 204 response has no body, which means the deletion succeeded silently.

  • Verify both changes persisted by fetching all books:
curl http://localhost:3000/books

You should see two books remaining. Book 1 now has the updated title "The Lord of the Rings", and book 2 ("1984") is gone.

Still seeing the original data?

  • Make sure you stopped and restarted the server after saving your code changes. The old server process does not pick up new code automatically.
  • If you restarted but still see the original three books, check that you saved server.js before restarting.
  • Remember, restarting the server resets the in-memory data to the seed values. Run your PUT and DELETE commands again after each restart.

Help me debug why my PUT and DELETE changes aren't showing up.

Secret mission

Add a Search Endpoint

Your API can return all books or find one by ID. But real APIs let users search and filter results without knowing exact IDs. Can you add a search endpoint that filters books by author or title using query parameters?

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-collection-api project folder on your Desktop (contains server.js, package.json, and node_modules).
  • The running Express.js server process on port 3000.

Keep everything running

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

  • Your server continues running on port 3000 until you close the terminal or press Ctrl+C.
  • Your project files remain on your Desktop, ready to edit anytime.
  • Node.js stays installed on your machine for future projects.

Pause - I'll come back to this later

Stop the running server to free up port 3000, but keep all your project files intact so you can pick up where you left off.

  • Switch to the terminal where your server is running.
  • Press Ctrl+C to stop the server process.
  • To restart later, open your terminal, navigate to the book-collection-api folder, and run node server.js.

Delete - I don't want to use this again

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

  • Switch to the terminal where your server is running and press Ctrl+C to stop it.
  • Delete the project folder by running these commands:

macOS

cd ~/Desktop
rm -rf book-collection-api

Windows

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

Nice Work!

Nice Work!

You just built a fully functional REST API from scratch. You now understand how backends receive requests, process data, and return structured responses using Express.js.

Here's what you learned:

  • Built a REST API server with Express.js handling 5 endpoints that cover all CRUD operations (Create, Read, Update, Delete).
  • Used proper HTTP status codes (200, 201, 400, 404, 204) to communicate success and errors between client and server.
  • Parsed JSON request bodies, validated input, and handled edge cases like missing resources with meaningful error messages.
  • Added search functionality with query parameter filtering for case-insensitive partial matching.

Ready to quiz yourself?