Skip to main content

Building a REST API

APIs (Application Programming Interfaces) are the backbone of modern web development. They allow different applications to communicate with each other—whether it’s your frontend talking to your backend, or third-party services integrating with your app.

What is REST?

REST (Representational State Transfer) is an architectural style for designing networked applications. It’s not a protocol or standard, but a set of constraints that, when followed, create scalable and maintainable APIs.

Core Principles of REST

PrincipleDescription
StatelessEach request contains all the information needed to process it. The server doesn’t store client state between requests.
Client-ServerThe client and server are separate, allowing them to evolve independently.
Uniform InterfaceResources are accessed using standard HTTP methods (GET, POST, PUT, DELETE).
Resource-BasedEverything is a resource (users, products, orders) identified by URLs.

HTTP Methods and Their Meanings

MethodPurposeExample
GETRetrieve dataGet all users, get user by ID
POSTCreate new dataCreate a new user
PUTUpdate existing data (replace)Update a user’s information
PATCHPartial updateUpdate only user’s email
DELETERemove dataDelete a user

Why REST?

  • Universal: Works with any programming language that can make HTTP requests
  • Scalable: Statelessness allows easy horizontal scaling
  • Cacheable: GET requests can be cached for better performance
  • Simple: Uses familiar HTTP concepts developers already know
In this chapter, we will build a simple REST API to manage a list of items, demonstrating these principles in action.

Setup

Create a new file server.js and install express if you haven’t.
const express = require('express');
const app = express();

app.use(express.json()); // Middleware to parse JSON bodies

const PORT = process.env.PORT || 5000;

// Mock Database
let items = [
  { id: 1, name: 'Item One' },
  { id: 2, name: 'Item Two' }
];

app.listen(PORT, () => console.log(`Server started on port ${PORT}`));

GET: Read Data

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

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

  if (found) {
    res.json(items.filter(item => item.id === parseInt(req.params.id)));
  } else {
    res.status(404).json({ msg: `No item with the id of ${req.params.id}` });
  }
});

POST: Create Data

app.post('/api/items', (req, res) => {
  const newItem = {
    id: items.length + 1, // Simple ID generation
    name: req.body.name
  };

  if (!newItem.name) {
    return res.status(400).json({ msg: 'Please include a name' });
  }

  items.push(newItem);
  res.json(items); // Return all items (or just the new one)
});

PUT: Update Data

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

  if (found) {
    const updItem = req.body;
    items.forEach(item => {
      if (item.id === parseInt(req.params.id)) {
        item.name = updItem.name ? updItem.name : item.name;
        res.json({ msg: 'Item updated', item });
      }
    });
  } else {
    res.status(404).json({ msg: `No item with the id of ${req.params.id}` });
  }
});

DELETE: Remove Data

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

  if (found) {
    items = items.filter(item => item.id !== parseInt(req.params.id));
    res.json({ msg: 'Item deleted', items });
  } else {
    res.status(404).json({ msg: `No item with the id of ${req.params.id}` });
  }
});

Testing with Postman / Insomnia

Since we don’t have a frontend yet, use tools like Postman or Insomnia to test your API.
  1. GET http://localhost:5000/api/items -> Should return list.
  2. POST http://localhost:5000/api/items with JSON body {"name": "New Item"} -> Should add item.
  3. PUT http://localhost:5000/api/items/1 with JSON body {"name": "Updated Item"} -> Should update item 1.
  4. DELETE http://localhost:5000/api/items/1 -> Should delete item 1.

Summary

  • REST APIs use standard HTTP methods for CRUD operations
  • GET for retrieving data
  • POST for creating data
  • PUT (or PATCH) for updating data
  • DELETE for removing data
  • Always validate input and handle errors

Request Validation with Joi

Never trust client input! Validate all requests:
npm install joi
const Joi = require('joi');

// Define schema
const itemSchema = Joi.object({
  name: Joi.string().min(3).max(50).required(),
  description: Joi.string().max(500),
  price: Joi.number().positive().precision(2),
  category: Joi.string().valid('electronics', 'clothing', 'food'),
  tags: Joi.array().items(Joi.string()),
  inStock: Joi.boolean().default(true)
});

// Validation middleware
const validate = (schema) => (req, res, next) => {
  const { error, value } = schema.validate(req.body, { abortEarly: false });
  
  if (error) {
    const errors = error.details.map(detail => ({
      field: detail.path.join('.'),
      message: detail.message
    }));
    return res.status(400).json({ success: false, errors });
  }
  
  req.body = value; // Use validated/sanitized values
  next();
};

