Quick Answer

To build a REST API with Node.js and Express, create a project, install Express with npm, and start a server that listens on a port. Then define routes for each HTTP method — GET to read data, POST to create, PUT to update, and DELETE to remove — and send back JSON with res.json(). You can test GET routes right in your browser and the others with curl or a tool like Postman.

What You'll Build

In this tutorial you'll build a small but complete REST API with Node.js and Express — the same kind of backend that powers apps, websites, and mobile products. Instead of a real database, we'll keep a list of books in memory so you can focus on one thing: how routes and HTTP methods work.

By the end you'll have working GET, POST, PUT, and DELETE routes for a books resource, each sending back clean JSON. You'll also know how to test them from your browser and the command line.

You only need basic Node.js knowledge: how to run node file.js and roughly what a function is. If words like API, endpoint, JSON, or status code feel fuzzy, read our beginner explainer first — What is an API? A Simple Explanation for Beginners — and then come back here to build one yourself.

Make sure Node.js is installed. Check by running node -v in your terminal; if you see a version number, you're ready.

Setting Up the Project

Express is a small library that handles the tedious parts of a web server for you — routing, reading requests, and sending responses — so your code stays short and readable. Let's create a fresh project and install it.

Open your terminal and run these commands one by one:

mkdir book-api
cd book-api
npm init -y
npm install express

Here's what each line does. mkdir and cd create and enter a new folder. npm init -y creates a package.json file (the project's settings) with default answers. npm install express downloads Express into a node_modules folder and records it as a dependency.

Now create a file called index.js in that folder. This single file will hold our whole API. In a real project you'd split it into several files, but one file keeps the ideas clear while you're learning.

Writing Your First Server

Let's get a server running before we add any real logic. Put this in index.js:

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

app.get('/', (req, res) => {
  res.json({ message: 'API is running' });
});

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

Line by line: we load Express, create an app, and pick a port (3000 is a common choice for local work). The app.get('/', ...) part defines a route — when someone sends a GET request to /, our function runs. That function receives two objects: req (the incoming request) and res (the response we send back). We reply with JSON using res.json(...). Finally, app.listen starts the server.

Run it with:

node index.js

You should see Server running at http://localhost:3000. Open that address in your browser and you'll see the JSON message. Congratulations — that's a live web server.

Reading Data with GET

A REST API is organized around resources. Ours is books. First, add some sample data near the top of the file, just below the PORT line:

let books = [
  { id: 1, title: 'The Alchemist', author: 'Paulo Coelho' },
  { id: 2, title: 'Wings of Fire', author: 'A.P.J. Abdul Kalam' }
];

Now add two GET routes. Place them above the app.listen line:

// GET all books
app.get('/api/books', (req, res) => {
  res.json(books);
});

// GET one book by id
app.get('/api/books/:id', (req, res) => {
  const book = books.find(b => b.id === Number(req.params.id));
  if (!book) {
    return res.status(404).json({ error: 'Book not found' });
  }
  res.json(book);
});

The first route returns the whole list. The second uses :id, a route parameter — a placeholder in the URL. Express hands it to you as req.params.id. Note that URL values always arrive as text, so we convert it with Number(...) before comparing. If no matching book exists, we send a 404 status with a helpful message, which is the polite REST way to say "not found".

Restart the server (stop it with Ctrl+C, then run node index.js again) and visit http://localhost:3000/api/books in your browser.

Creating Data with POST

To add a new book, the client sends data in the request body as JSON. Express doesn't read that body by default, so you must switch on a small piece of middleware. Add this line once, right after const app = express();:

app.use(express.json());

This tells Express: "if a request arrives with JSON, parse it and put it on req.body for me." Forgetting this line is the single most common beginner bug — req.body ends up undefined. Now add the POST route:

// POST a new book
app.post('/api/books', (req, res) => {
  const { title, author } = req.body;
  if (!title || !author) {
    return res.status(400).json({ error: 'title and author are required' });
  }
  const newBook = {
    id: books.length ? books[books.length - 1].id + 1 : 1,
    title,
    author
  };
  books.push(newBook);
  res.status(201).json(newBook);
});

We read title and author from the body and validate them — never trust incoming data. If something is missing, we reply 400 (Bad Request). Otherwise we build a new book with the next id, add it to the list, and reply with status 201 (Created) plus the new record. Sending back what you created is a REST convention that helps the client confirm the result.

