Chapter 2.5☕ 18 min read

REST API Design — Poora RESTful Biryani API

REST (Representational State Transfer) is a standard way to design APIs. It uses HTTP methods to perform CRUD operations on resources.

01What is REST? — API Design Basics

REST (Representational State Transfer) is an architectural style for designing networked applications. It's not a framework or library — it's a set of guidelines on how to structure your API.

Think of REST like Swiggy's internal API design:

  • Resources — Everything is a resource: restaurants, menu items, orders, users
  • URLs represent resources/restaurants, /menus, /orders
  • HTTP methods represent actions — GET to read, POST to create, PUT to update, DELETE to remove
  • Stateless — Each request contains all the information needed (no server-side sessions)
  • Structured responses — Consistent JSON format with proper status codes

REST API URL patterns for a Biryani API:

GET    /biryani        → List all biryanis
GET    /biryani/1      → Get biryani with id 1
POST   /biryani        → Create a new biryani
PUT    /biryani/1      → Update biryani with id 1
DELETE /biryani/1      → Delete biryani with id 1

Notice the pattern: plural nouns for resources (/biryani, /orders, /users), IDs to identify specific resources, and HTTP methods for actions. No verbs in URLs (/createBiryani, /deleteOrder — WRONG!).

02HTTP Methods — CRUD Operations

CRUD stands for Create, Read, Update, Delete — the four basic operations for any data store. In REST, these map to HTTP methods:

OperationHTTP MethodURL PatternStatus CodeSwiggy Analogy
CreatePOST/biryani201 CreatedAdding a new biryani to menu
Read (all)GET/biryani200 OKViewing full menu
Read (one)GET/biryani/1200 OKViewing one biryani details
Update (full)PUT/biryani/1200 OKReplacing recipe completely
Update (partial)PATCH/biryani/1200 OKChanging only the price
DeleteDELETE/biryani/1200 OK / 204 No ContentRemoving item from menu

Example of each method:

app.post('/biryani', (req, res) => {
  const newBiryani = { id: nextId++, ...req.body };
  menu.push(newBiryani);
  res.status(201).json({ success: true, data: newBiryani });
});

app.put('/biryani/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = menu.findIndex(b => b.id === id);
  if (index === -1) {
    return res.status(404).json({ success: false, message: 'Not found' });
  }
  menu[index] = { id, ...req.body };
  res.status(200).json({ success: true, data: menu[index] });
});

app.delete('/biryani/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = menu.findIndex(b => b.id === id);
  if (index === -1) {
    return res.status(404).json({ success: false, message: 'Not found' });
  }
  menu.splice(index, 1);
  res.status(200).json({ success: true, message: 'Biryani deleted!' });
});

PUT vs PATCH: PUT replaces the ENTIRE resource. PATCH updates only the fields that are sent. If you send only { "price": 300 } via PUT, the item will lose its name and spice. Use PATCH for partial updates.

03Structured JSON Responses — { success, data, message }

A structured JSON response format makes your API predictable and easy to consume. Every response should follow the same pattern:

// Success response
{
  "success": true,
  "data": { ... },           // The actual data (object or array)
  "message": "Optional message",
  "count": 5                  // For arrays — how many items
}

// Error response
{
  "success": false,
  "message": "What went wrong",
  "error": "Details (only in development)"
}

Consistent response helper:

function sendSuccess(res, data, message, status = 200) {
  const response = { success: true };
  if (message) response.message = message;
  if (Array.isArray(data)) {
    response.count = data.length;
    response.data = data;
  } else {
    response.data = data;
  }
  return res.status(status).json(response);
}

function sendError(res, message, status = 400, error = null) {
  const response = {
    success: false,
    message
  };
  if (error && process.env.NODE_ENV !== 'production') {
    response.error = error.message || error;
  }
  return res.status(status).json(response);
}

Using the helpers:

app.get('/biryani', (req, res) => {
  return sendSuccess(res, menu, 'Menu loaded successfully!');
});

app.get('/biryani/:id', (req, res) => {
  const item = menu.find(b => b.id === Number(req.params.id));
  if (!item) {
    return sendError(res, 'Biryani not found!', 404);
  }
  return sendSuccess(res, item);
});

app.post('/biryani', (req, res) => {
  if (!req.body.name) {
    return sendError(res, 'Name is required!', 400);
  }
  const newItem = { id: nextId++, ...req.body };
  menu.push(newItem);
  return sendSuccess(res, newItem, 'Biryani created!', 201);
});

This pattern is used by Swiggy, Uber, and most production APIs. Consistent format = frontend teams can write generic parsers instead of case-by-case handling.

04Express Router — RESTful Biryani API

Let's build a complete RESTful Biryani API using Express Router — the same pattern Swiggy's backend uses for organizing code.

routes/biryani.routes.js — Full CRUD:

import { Router } from 'express';

const router = Router();

// In-memory data store
let menu = [
  { id: 1, name: 'Chicken Biryani', price: 250, spice: 'Medium' },
  { id: 2, name: 'Mutton Biryani', price: 350, spice: 'High' },
  { id: 3, name: 'Veg Biryani', price: 200, spice: 'Low' }
];
let nextId = 4;

