Chapter 5.1☕ 25 min read

Project 1: Biryani Order API — Complete CRUD

This is where everything comes together. You'll build a complete Biryani Order API using MVC architecture — the same pattern used by Swiggy, Uber, and Netflix in their Node.js services.

01Project Overview — Biryani Order API Banayenge

Welcome to Stage 5 — Projects! In this stage, you'll build real-world Node.js projects that combine everything you've learned: Express, MongoDB, Mongoose, JWT Auth, File Upload, Email, Validation, and Error Handling.

Project 1: Biryani Order API is a complete CRUD (Create, Read, Update, Delete) backend for a biryani ordering system. This is the foundation of all food delivery apps like Swiggy and Zomato.

What we'll build:

  • Full REST API with Express — GET, POST, PUT, DELETE endpoints
  • Mongoose models for Biryani, Order, and User
  • Controllers with business logic
  • Middleware for auth, error handling, and validation
  • Centralized config and error handling

MVC Architecture: Our project follows the Model-View-Controller pattern (minus the View since this is an API):

  • Models — Define data structure (Mongoose schemas)
  • Controllers — Handle business logic (what to do with the data)
  • Routes — Map URLs to controllers (which URL triggers which logic)
  • Middleware — Process requests before/after controllers (auth, validation)

By the end of this chapter, you'll have a working API that can create biryani items, list them, update prices, and delete items — all with proper error handling and validation.

02Project Structure — MVC Architecture Setup

A proper project structure is what separates a professional Node.js app from a messy script. Let's set up the MVC architecture.

Project folder structure:

biryani-order-api/\n├── server.js                 # Entry point — wires everything together\n├── package.json\n├── .env                      # Environment variables\n├── .gitignore\n├── config/\n│   └── index.js              # Centralized configuration\n├── models/\n│   ├── Biryani.js            # Biryani schema\n│   ├── Order.js              # Order schema\n│   └── User.js               # User schema\n├── controllers/\n│   ├── biryani.controller.js # Biryani CRUD logic\n│   ├── order.controller.js   # Order processing logic\n│   └── auth.controller.js    # Auth logic\n├── routes/\n│   ├── biryani.routes.js     # Biryani endpoints\n│   ├── order.routes.js       # Order endpoints\n│   └── auth.routes.js        # Auth endpoints\n├── middleware/\n│   ├── auth.middleware.js     # JWT protection\n│   ├── validation.middleware.js # Input validation\n│   └── error.middleware.js    # Error handler\n└── utils/\n    ├── AppError.js           # Custom error class\n    └── asyncHandler.js       # Async error wrapper

Why this structure?

  • Separation of concerns — Each layer has a single responsibility
  • Scalability — Add new features by adding new files, not modifying existing ones
  • Testability — Each module can be tested independently
  • Team collaboration — Multiple devs can work on different files simultaneously

Initialize the project:

mkdir biryani-order-api && cd biryani-order-api\nnpm init -y\nnpm install express mongoose dotenv bcryptjs jsonwebtoken express-validator nodemailer multer\nnpm install --save-dev nodemon

package.json scripts:

{\n  "name": "biryani-order-api",\n  "version": "1.0.0",\n  "type": "module",\n  "scripts": {\n    "start": "node server.js",\n    "dev": "nodemon server.js"\n  }\n}

Note: "type": "module" enables ES Module syntax (import/export) instead of CommonJS (require/module.exports).

03server.js — Sab Kuch Ek Saath Jodna

server.js is the entry point of your application. It imports all modules, configures middleware, mounts routes, and starts the server. Think of it as the main switchboard of your kitchen — it connects everything.

// server.js — Biryani Order API Entry Point
import 'dotenv/config';
import express from 'express';
import mongoose from 'mongoose';
import config from './config/index.js';
import { notFoundHandler, errorHandler } from './middleware/error.middleware.js';

// Import routes
import biryaniRoutes from './routes/biryani.routes.js';
import orderRoutes from './routes/order.routes.js';
import authRoutes from './routes/auth.routes.js';

const app = express();

// ─── Middleware ──────────────────────────────────────────────
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Request logger
app.use((req, res, next) => {
  console.log('[' + new Date().toISOString() + '] ' + req.method + ' ' + req.originalUrl);
  next();
});

