05. REST API

This module covers how to build a RESTful API with Express. We'll implement CRUD operations (Create, Read, Update, Delete) and learn REST conventions.

Demo Files

Run: npm install then node rest-api.js

Try it live: cse135.site:3004/demo - Interactive REST API tester

What is REST?

REST (Representational State Transfer) is an architectural style for building web APIs. Key principles:

Why "RESTful" Instead of "REST"?

You'll often hear APIs described as "RESTful" rather than "REST APIs." This distinction matters. REST was defined by Roy Fielding in his 2000 doctoral dissertation with six architectural constraints:

  1. Client-Server: Separation of concerns between UI and data storage
  2. Stateless: No client context stored on server between requests
  3. Cacheable: Responses must define themselves as cacheable or not
  4. Uniform Interface: Standardized way to interact with resources
  5. Layered System: Client can't tell if connected directly to server
  6. Code on Demand (optional): Server can extend client functionality

The "Uniform Interface" constraint includes a requirement called HATEOAS (Hypermedia as the Engine of Application State): responses should include links to related actions and resources, so clients discover the API by following links rather than hardcoding URLs.

The Reality: Most APIs called "REST" don't actually follow all these constraints. They're better described as "JSON over HTTP with REST-like URLs." This isn't necessarily wrong; it's pragmatic. Full HATEOAS compliance adds complexity that many applications don't need.

Common Deviations from Strict REST

Constraint Strict REST Common Practice
HATEOAS Responses include links to actions Clients hardcode API endpoints
Statelessness No server-side sessions Often use JWTs or session cookies
PUT vs PATCH PUT replaces entire resource PUT often does partial updates
Resource URLs Nouns only (/users/1) Sometimes verbs (/users/1/activate)
Practical Advice: Don't get dogmatic about REST purity. The goal is a consistent, predictable API that developers can use easily. If your team understands the conventions you're using, that's what matters.

HTTP Methods and CRUD

Operation HTTP Method URL Pattern Description
Create POST /api/items Create a new item
Read (all) GET /api/items Get all items
Read (one) GET /api/items/:id Get one item by ID
Update PUT /api/items/:id Update an item
Delete DELETE /api/items/:id Delete an item

Status Codes

REST APIs use HTTP status codes to indicate success or failure:

Code Meaning When to Use
200 OK Successful GET, PUT, DELETE
201 Created Successful POST (new resource created)
400 Bad Request Invalid input data
404 Not Found Resource doesn't exist
500 Server Error Something went wrong on the server

Building a REST API

Let's build a simple "items" API with in-memory storage:

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

app.use(express.json());

// In-memory "database"
let items = [
    { id: 1, name: 'Item One', completed: false },
    { id: 2, name: 'Item Two', completed: true }
];
let nextId = 3;

GET - Read All Items

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

GET - Read One Item

// GET /api/items/:id - Get one item
app.get('/api/items/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const item = items.find(i => i.id === id);

    if (!item) {
        return res.status(404).json({ error: 'Item not found' });
    }

    res.json(item);
});

POST - Create Item

// POST /api/items - Create new item
app.post('/api/items', (req, res) => {
    const { name } = req.body;

    // Validate input
    if (!name || typeof name !== 'string') {
        return res.status(400).json({ error: 'Name is required' });
    }

    const newItem = {
        id: nextId++,
        name: name.trim(),
        completed: false
    };

    items.push(newItem);
    res.status(201).json(newItem);  // 201 = Created
});

PUT - Update Item

// PUT /api/items/:id - Update an item
app.put('/api/items/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const item = items.find(i => i.id === id);

    if (!item) {
        return res.status(404).json({ error: 'Item not found' });
    }

    // Update fields if provided
    if (req.body.name !== undefined) {
        item.name = req.body.name;
    }
    if (req.body.completed !== undefined) {
        item.completed = Boolean(req.body.completed);
    }

    res.json(item);
});

DELETE - Remove Item

// DELETE /api/items/:id - Delete an item
app.delete('/api/items/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = items.findIndex(i => i.id === id);

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

    const deleted = items.splice(index, 1)[0];
    res.json({ message: 'Item deleted', item: deleted });
});

Testing with fetch()

Here's how to call the API from JavaScript:

// GET all items
const items = await fetch('/api/items').then(r => r.json());

// GET one item
const item = await fetch('/api/items/1').then(r => r.json());

// POST - Create item
const newItem = await fetch('/api/items', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'New Item' })
}).then(r => r.json());

// PUT - Update item
const updated = await fetch('/api/items/1', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Updated Name', completed: true })
}).then(r => r.json());

// DELETE - Remove item
const result = await fetch('/api/items/1', {
    method: 'DELETE'
}).then(r => r.json());

Testing with curl

Command-line testing:

# GET all items
curl http://localhost:3000/api/items

# GET one item
curl http://localhost:3000/api/items/1

# POST - Create item
curl -X POST http://localhost:3000/api/items \
  -H "Content-Type: application/json" \
  -d '{"name": "New Item"}'

# PUT - Update item
curl -X PUT http://localhost:3000/api/items/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated", "completed": true}'

# DELETE - Remove item
curl -X DELETE http://localhost:3000/api/items/1

Error Handling Pattern

Consistent error responses make APIs easier to use:

// Always return JSON with consistent structure
// Success: { data: ... } or the resource directly
// Error: { error: "message" }

app.get('/api/items/:id', (req, res) => {
    const id = parseInt(req.params.id);

    // Validate ID format
    if (isNaN(id)) {
        return res.status(400).json({ error: 'Invalid ID format' });
    }

    const item = items.find(i => i.id === id);

    if (!item) {
        return res.status(404).json({ error: 'Item not found' });
    }

    res.json(item);
});
Best Practice: Always return JSON from your API, even for errors. This makes client-side error handling consistent.

CORS (Cross-Origin Requests)

If your API will be called from a different domain, you need CORS headers:

// Simple CORS middleware
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type');

    // Handle preflight
    if (req.method === 'OPTIONS') {
        return res.sendStatus(200);
    }
    next();
});

// Or use the cors package:
// npm install cors
// const cors = require('cors');
// app.use(cors());

Try It Live

Test a real REST API running on our server:

Live Demo: cse135.site:3004/demo

This interactive demo lets you:

Note: The demo uses shared in-memory storage, so you'll see items created by other students!

View the source: rest-live-demo.js

Summary

Concept Key Points
REST Resources + HTTP methods + stateless + JSON
URL Design /api/resource and /api/resource/:id
GET Read data, no body, idempotent
POST Create data, return 201
PUT Update data, idempotent
DELETE Remove data, idempotent
Status Codes 200 OK, 201 Created, 400 Bad Request, 404 Not Found
Note: This demo uses in-memory storage. In a real application, you'd connect to a database like MySQL, PostgreSQL, or MongoDB. The API patterns remain the same; only the data access code changes.