NODE.JS DEVELOPMENT:Day 6: Building a REST API with Express

Mastering day 6: building a rest api with express concepts and implementation.

Building a REST API with Express.js

A REST API (Representational State Transfer Application Programming Interface) is a web service that uses HTTP methods to allow clients to interact with server resources. Express.js makes building REST APIs straightforward.

REST Principles

  1. Stateless: Every request contains all information needed to process it
  2. Uniform Interface: Standard HTTP methods and status codes
  3. Resource-Based: URLs identify resources, not actions
  4. Client-Server: Clear separation between frontend and backend

HTTP Methods and Their Meanings

MethodPurposeBody?Idempotent?
GETRead resourceNoYes
POSTCreate resourceYesNo
PUTReplace resourceYesYes
PATCHPartial updateYesNo
DELETEDelete resourceNoYes

HTTP Status Codes

CodeMeaning
200 OKSuccess
201 CreatedResource created
204 No ContentSuccess, no body
400 Bad RequestInvalid input
401 UnauthorizedAuth required
403 ForbiddenInsufficient permissions
404 Not FoundResource doesn't exist
409 ConflictDuplicate resource
422 UnprocessableValidation failure
500 Internal Server ErrorServer bug

Building a Books REST API

const express = require('express');
const router = express.Router();

// In-memory data store (replace with database in production)
let books = [
  { id: 1, title: 'Clean Code', author: 'Robert Martin', year: 2008 },
  { id: 2, title: 'You Don't Know JS', author: 'Kyle Simpson', year: 2015 },
];
let nextId = 3;

// GET /api/books — list all books (with optional filter)
router.get('/', (req, res) => {
  const { author, year } = req.query;
  let result = books;
  
  if (author) result = result.filter(b => b.author.toLowerCase().includes(author.toLowerCase()));
  if (year) result = result.filter(b => b.year === parseInt(year));
  
  res.json({ data: result, count: result.length });
});

// GET /api/books/:id — get single book
router.get('/: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({ data: book });
});

// POST /api/books — create book
router.post('/', (req, res) => {
  const { title, author, year } = req.body;
  
  // Validation
  if (!title || !author) {
    return res.status(422).json({ error: 'title and author are required' });
  }
  
  const book = { id: nextId++, title, author, year: year || null };
  books.push(book);
  res.status(201).json({ data: book });
});

// PATCH /api/books/:id — partial update
router.patch('/: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[index] = { ...books[index], ...req.body, id: books[index].id }; // id immutable
  res.json({ data: books[index] });
});

// DELETE /api/books/:id — delete book
router.delete('/: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();  // 204 No Content
});

module.exports = router;

Mounting the Router

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

app.use(express.json());
app.use('/api/books', require('./routes/books'));

app.listen(3000);

Input Validation with express-validator

const { body, param, validationResult } = require('express-validator');

const bookValidation = [
  body('title').trim().notEmpty().withMessage('Title is required').isLength({ max: 200 }),
  body('author').trim().notEmpty().withMessage('Author is required'),
  body('year').optional().isInt({ min: 1000, max: 2100 }).withMessage('Invalid year'),
];

router.post('/', bookValidation, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
  // Process valid request...
});

Error Handling Middleware

// Catch-all error handler — must have 4 parameters
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  // Handle specific error types
  if (err.name === 'ValidationError') {
    return res.status(422).json({ error: err.message });
  }
  if (err.name === 'CastError') {
    return res.status(400).json({ error: 'Invalid ID format' });
  }
  
  // Default error response
  const status = err.status || 500;
  res.status(status).json({
    error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message
  });
});

// 404 handler — after all routes
app.use((req, res) => {
  res.status(404).json({ error: `Route ${req.method} ${req.path} not found` });
});

Hands-on Examples

Testing the Books API with curl

# Create a book
curl -X POST http://localhost:3000/api/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Node.js Design Patterns","author":"Mario Casciaro","year":2020}'

# Get all books
curl http://localhost:3000/api/books

# Get by id
curl http://localhost:3000/api/books/1

# Update
curl -X PATCH http://localhost:3000/api/books/1 \
  -H "Content-Type: application/json" \
  -d '{"year":2020}'

# Delete
curl -X DELETE http://localhost:3000/api/books/1

curl is the standard command-line tool for testing HTTP APIs. The -X flag sets the method, -H adds headers, and -d sends the request body.