// ─── Routes ─────────────────────────────────────────────────
app.get('/api/health', (req, res) => {
  res.json({ status: 'OK', message: '🍔 Biryani API is running!' });
});

app.use('/api/auth', authRoutes);
app.use('/api/biryanis', biryaniRoutes);
app.use('/api/orders', orderRoutes);

// ─── Error Handling ─────────────────────────────────────────
app.use(notFoundHandler);
app.use(errorHandler);

// ─── Start Server ───────────────────────────────────────────
const startServer = async () => {
  try {
    await mongoose.connect(config.mongo.uri);
    console.log('✅ MongoDB connected: ' + mongoose.connection.host);

    app.listen(config.port, () => {
      console.log('🚀 Biryani Order API running on port ' + config.port);
      console.log('📍 Environment: ' + config.env);
    });
  } catch (error) {
    console.error('❌ Failed to start server:', error.message);
    process.exit(1);
  }
};

startServer();

What server.js does:

  1. Loads environment variables with dotenv
  2. Creates Express app and configures global middleware (JSON parser, logger)
  3. Mounts route groups at their base paths (/api/auth, /api/biryanis, /api/orders)
  4. Adds 404 and error handlers at the end (order matters!)
  5. Connects to MongoDB and starts listening

Key design decisions:

  • Error handlers are ALWAYS the last middleware — never before routes
  • dotenv is imported first (import 'dotenv/config') before any module that reads env vars
  • Server startup is wrapped in an async function for proper error handling
  • Health check endpoint at /api/health for monitoring
04CRUD Operations — Create, Read, Update, Delete Biryani

Now let's implement the CRUD operations for our Biryani model. CRUD stands for Create, Read, Update, Delete — the four basic operations of persistent storage.

Biryani Model:

// models/Biryani.js — Biryani Mongoose Schema
import mongoose from 'mongoose';

const biryaniSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Biryani ka name daalo bhai!'],
    trim: true,
    maxlength: [100, 'Name itna lamba nahi hota!']
  },
  price: {
    type: Number,
    required: [true, 'Price daalo bhai!'],
    min: [10, 'Itni sasti biryani? Min Rs.10'],
    max: [5000, 'Itni mehngi biryani? Max Rs.5000']
  },
  category: {
    type: String,
    required: true,
    enum: ['Chicken', 'Mutton', 'Veg', 'Fish', 'Beef']
  },
  description: { type: String, default: 'Delicious Hyderabadi biryani!' },
  isAvailable: { type: Boolean, default: true },
  rating: { type: Number, default: 4.5, min: 1, max: 5 },
  photoUrl: { type: String, default: '' }
}, { timestamps: true });

export default mongoose.model('Biryani', biryaniSchema);

Biryani Controller — Complete CRUD:

// controllers/biryani.controller.js — Biryani CRUD Logic
import Biryani from '../models/Biryani.js';
import AppError from '../utils/AppError.js';
import asyncHandler from '../utils/asyncHandler.js';

// GET /api/biryanis — Read all biryanis
export const getAllBiryanis = asyncHandler(async (req, res) => {
  const { category, page = 1, limit = 10 } = req.query;

  const query = {};
  if (category) query.category = category;

  const biryanis = await Biryani.find(query)
    .skip((page - 1) * limit)
    .limit(limit)
    .sort('-createdAt');

  const total = await Biryani.countDocuments(query);

  res.json({
    success: true,
    count: biryanis.length,
    total,
    page: parseInt(page),
    totalPages: Math.ceil(total / limit),
    data: biryanis
  });
});

// GET /api/biryanis/:id — Read single biryani
export const getBiryani = asyncHandler(async (req, res, next) => {
  const biryani = await Biryani.findById(req.params.id);
  if (!biryani) {
    throw new AppError('Biryani not found! Yeh biryani exist nahi karti 🍔', 404);
  }
  res.json({ success: true, data: biryani });
});

// POST /api/biryanis — Create new biryani
export const createBiryani = asyncHandler(async (req, res) => {
  const biryani = await Biryani.create(req.body);
  res.status(201).json({
    success: true,
    message: biryani.name + ' menu mein add ho gaya! 🎉',
    data: biryani
  });
});

