REST API

Building a RESTful API with Express, implementing CRUD operations and learning REST conventions

What is REST?

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

  • Resources: Everything is a resource (users, products, orders) identified by URLs
  • HTTP Methods: Use standard HTTP verbs for operations
  • Stateless: Each request contains all information needed; server doesn't store session state
  • JSON: Responses are typically JSON (though not required)

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.

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)

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); });

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());

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