Updating and Deleting (PUT and DELETE)

The last two methods let clients change and remove data. Add both routes above app.listen:

// PUT (update) a book
app.put('/api/books/:id', (req, res) => {
  const book = books.find(b => b.id === Number(req.params.id));
  if (!book) {
    return res.status(404).json({ error: 'Book not found' });
  }
  const { title, author } = req.body;
  if (title !== undefined) book.title = title;
  if (author !== undefined) book.author = author;
  res.json(book);
});

// DELETE a book
app.delete('/api/books/:id', (req, res) => {
  const index = books.findIndex(b => b.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }
  const removed = books.splice(index, 1)[0];
  res.json(removed);
});

PUT finds the book by id and updates the fields that were sent, leaving others untouched. DELETE finds the book's position with findIndex and removes it with splice, then returns the deleted record. Both return 404 when the id doesn't exist. Notice the pattern: read the id, find the resource, handle the missing case, then act. Every route follows that same rhythm, which is what makes REST APIs predictable.

Testing with a Browser and curl

Your browser can only send GET requests easily, so it's perfect for the two GET routes — just paste the URL. For POST, PUT, and DELETE you need a tool that can set the method and body. The simplest is curl, a command that ships with Windows, macOS, and Linux.

With the server running, open a second terminal and try these:

# List all books
curl http://localhost:3000/api/books

# Create a book
curl -X POST http://localhost:3000/api/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Ignited Minds","author":"A.P.J. Abdul Kalam"}'

# Update a book
curl -X PUT http://localhost:3000/api/books/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"The Alchemist (2nd ed.)"}'

# Delete a book
curl -X DELETE http://localhost:3000/api/books/2

The -X flag sets the HTTP method, -H adds the Content-Type header so Express knows the body is JSON, and -d supplies that body. If you skip the header, express.json() won't parse the data and your fields will be empty.

Windows note: the single-quote style above is for macOS and Linux. On Windows, PowerShell treats curl as an alias for a different tool, so use curl.exe and double quotes with escaped inner quotes, or install the free Thunder Client extension in VS Code, which gives you a friendly click-based way to send any request.

Common Gotchas and Where to Go Next

A few things trip up almost everyone the first time:

  • Restart after edits. Node doesn't reload your file automatically. Stop the server and run it again, or install nodemon (npm install --save-dev nodemon) to restart on save.
  • Data resets on restart. Our books array lives in memory, so everything you add disappears when the server stops. That's fine for learning — persisting data is the next step.
  • Use the right status codes. 200 for success, 201 for created, 400 for bad input, 404 for not found. They tell clients what happened without reading the message.
  • One port at a time. If you see EADDRINUSE, an old server is still running on port 3000 — close it first.

My recommendation: build this exact API by hand at least once, then extend it. Add a PATCH route, add validation, and when you're comfortable, swap the in-memory array for a real database so your data survives restarts. That's the natural bridge from a toy API to a real one.

To go deeper on Node.js, Express, middleware, and connecting a database with worked projects, follow our free, structured Node.js course. It picks up right where this tutorial leaves off.

Frequently Asked Questions

How much Node.js do I need to know before this tutorial?

Very little. If you can install Node.js, run a file with node index.js, and read basic JavaScript like functions and arrays, you can follow along. Express handles the hard server parts for you, so the focus is on routing rather than low-level networking.

Why is req.body undefined in my POST route?

Almost always because you forgot to add app.use(express.json()); near the top of your file, or the client didn't send a Content-Type: application/json header. That middleware is what reads the JSON body and attaches it to req.body. Add the line, send the header, and restart the server.

What's the difference between PUT and PATCH?

PUT is meant to replace a resource with the full new version you send, while PATCH updates only the specific fields you include. In practice many APIs use PUT loosely for partial updates too, as we did here. Once you're comfortable, adding a separate PATCH route is a good exercise.

Can I test POST, PUT, and DELETE requests in a browser?

Not easily — typing a URL in the address bar always sends a GET request. To send other methods, use curl from the terminal, a VS Code extension like Thunder Client, or a tool such as Postman. Your two GET routes, though, work fine straight from the browser.

Where is the data stored, and why does it disappear when I restart?

In this tutorial the books live in a plain JavaScript array in memory, so they exist only while the server is running. Restarting resets the list to the two sample books. To keep data permanently, the next step is connecting a database such as MongoDB or a SQL database, which is exactly what a production API does.