APIs & AsyncIntermediate10 min07 / 8

Building a REST API

Learn the REST conventions that power the web, then build a real in-memory CRUD API with Express — resources, HTTP verbs, status codes, and JSON all included.

Almost every app you use daily — weather apps, social feeds, shopping carts — is secretly two programs talking to each other over the internet. The frontend (what you see) sends requests; the backend (the server) replies with data. The agreed-upon language for that conversation is usually a REST API. Once you understand REST, you can build the backend half of any web application. In this lesson you will go from zero to a working, in-memory API that can create, read, update, and delete items — the four fundamental operations of nearly every data-driven app.

#What is REST?

REST stands for Representational State Transfer — a set of conventions (not a strict standard) for designing web APIs. Three ideas make up the heart of it:

  1. Resources — Everything the API exposes is a "resource" (a book, a user, an order). Each resource lives at a URL.
  2. HTTP verbs — The type of action you want is encoded in the HTTP method: GET to read, POST to create, PUT/PATCH to update, DELETE to remove.
  3. Stateless requests — Each request must contain all the information the server needs. The server does not remember what you asked last time.

That's it. REST is not a library or a framework; it is a style.

Think of it like

A REST API is like a post office

Imagine every type of item in a warehouse has its own address (URL). You send a letter (HTTP request) to that address. The envelope color tells the warehouse worker what you want: a blue envelope means "send me the item" (GET), a green envelope means "here is a new item, please store it" (POST), a yellow envelope means "replace this item" (PUT), and a red envelope means "throw this item away" (DELETE). The worker sends back a reply slip (the HTTP response) confirming what happened — or explaining what went wrong.

#HTTP Verbs at a Glance

For a resource like /books, here is the conventional mapping of verbs to actions:

| Method | URL | What it does | |--------|-----|--------------| | GET | /books | Return all books | | GET | /books/42 | Return book with id 42 | | POST | /books | Create a new book | | PUT | /books/42 | Replace book 42 entirely | | DELETE | /books/42 | Delete book 42 |

This naming convention is not enforced by any technology — it is a social contract that makes APIs predictable for every developer who uses them.

#Status Codes: the Server's Emoji

Every HTTP response carries a status code — a three-digit number that tells the client at a glance whether things went well. You do not need to memorize them all, but a handful appear in every API:

  • 200 OK — the request succeeded
  • 201 Created — a new resource was created (use this after a successful POST)
  • 400 Bad Request — the client sent something the server could not understand
  • 404 Not Found — the resource does not exist
  • 500 Internal Server Error — something broke on the server side

Choosing the right status code is part of building a polite API. A client should be able to know whether its request worked without parsing the body.

Tip

201 vs 200 after a POST

When you create something new, respond with 201 Created, not 200 OK. It is a small detail, but it lets the caller know exactly what happened — and some HTTP clients and proxies treat 201 differently from 200. It costs one extra character and signals intent clearly.

#Setting Up Express

Express is the most popular Node.js framework for building APIs. It handles the plumbing — routing, parsing request bodies, sending responses — so you can focus on your logic. Start a new project and install it:

This creates a package.json and installs Express. You only need these three commands to start.
mkdir books-api && cd books-api
npm init -y
npm install express

#Building the API: GET and POST

Here is a complete, working file for an in-memory books API. "In-memory" means the data lives in a JavaScript array — it resets when the server restarts, which is perfect for learning because there is no database to set up.

Run with 'node index.js'. The array is your database — simple, zero config.
// index.js
const express = require("express");
const app = express();

// Tell Express to parse JSON request bodies automatically
app.use(express.json());

// In-memory "database"
let books = [
  { id: 1, title: "The Pragmatic Programmer", author: "Hunt & Thomas" },
  { id: 2, title: "You Don't Know JS", author: "Kyle Simpson" }
];
let nextId = 3;

// GET /books — return all books
app.get("/books", (req, res) => {
  res.json(books);
});

// GET /books/:id — return one book
app.get("/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);
});

// POST /books — create a new book
app.post("/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: nextId++, title, author };
  books.push(newBook);
  res.status(201).json(newBook);
});

