Topic 10 of 18 · Node Expert

REST APIs

Lesson TL;DRTopic 10: REST APIs 📖 11 min read · 🎯 advanced · 🧭 Prerequisites: buildingahttpserverwithnodejsusinghttpapis, socketiothefrontendandachatapp Why this matters Up until now, you've built Node servers...
11 min read·advanced·rest-api · express · node-js · http-methods

Topic 10: REST APIs

📖 11 min read · 🎯 advanced · 🧭 Prerequisites: building-a-http-server-with-node-js-using-http-apis, socket-io-the-front-end-and-a-chat-app

Why this matters

Up until now, you've built Node servers that mostly talk to a browser on your own machine. But here's the thing — the real world expects your server to speak a shared language that any client can understand, whether it's a browser, a mobile app, or someone else's server entirely. That shared language is REST. Four simple verbs: GET to fetch data, POST to create it, PUT to replace it, DELETE to remove it. Today we build an Express REST API from scratch — and once you know this, you can connect anything to anything.

What You'll Learn

  • Initialize a Node.js project and install Express.js as a REST framework
  • Define GET, POST, PUT, and DELETE endpoints with Express route handlers
  • Manage in-memory resource data using standard HTTP status codes
  • Test all CRUD operations against a running API with Postman

The Analogy

Think of a REST API as the front desk of a grand hotel registry. Guests (clients) walk up and make requests: "Show me room 12," "Book a new room," "Change the name on room 7," "Check out of room 3." The front-desk clerk (your Express server) looks up the registry (your data store), performs the requested operation, and hands back a formal receipt — a status code and a JSON response. Every interaction follows the same polite protocol, so any guest who knows the rules can use any front desk in the world.

Chapter 1: Setting Up the Workshop

Before writing a single route, the project environment needs to be ready. Three steps get you there.

1. Install Node.js and npm

Download Node.js from the official site — npm ships with it automatically. npm (Node Package Manager) is the toolbox that installs every library you need.

2. Initialize a new project

Create a project directory, then run:

npm init -y

This generates a package.json file that tracks all project dependencies and metadata. The -y flag accepts every default prompt automatically.

3. Install Express.js

Express is a flexible, minimalistic Node.js framework designed for building web servers and REST APIs quickly:

npm install express

After this command finishes, express is listed under dependencies in package.json and its files live inside node_modules/.

Chapter 2: Crafting the API

With the environment ready, create a file named app.js and build the API piece by piece.

2.1 — Creating the Server

Require Express, instantiate the app, configure JSON body parsing, and start the listener:

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

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

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

express.json() is middleware that reads the raw request body and populates req.body with a parsed JavaScript object — essential for POST and PUT routes.

2.2 — Seeding In-Memory Data

For this lesson the data store is a plain JavaScript array. In production you would swap this for a database, but the route logic stays identical:

let creatures = [
  { id: 1, name: 'Dragon',  type: 'Fire'  },
  { id: 2, name: 'Phoenix', type: 'Fire'  },
  { id: 3, name: 'Unicorn', type: 'Earth' },
];

2.3 — Defining the Endpoints

GET all resources

// Get all creatures
app.get('/creatures', (req, res) => {
  res.json(creatures);
});

Returns the full array as JSON with an implicit 200 OK.

GET a single resource by ID

// Get a specific creature by ID
app.get('/creatures/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const creature = creatures.find(c => c.id === id);
  if (creature) {
    res.json(creature);
  } else {
    res.status(404).send('Creature not found');
  }
});

:id is a route parameter; Express exposes it at req.params.id as a string (e.g., "7"). Parse it with parseInt(req.params.id, 10) and compare with strict === so ESLint's eqeqeq rule and a future TypeScript migration both stay happy. If no match is found, the handler responds with 404 Not Found.

parseInt caveat: parseInt("7abc", 10) returns 7 (truncating trailing junk), so GET /creatures/7abc would match creature 7. For strict validation either swap to Number(req.params.id) (returns NaN on "7abc") or guard with Number.isInteger(Number(req.params.id)) and reject early with 400.

POST — create a new resource

// Add a new creature
app.post('/creatures', (req, res) => {
  const nextId = creatures.reduce((max, c) => Math.max(max, c.id), 0) + 1;
  const newCreature = { id: nextId, ...req.body };
  creatures.push(newCreature);
  res.status(201).json(newCreature);
});

The spread ...req.body merges the client-supplied fields with the auto-generated id. We compute the next id from Math.max of existing ids (rather than creatures.length + 1) so that a POST → DELETE → POST sequence does not produce duplicate ids. 201 Created is the correct success status for resource creation. In production you would typically delegate id assignment to the database (SERIAL, UUID, etc.).

PUT — replace the resource

// Replace a creature (PUT = full replacement, per the HTTP spec)
app.put('/creatures/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const index = creatures.findIndex(c => c.id === id);
  if (index !== -1) {
    creatures[index] = { id, ...req.body };
    res.json(creatures[index]);
  } else {
    res.status(404).send('Creature not found');
  }
});

PUT replaces the whole resource — any field the client omits is now gone. If you instead want a partial update (change only the fields the client sent), that's a different verb: PATCH.

PATCH — partially update a resource

// Patch a creature (PATCH = merge-style partial update)
app.patch('/creatures/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const creature = creatures.find(c => c.id === id);
  if (creature) {
    Object.assign(creature, req.body);
    res.json(creature);
  } else {
    res.status(404).send('Creature not found');
  }
});