// Usage
app.post('/api/items', validate(itemSchema), (req, res) => {
  // req.body is now validated and sanitized
  res.status(201).json(req.body);
});

API Response Standards

Maintain consistent response formats:
// Success responses
const sendSuccess = (res, data, statusCode = 200) => {
  res.status(statusCode).json({
    success: true,
    data
  });
};

// Paginated responses
const sendPaginated = (res, data, page, limit, total) => {
  res.json({
    success: true,
    data,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit),
      hasNext: page * limit < total,
      hasPrev: page > 1
    }
  });
};

// Error responses
const sendError = (res, message, statusCode = 400) => {
  res.status(statusCode).json({
    success: false,
    error: message
  });
};

Pagination

app.get('/api/items', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  
  // Database query with pagination
  const items = await Item.find()
    .skip(skip)
    .limit(limit)
    .sort({ createdAt: -1 });
  
  const total = await Item.countDocuments();
  
  res.json({
    success: true,
    data: items,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  });
});

Filtering and Sorting

app.get('/api/items', async (req, res) => {
  // Build filter object
  const filter = {};
  
  if (req.query.category) {
    filter.category = req.query.category;
  }
  if (req.query.minPrice) {
    filter.price = { ...filter.price, $gte: parseFloat(req.query.minPrice) };
  }
  if (req.query.maxPrice) {
    filter.price = { ...filter.price, $lte: parseFloat(req.query.maxPrice) };
  }
  if (req.query.search) {
    filter.name = { $regex: req.query.search, $options: 'i' };
  }
  
  // Build sort object
  const sort = {};
  if (req.query.sortBy) {
    const order = req.query.order === 'desc' ? -1 : 1;
    sort[req.query.sortBy] = order;
  }
  
  const items = await Item.find(filter).sort(sort);
  res.json({ success: true, data: items });
});

// Example: GET /api/items?category=electronics&minPrice=100&sortBy=price&order=desc

HTTP Status Codes Reference

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE
400Bad RequestInvalid input, validation error
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn’t exist
409ConflictDuplicate entry, version conflict
422Unprocessable EntityValid JSON but semantic errors
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error

API Versioning

const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Or using headers
app.use('/api', (req, res, next) => {
  const version = req.headers['api-version'] || 'v1';
  req.apiVersion = version;
  next();
});

Complete REST Controller Example

// controllers/itemController.js
class ItemController {
  // GET /api/items
  async getAll(req, res, next) {
    try {
      const { page = 1, limit = 10, sort = '-createdAt' } = req.query;
      
      const items = await Item.find()
        .sort(sort)
        .skip((page - 1) * limit)
        .limit(parseInt(limit));
        
      const total = await Item.countDocuments();
      
      res.json({
        success: true,
        count: items.length,
        data: items,
        pagination: { page: parseInt(page), limit: parseInt(limit), total }
      });
    } catch (error) {
      next(error);
    }
  }

  // GET /api/items/:id
  async getById(req, res, next) {
    try {
      const item = await Item.findById(req.params.id);
      
      if (!item) {
        return res.status(404).json({
          success: false,
          error: 'Item not found'
        });
      }
      
      res.json({ success: true, data: item });
    } catch (error) {
      next(error);
    }
  }

  // POST /api/items
  async create(req, res, next) {
    try {
      const item = await Item.create(req.body);
      res.status(201).json({ success: true, data: item });
    } catch (error) {
      next(error);
    }
  }

  // PUT /api/items/:id
  async update(req, res, next) {
    try {
      const item = await Item.findByIdAndUpdate(
        req.params.id,
        req.body,
        { new: true, runValidators: true }
      );
      
      if (!item) {
        return res.status(404).json({
          success: false,
          error: 'Item not found'
        });
      }
      
      res.json({ success: true, data: item });
    } catch (error) {
      next(error);
    }
  }

  // DELETE /api/items/:id
  async delete(req, res, next) {
    try {
      const item = await Item.findByIdAndDelete(req.params.id);
      
      if (!item) {
        return res.status(404).json({
          success: false,
          error: 'Item not found'
        });
      }
      
      res.status(204).send();
    } catch (error) {
      next(error);
    }
  }
}

module.exports = new ItemController();