app.listen(3000, () => console.log("API running on http://localhost:3000"));
Common mistake

Forgetting express.json() is a classic first bug

Without app.use(express.json()), req.body is always undefined — even when the client sends a perfectly valid JSON body. This trips up almost every beginner. Always add express.json() before your routes if you expect a JSON body.

#Adding PUT and DELETE

Add these routes to index.js before app.listen. Now you have full CRUD.
// PUT /books/:id — replace a book
app.put("/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 { title, author } = req.body;
  if (!title || !author) {
    return res.status(400).json({ error: "title and author are required" });
  }

  books[index] = { id: Number(req.params.id), title, author };
  res.json(books[index]);
});

// DELETE /books/:id — remove a book
app.delete("/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" });

  books.splice(index, 1);
  res.status(200).json({ message: "Book deleted" });
});

#Testing Your API

With the server running (node index.js), you can test every endpoint with curl in a second terminal — no browser or extra tool needed.

curl is your best friend for quickly poking at an API. The -X flag sets the HTTP method; -H adds a header; -d sends a body.
# Get all books
curl http://localhost:3000/books

# Create a new book
curl -X POST http://localhost:3000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Clean Code", "author": "Robert Martin"}'

# Get book with id 3
curl http://localhost:3000/books/3

# Delete book with id 1
curl -X DELETE http://localhost:3000/books/1
Quick check

You send a POST request to create a new resource, and the server responds with status code 200. What is the problem?

Key takeaways

  • REST maps HTTP verbs (GET, POST, PUT, DELETE) onto CRUD actions on named resources (URLs).
  • Status codes communicate the result at a glance: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Server Error.
  • Always add `app.use(express.json())` before your routes, or `req.body` will be undefined.
  • An in-memory array is a great zero-setup database for learning; swap it for a real database when you're ready.
  • Test any API quickly with `curl` from the terminal — no extra tools needed.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

This POST route from the books API runs when a client sends a valid JSON body of {"title": "Clean Code", "author": "Robert Martin"}. What status code does the server send back?

predict-output
app.post("/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: nextId++, title, author };
  books.push(newBook);
  res.status(201).json(newBook);
});
Fix the bug#2

This POST route reads req.body but title and author always come back undefined, even when the client sends valid JSON. What's wrong?

fix-bug
const express = require("express");
const app = express();

app.post("/books", (req, res) => {
  const { title, author } = req.body;
  const newBook = { id: nextId++, title, author };
  books.push(newBook);
  res.status(201).json(newBook);
});

app.listen(3000);
Fill in the blank#3

Complete this GET route so it returns 404 when the book id doesn't exist. Fill in the HTTP status code for 'Not Found'.

app.get("/books/:id", (req, res) => {
  const book = books.find(b => b.id === Number(req.params.id));
  if (!book) return res.status().json({ error: "Book not found" });
  res.json(book);
});
Reorder the lines#4

Put these lines in the correct order to build a minimal Express books API that parses JSON, serves GET /books, and starts listening.

1
app.listen(3000, () => console.log("API running"));
2
app.use(express.json());
3
const app = express();
4
const express = require("express");
5
app.get("/books", (req, res) => res.json(books));
Your turn
Practice exercise

Extend the books API with a PATCH /books/:id endpoint that lets callers update only the fields they provide. If only 'title' is sent, only the title should change; if only 'author' is sent, only the author should change. Return 404 if the book does not exist, and 400 if neither field is provided.

Try it yourself — a starting point to build on:

starter.js
// Assume 'books' array and Express app already exist from the lesson.

// TODO: Add a PATCH /books/:id route
app.patch("/books/:id", (req, res) => {
  // TODO: Find the book by id (remember to convert req.params.id to a Number)
  const index = /* find the book's index */;

  // TODO: Return 404 if not found

  // TODO: Destructure title and author from req.body

  // TODO: Return 400 if neither field was provided

  // TODO: Update only the fields that were provided (leave the rest unchanged)

  // TODO: Respond with the updated book
});