// Helper functions
function sendSuccess(res, data, message, status = 200) {
  const response = { success: true };
  if (message) response.message = message;
  if (Array.isArray(data)) { response.count = data.length; response.data = data; }
  else { response.data = data; }
  return res.status(status).json(response);
}

function sendError(res, message, status = 400) {
  return res.status(status).json({ success: false, message });
}

// GET /biryani — Read all
router.get('/', (req, res) => {
  return sendSuccess(res, menu, 'Biryani menu loaded!');
});

// GET /biryani/:id — Read one
router.get('/:id', (req, res) => {
  const item = menu.find(b => b.id === Number(req.params.id));
  if (!item) return sendError(res, 'Biryani not found!', 404);
  return sendSuccess(res, item);
});

// POST /biryani — Create
router.post('/', (req, res) => {
  const { name, price, spice } = req.body;
  if (!name || !price) return sendError(res, 'Name and price required!', 400);

  const newItem = { id: nextId++, name, price: Number(price), spice: spice || 'Medium' };
  menu.push(newItem);
  return sendSuccess(res, newItem, 'Biryani added!', 201);
});

// PUT /biryani/:id — Update (full replacement)
router.put('/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = menu.findIndex(b => b.id === id);
  if (index === -1) return sendError(res, 'Biryani not found!', 404);

  const { name, price, spice } = req.body;
  if (!name || !price) return sendError(res, 'Name and price required!', 400);

  menu[index] = { id, name, price: Number(price), spice: spice || menu[index].spice };
  return sendSuccess(res, menu[index], 'Biryani updated!');
});

// DELETE /biryani/:id — Delete
router.delete('/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = menu.findIndex(b => b.id === id);
  if (index === -1) return sendError(res, 'Biryani not found!', 404);

  menu.splice(index, 1);
  return sendSuccess(res, null, 'Biryani deleted!');
});

export default router;

server.js — Mount the router:

import express from 'express';
import biryaniRoutes from './routes/biryani.routes.js';

const app = express();
app.use(express.json());

// Mount the router at /biryani
app.use('/biryani', biryaniRoutes);

// 404 handler
app.use((req, res) => {
  res.status(404).json({ success: false, message: 'Route not found' });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ success: false, message: 'Server error' });
});

app.listen(3000);

This is exactly how production APIs are organized. Each resource gets its own router file. The server just mounts them.

05Pagination & Error Handling — Production Ready

Real APIs need pagination (what if you have 10,000 biryanis?) and robust error handling (what if the database crashes?).

Pagination implementation:

// GET /biryani?page=1&limit=5&sort=asc
router.get('/', (req, res) => {
  let result = [...menu];
  const page = Math.max(1, Number(req.query.page) || 1);
  const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 10));
  const sort = req.query.sort || 'asc';

  // Sort by price
  result.sort((a, b) => sort === 'desc' ? b.price - a.price : a.price - b.price);

  // Calculate pagination
  const totalItems = result.length;
  const totalPages = Math.ceil(totalItems / limit);
  const start = (page - 1) * limit;
  const paginatedItems = result.slice(start, start + limit);

  res.json({
    success: true,
    data: paginatedItems,
    pagination: {
      page,
      limit,
      totalItems,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1
    }
  });
});

Complete error handling strategy:

// 400 — Validation errors (client sent bad data)
if (!req.body.name) {
  return res.status(400).json({
    success: false,
    message: 'Validation error: name is required',
    field: 'name'
  });
}

// 404 — Resource not found
if (!item) {
  return res.status(404).json({
    success: false,
    message: 'Resource not found'
  });
}

// 409 — Conflict (duplicate, already exists)
if (menu.find(b => b.name === req.body.name)) {
  return res.status(409).json({
    success: false,
    message: 'Biryani with this name already exists!'
  });
}

// 500 — Server error (catch-all in error middleware)
app.use((err, req, res, next) => {
  console.error('🔥 Unhandled error:', err);
  res.status(500).json({
    success: false,
    message: 'Internal server error',
    ...(process.env.NODE_ENV === 'development' && { error: err.message })
  });
});

What a production REST API looks like:

EndpointSuccessError Cases
GET /biryani?page=1&limit=5200 + paginated array500 (server crash)
GET /biryani/42200 + single object404 (not found), 500
POST /biryani201 + created object400 (validation), 409 (duplicate), 500
PUT /biryani/42200 + updated object404 (not found), 400 (validation), 500
DELETE /biryani/42200 + success message404 (not found), 500

Stage 2 Complete! 🎉 You now know: Express routing (GET/POST), params and query strings, middleware pipeline, and REST API design. Next up: Stage 3 — Databases and CRUD!

Key Takeaways

  • ✅ REST = Representational State Transfer. Uses HTTP methods as verbs, URLs as nouns.
  • ✅ CRUD mapping: POST=Create, GET=Read, PUT=Update (full), PATCH=Update (partial), DELETE=Delete.
  • ✅ Structured JSON: { success: boolean, data: ..., message: "...", count: number }
  • ✅ Use Express Router (express.Router()) to organize routes per resource.
  • ✅ Always return proper status codes: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 409 Conflict, 500 Server Error.
  • ✅ Pagination: page + limit params, return { pagination: { page, limit, totalItems, totalPages } }
  • ✅ Never put verbs in URLs (/createBiryani) — let HTTP methods do the talking.
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase