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.
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!).
CRUD stands for Create, Read, Update, Delete — the four basic operations for any data store. In REST, these map to HTTP methods:
| Operation | HTTP Method | URL Pattern | Status Code | Swiggy Analogy |
|---|---|---|---|---|
| Create | POST | /biryani | 201 Created | Adding a new biryani to menu |
| Read (all) | GET | /biryani | 200 OK | Viewing full menu |
| Read (one) | GET | /biryani/1 | 200 OK | Viewing one biryani details |
| Update (full) | PUT | /biryani/1 | 200 OK | Replacing recipe completely |
| Update (partial) | PATCH | /biryani/1 | 200 OK | Changing only the price |
| Delete | DELETE | /biryani/1 | 200 OK / 204 No Content | Removing 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.
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.
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.
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:
| Endpoint | Success | Error Cases |
|---|---|---|
| GET /biryani?page=1&limit=5 | 200 + paginated array | 500 (server crash) |
| GET /biryani/42 | 200 + single object | 404 (not found), 500 |
| POST /biryani | 201 + created object | 400 (validation), 409 (duplicate), 500 |
| PUT /biryani/42 | 200 + updated object | 404 (not found), 400 (validation), 500 |
| DELETE /biryani/42 | 200 + success message | 404 (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.
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login