Lesson 13 of 20

Building a REST API

CRUD Operations with Express

CRUD stands for Create, Read, Update, and Delete — the four basic operations you perform on data. Building a REST API means mapping these operations to HTTP methods and route handlers.

We will build a complete CRUD API for a 'books' resource. For now, we will store data in an array (in-memory). Later lessons will show how to connect to a real database.

Example
const express = require('express');
const app = express();
app.use(express.json());

// In-memory data store
let books = [
  { id: 1, title: '1984', author: 'George Orwell' },
  { id: 2, title: 'Dune', author: 'Frank Herbert' }
];
let nextId = 3;

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

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

// POST — Create 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 book = { id: nextId++, title, author };
  books.push(book);
  res.status(201).json(book);
});

// PUT — Update a book
app.put('/api/books/:id', (req, res) => {
  const book = books.find(b => b.id === parseInt(req.params.id));
  if (!book) 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' });
  }
  book.title = title;
  book.author = author;
  res.json(book);
});

// DELETE — Remove a book
app.delete('/api/books/:id', (req, res) => {
  const index = books.findIndex(b => b.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Book not found' });
  books.splice(index, 1);
  res.status(204).send();
});

app.listen(3000, () => console.log('API running on port 3000'));
  • GET /api/books — Read all books
  • GET /api/books/:id — Read a single book
  • POST /api/books — Create a new book
  • PUT /api/books/:id — Update an existing book
  • DELETE /api/books/:id — Delete a book
Notes
  • This uses in-memory storage — data is lost when the server restarts. In production, you would use a database like MongoDB or PostgreSQL.

Request Body Parsing and Validation

When clients send data in POST or PUT requests, you need to parse and validate it. Express's express.json() middleware parses JSON bodies, but you should always validate the data before using it.

Basic validation checks for required fields, correct data types, and reasonable values. For complex validation, libraries like Joi or express-validator are recommended.

Example
const express = require('express');
const app = express();
app.use(express.json());

// Simple validation function
function validateBook(body) {
  const errors = [];
  if (!body.title || typeof body.title !== 'string') {
    errors.push('Title is required and must be a string');
  }
  if (!body.author || typeof body.author !== 'string') {
    errors.push('Author is required and must be a string');
  }
  if (body.year && (typeof body.year !== 'number' || body.year < 0)) {
    errors.push('Year must be a positive number');
  }
  return errors;
}

app.post('/api/books', (req, res) => {
  const errors = validateBook(req.body);
  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }

  const book = {
    id: Date.now(),
    title: req.body.title.trim(),
    author: req.body.author.trim(),
    year: req.body.year || null
  };

  res.status(201).json(book);
});

// Test with curl:
// curl -X POST http://localhost:3000/api/books \
//   -H "Content-Type: application/json" \
//   -d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'
Notes
  • Always validate and sanitize user input. Never trust data from the client — trim strings, check types, and validate ranges.