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.
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.
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).
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:
- Loads environment variables with
dotenv - Creates Express app and configures global middleware (JSON parser, logger)
- Mounts route groups at their base paths (
/api/auth,/api/biryanis,/api/orders) - Adds 404 and error handlers at the end (order matters!)
- 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/healthfor monitoring
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.
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:
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/health | Health check | None |
| POST | /api/auth/register | Register user | None |
| POST | /api/auth/login | Login user | None |
| GET | /api/biryanis | List all biryanis | None |
| GET | /api/biryanis/:id | Get single biryani | None |
| POST | /api/biryanis | Create biryani | Admin |
| PUT | /api/biryanis/:id | Update biryani | Admin |
| DELETE | /api/biryanis/:id | Delete biryani | Admin |
| POST | /api/orders | Place order | User |
| GET | /api/orders/me | My orders | User |
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
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