Object.assign(creature, req.body) mutates the found object in place, merging only the keys the client supplied. The two verbs intentionally behave differently: PUT is "here's the new full resource", PATCH is "here's a delta — keep the rest".

DELETE — remove a resource

// Delete a creature
app.delete('/creatures/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const index = creatures.findIndex(c => c.id === id);
  if (index !== -1) {
    creatures.splice(index, 1);
    res.status(204).send();
  } else {
    res.status(404).send('Creature not found');
  }
});

204 No Content is the conventional response for a successful deletion — the resource is gone, so there is nothing to return in the body.

2.4 — Running the Server

node app.js

The server starts at http://localhost:3000 and is ready to accept requests.

Chapter 3: Testing the API with Postman

With the server running, open Postman (or any HTTP client) and fire each of the following requests to verify correct behavior:

MethodURLExpected StatusWhat to check
GET/creatures200Array of 3 creatures
GET/creatures/1200Dragon object
GET/creatures/99404"Creature not found"
POST/creatures201New creature echoed back
PUT/creatures/1200Updated Dragon object
DELETE/creatures/1204Empty body

For the POST request, set the body to raw → JSON in Postman and send:

{
  "name": "Griffin",
  "type": "Air"
}

The server should respond with { "id": 4, "name": "Griffin", "type": "Air" }.

Each test validates that the correct HTTP verb triggers the correct handler and that status codes communicate the outcome accurately.

🧪 Try It Yourself

Task: Extend the creatures API with a search endpoint that filters by type.

  1. Critical first step — choose a path that cannot collide with /creatures/:id. Express matches routes top-to-bottom; if you registered /creatures/:id before /creatures/search, the literal string "search" would be captured as req.params.id and you would get 404 Creature not found. Pick a path that side-steps the issue entirely — /search/creatures is the safe shape.
  2. Add this route to app.js above the app.listen call (anywhere before it is fine, because the path no longer overlaps with /creatures/:id):
// GET /search/creatures?type=Fire
app.get('/search/creatures', (req, res) => {
  const { type } = req.query;
  const results = type
    ? creatures.filter(c => c.type.toLowerCase() === type.toLowerCase())
    : creatures;
  res.json(results);
});
  1. Start the server with node app.js, then visit http://localhost:3000/search/creatures?type=Fire in your browser or Postman.

Success criterion: You should see a JSON array containing only Dragon and Phoenix — the two creatures with "type": "Fire".

Alternative shape: If you really want the URL to be /creatures/search, you must register it above /creatures/:id in your route table. The path-segment collision is a classic Express foot-gun; using a distinct prefix (/search/...) sidesteps the issue altogether and is the recommendation for this exercise.

🔍 Checkpoint Quiz

Q1. Why does the POST handler respond with status 201 instead of 200?

A) 200 is reserved for GET requests only B) 201 Created specifically communicates that a new resource was created, giving clients richer semantic information C) Express automatically rejects 200 on POST routes D) 201 skips body serialization for performance

Q2. Given the following handler, what does a DELETE /creatures/7 request return when no creature with ID 7 exists?

app.delete('/creatures/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const index = creatures.findIndex(c => c.id === id);
  if (index !== -1) {
    creatures.splice(index, 1);
    res.status(204).send();
  } else {
    res.status(404).send('Creature not found');
  }
});

A) 200 with an empty array B) 204 with no body C) 404 with the text "Creature not found" D) 500 because splice throws on a missing index

Q3. What is the role of app.use(express.json()) in the server setup?

A) It converts all responses to JSON automatically B) It restricts the API to only accept application/json Content-Type headers C) It parses raw request bodies into req.body so route handlers can read JSON payloads D) It validates that all JSON bodies conform to a predefined schema

Q4. You are building a REST API for a bookstore. Users should be able to update only the price field of a book without sending the full book object. Which HTTP method best fits this operation?

A) GET /books/:id?price=14.99 — read-with-side-effect
B) PUT /books/:id with the full book object replayed minus the changed price
C) PATCH /books/:id with a body like { "price": 14.99 }, merged via Object.assign
D) POST /books/:id/price because POST is the universal "change something" verb

A1. B — 201 Created is the semantically correct status for resource creation. It tells the client that not only did the request succeed, but a new resource now exists at the server.

A2. C — findIndex returns -1 when no match is found, so the else branch executes and sends 404 with the message "Creature not found".

A3. C — express.json() is body-parsing middleware. Without it, req.body is undefined for JSON payloads, causing POST and PUT handlers to silently lose all submitted data.

A4. C — PATCH /books/:id with Object.assign(book, req.body) merges only the keys the client supplied, leaving every other field untouched. PUT would replace the entire resource (and wipe any field the client omitted); GET must be safe/side-effect-free; POST on a sub-resource works but is less idiomatic than PATCH for a partial field update.

🪞 Recap

  • npm init -y bootstraps a Node.js project and npm install express adds the Express framework.
  • Express route methods (app.get, app.post, app.put, app.delete) map HTTP verbs to handler functions.
  • express.json() middleware must be registered before any route that reads req.body.
  • HTTP status codes are semantic contracts: 200 success, 201 created, 204 no content, 404 not found.
  • Route parameters (:id) are accessed via req.params; query strings via req.query.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

Block your seat for ₹2,500 and join the next cohort.