// PUT /api/biryanis/:id — Update biryani
export const updateBiryani = asyncHandler(async (req, res, next) => {
  const biryani = await Biryani.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  );
  if (!biryani) {
    throw new AppError('Biryani not found! Update nahi kar sakte 🍔', 404);
  }
  res.json({
    success: true,
    message: biryani.name + ' updated successfully! ✏️',
    data: biryani
  });
});

// DELETE /api/biryanis/:id — Delete biryani
export const deleteBiryani = asyncHandler(async (req, res, next) => {
  const biryani = await Biryani.findByIdAndDelete(req.params.id);
  if (!biryani) {
    throw new AppError('Biryani not found! Delete nahi kar sakte 🍔', 404);
  }
  res.json({
    success: true,
    message: biryani.name + ' delete ho gaya! 🗑️'
  });
});

Biryani Routes:

// routes/biryani.routes.js — Biryani CRUD Routes
import express from 'express';
import {
  getAllBiryanis, getBiryani,
  createBiryani, updateBiryani, deleteBiryani
} from '../controllers/biryani.controller.js';
import { protect, authorize } from '../middleware/auth.middleware.js';

const router = express.Router();

// Public routes
router.get('/', getAllBiryanis);
router.get('/:id', getBiryani);

// Protected routes (admin/restaurant only)
router.post('/', protect, authorize('admin', 'restaurant'), createBiryani);
router.put('/:id', protect, authorize('admin', 'restaurant'), updateBiryani);
router.delete('/:id', protect, authorize('admin'), deleteBiryani);

export default router;

The CRUD pattern is universal: Every resource in a REST API follows the same CRUD pattern. Once you learn it for Biryani, you can apply it to Users, Orders, Restaurants, or any other resource.

05Complete API — Testing the Full Flow

Let's test our complete API flow — from creating biryani to placing orders.

1. Create a biryani (POST /api/biryanis):

curl -X POST http://localhost:3000/api/biryanis \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer " \
  -d '{
    "name": "Chicken Biryani",
    "price": 250,
    "category": "Chicken",
    "description": "Hyderabadi dum biryani with tender chicken"
  }'

# Response:
# { "success": true, "message": "Chicken Biryani menu mein add ho gaya! 🎉", "data": {...} }

2. List all biryanis (GET /api/biryanis):

curl http://localhost:3000/api/biryanis?category=Chicken&page=1&limit=5

# Response:
# { "success": true, "count": 3, "total": 10, "page": 1, "totalPages": 2, "data": [...] }

3. Update biryani price (PUT /api/biryanis/:id):

curl -X PUT http://localhost:3000/api/biryanis/abc123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer " \
  -d '{"price": 299}'

4. Delete biryani (DELETE /api/biryanis/:id):

curl -X DELETE http://localhost:3000/api/biryanis/abc123 \
  -H "Authorization: Bearer "

Complete API Endpoints:

MethodEndpointDescriptionAuth
GET/api/healthHealth checkNone
POST/api/auth/registerRegister userNone
POST/api/auth/loginLogin userNone
GET/api/biryanisList all biryanisNone
GET/api/biryanis/:idGet single biryaniNone
POST/api/biryanisCreate biryaniAdmin
PUT/api/biryanis/:idUpdate biryaniAdmin
DELETE/api/biryanis/:idDelete biryaniAdmin
POST/api/ordersPlace orderUser
GET/api/orders/meMy ordersUser

Key Takeaways

  • ✅ MVC Architecture: Models (data) → Controllers (logic) → Routes (URL mapping)
  • ✅ project structure: server.js → config/ → models/ → controllers/ → routes/ → middleware/ → utils/
  • ✅ server.js wires everything together: imports routes, mounts middleware, starts server
  • ✅ CRUD = Create (POST), Read (GET), Update (PUT), Delete (DELETE)
  • ✅ Controllers handle business logic; Routes map URLs to controllers; Middleware processes requests
  • ✅ Pagination with skip() and limit() for listing endpoints
  • ✅ Always use asyncHandler to catch async errors without try-catch in every route
  • ✅ Circular dependencies (A imports B imports A) cause hard-to-debug bugs — keep import chain one-way
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