Project 3: Auto Booking API — Cab/Auto System
Build a booking system like Uber/Ola for Hyderabad autos. Learn state machine design, valid status transitions, and real-time updates.
Auto rickshaws are the heartbeat of Hyderabad traffic. In this project, we'll build a Ride Booking API — think Uber/Ola but for the classic Hyderabad auto-rickshaw!
What we'll build:
- Ride model with status tracking (requested → accepted → ongoing → completed)
- State machine to prevent invalid status transitions (can't go from "completed" back to "requested")
- Role-based access — riders request rides, drivers accept/complete them
- Nearby drivers — find available autos near the pickup location
- Socket.io concept — real-time updates vs HTTP polling
Key concept: State Machine
A state machine defines valid transitions between states. For a ride:
requested → accepted → ongoing → completed
↓ ↓
cancelled cancelled (driver can cancel before pickup)
Invalid transitions (must be prevented):
- ❌ requested → completed (skip pickup + ride)
- ❌ ongoing → requested (go backwards)
- ❌ completed → accepted (re-open closed ride)
- ❌ cancelled → ongoing (resume cancelled ride)
By the end of this project, you'll have a working ride booking API that safely manages ride states — just like Uber/Ola!
A state machine ensures that our ride status can only change in valid ways. Think of it like an auto-rickshaw meter — you can't go from "Trip Ended" back to "Trip Started" without starting a new trip.
Valid state transitions:
// ┌─────────────┐
// │ REQUESTED │ ← User requests a ride
// └──────┬──────┘
// ┌───┴───┐
// ▼ ▼
// ┌────────┐ ┌───────────┐
// │ACCEPTED│ │ CANCELLED │ ← User or driver cancels
// └───┬────┘ └───────────┘
// ▼
// ┌──────────┐
// │ ONGOING │ ← Driver starts the ride
// └────┬─────┘
// ▼
// ┌───────────┐
// │COMPLETED│ ← Ride finished, payment done
// └──────────┘
Implementing the state machine in code:
// models/Ride.js — Ride Schema with State Machine
import mongoose from 'mongoose';
const RIDE_STATUS = ['requested', 'accepted', 'ongoing', 'completed', 'cancelled'];
// Valid transitions map
const VALID_TRANSITIONS = {
'requested': ['accepted', 'cancelled'],
'accepted': ['ongoing', 'cancelled'],
'ongoing': ['completed', 'cancelled'],
'completed': [], // Terminal state — no transitions from completed
'cancelled': [] // Terminal state — no transitions from cancelled
};
const rideSchema = new mongoose.Schema({
rider: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
driver: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
status: {
type: String,
enum: RIDE_STATUS,
default: 'requested'
},
pickup: {
address: { type: String, required: [true, 'Pickup address daalo bhai!'] },
location: {
type: { type: String, enum: ['Point'], default: 'Point' },
coordinates: { type: [Number], required: true } // [lng, lat]
}
},
dropoff: {
address: String,
location: {
type: { type: String, enum: ['Point'] },
coordinates: { type: [Number] }
}
},
fare: {
estimated: { type: Number },
actual: { type: Number }
},
distance: { type: Number }, // in km
duration: { type: Number }, // in minutes
paymentMethod: {
type: String,
enum: ['cash', 'card', 'wallet'],
default: 'cash'
}
}, { timestamps: true });
// Static method to check if transition is valid
rideSchema.statics.isValidTransition = function(currentStatus, newStatus) {
const allowedTransitions = VALID_TRANSITIONS[currentStatus];
if (!allowedTransitions) return false;
return allowedTransitions.includes(newStatus);
};
// Instance method to change status safely
rideSchema.methods.changeStatus = function(newStatus) {
const constructor = this.constructor;
if (!constructor.isValidTransition(this.status, newStatus)) {
throw new Error(
'Invalid status transition! ' + this.status + ' → ' + newStatus +
' allowed nahi hai bhai! ❌'
);
}
this.status = newStatus;
};
export default mongoose.model('Ride', rideSchema);
Why state machines matter: Without a state machine, a buggy client could send PATCH /rides/:id { status: "completed" } on a ride that hasn't even been accepted yet. The state machine prevents this — the ride must go through requested → accepted → ongoing → completed in order.
Now let's build the ride controller that uses our state machine to update ride statuses safely.
// controllers/ride.controller.js — Ride Management
import Ride from '../models/Ride.js';
import User from '../models/User.js';
import AppError from '../utils/AppError.js';
import asyncHandler from '../utils/asyncHandler.js';
// POST /api/rides — Request a new ride
export const requestRide = asyncHandler(async (req, res) => {
const { pickup, dropoff } = req.body;
const ride = await Ride.create({
rider: req.user.id,
pickup,
dropoff,
fare: {
estimated: calculateFare(pickup, dropoff) // Simple distance-based
},
status: 'requested'
});
res.status(201).json({
success: true,
message: 'Ride requested! Auto aa raha hai bhai! 🛺',
data: ride
});
});
// PATCH /api/rides/:id/accept — Driver accepts ride
export const acceptRide = asyncHandler(async (req, res, next) => {
const ride = await Ride.findById(req.params.id);
if (!ride) throw new AppError('Ride not found!', 404);
// Check state machine
ride.changeStatus('accepted'); // Throws if invalid
ride.driver = req.user.id; // Assign driver
await ride.save();
res.json({
success: true,
message: 'Ride accepted! Driver aa raha hai! 🚗',
data: ride
});
});
// PATCH /api/rides/:id/start — Driver starts the ride
export const startRide = asyncHandler(async (req, res, next) => {
const ride = await Ride.findById(req.params.id);
if (!ride) throw new AppError('Ride not found!', 404);
// Only the assigned driver can start
if (ride.driver.toString() !== req.user.id) {
throw new AppError('Yeh ride aapki nahi hai bhai! Sirf driver start kar sakta hai!', 403);
}
ride.changeStatus('ongoing');
await ride.save();
res.json({ success: true, message: 'Ride started! 🚗💨', data: ride });
});
// PATCH /api/rides/:id/complete — Complete the ride
export const completeRide = asyncHandler(async (req, res, next) => {
const ride = await Ride.findById(req.params.id);
if (!ride) throw new AppError('Ride not found!', 404);
ride.changeStatus('completed');
ride.fare.actual = ride.fare.estimated; // In real app, calculate from actual distance
await ride.save();
res.json({
success: true,
message: 'Ride completed! Thank you! Hyderabad khatam! 🏁',
data: ride
});
});
// PATCH /api/rides/:id/cancel — Cancel ride (rider or driver)
export const cancelRide = asyncHandler(async (req, res, next) => {
const ride = await Ride.findById(req.params.id);
if (!ride) throw new AppError('Ride not found!', 404);
ride.changeStatus('cancelled');
ride.cancelledBy = req.user.id;
await ride.save();
res.json({ success: true, message: 'Ride cancelled! 🛑', data: ride });
});
// Simple fare calculator
function calculateFare(pickup, dropoff) {
if (!pickup || !dropoff) return 50; // Minimum fare
// In real app: use Google Maps Distance Matrix API or geolocation math
return Math.floor(50 + Math.random() * 150); // Rs. 50-200
}
Route wiring:
// routes/ride.routes.js
router.post('/', protect, requestRide); // Rider requests
router.patch('/:id/accept', protect, authorize('driver'), acceptRide);
router.patch('/:id/start', protect, authorize('driver'), startRide);
router.patch('/:id/complete', protect, authorize('driver'), completeRide);
router.patch('/:id/cancel', protect, cancelRide); // Anyone can cancel
Key security consideration: The state machine prevents invalid transitions at the MODEL level — not just the route level. Even if a hacker sends a direct request to MongoDB (bypassing your API), the model's changeStatus method enforces valid transitions.
Real-time updates are critical for a ride booking app. When a driver accepts a ride, the rider needs to know IMMEDIATELY — not 5 seconds later. There are two approaches:
1. HTTP Polling (Old way — Purana):
// Client polls every 2 seconds
setInterval(async () => {
const response = await fetch('/api/rides/' + rideId);
const data = await response.json();
if (data.status === 'accepted') {
showDriverInfo(data.driver);
}
}, 2000); // Every 2 seconds!
// ❌ Problems:
// - 30 requests per minute per user
// - 1000 users = 30,000 requests per minute!
// - Most requests return NO change (wasted bandwidth)
// - 2 second delay — not truly real-time
2. WebSockets (New way — Naya):
// npm install socket.io
// server.js — Setup WebSocket server
import { Server } from 'socket.io';
const io = new Server(httpServer, {
cors: { origin: '*' }
});
io.on('connection', (socket) => {
console.log('🔌 Client connected:', socket.id);
// Join a ride room
socket.on('join-ride', (rideId) => {
socket.join('ride:' + rideId);
});
});
// In ride controller — emit real-time updates
export const acceptRide = asyncHandler(async (req, res) => {
const ride = await Ride.findById(req.params.id);
ride.changeStatus('accepted');
ride.driver = req.user.id;
await ride.save();
// 🔴 Real-time update to rider!
io.to('ride:' + ride._id).emit('ride-update', {
status: 'accepted',
driver: req.user,
message: 'Driver mil gaya! 🚗'
});
res.json({ success: true, data: ride });
});
// Client-side (browser)
const socket = io('http://localhost:3000');
socket.emit('join-ride', rideId);
socket.on('ride-update', (data) => {
console.log('🔴 Real-time update:', data);
// Update UI immediately!
});
Comparison:
| Feature | HTTP Polling | WebSockets |
|---|---|---|
| Latency | Up to polling interval (2s) | ~50ms (near real-time) |
| Server load | High (many empty responses) | Low (only sends when data changes) |
| Bandwidth | Wasteful (repeated headers/requests) | Efficient (minimal overhead) |
| Connection | HTTP (request/response) | Persistent TCP connection |
| Implementation | Simple — just setInterval + fetch | Requires Socket.io library |
For this project, we'll implement the controller logic with Socket.io ready. In a real app, you'd emit events from every status-changing endpoint.
Let's trace the complete ride booking flow from start to finish.
Complete flow:
- Rider requests ride → POST /api/rides → status = "requested"
- Driver sees available rides → GET /api/rides/available → find rides where status = "requested"
- Driver accepts → PATCH /api/rides/:id/accept → status = "accepted", driver assigned
- Driver arrives at pickup → Real-time notification to rider via Socket.io
- Driver starts ride → PATCH /api/rides/:id/start → status = "ongoing"
- Ride happens → Auto meter is running!
- Driver completes ride → PATCH /api/rides/:id/complete → status = "completed", fare calculated
- Payment → Cash, card, or wallet
- Both rate each other → POST /api/rides/:id/rating
Testing the flow:
# 1. Rider requests a ride
curl -X POST http://localhost:3000/api/rides \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"pickup": { "address": "Hitech City, Hyderabad", "location": { "coordinates": [78.37, 17.44] } },
"dropoff": { "address": "Gachibowli, Hyderabad" }
}'
# 2. Driver accepts the ride
curl -X PATCH http://localhost:3000/api/rides//accept \
-H "Authorization: Bearer "
# 3. Driver starts the ride
curl -X PATCH http://localhost:3000/api/rides//start \
-H "Authorization: Bearer "
# 4. Driver completes the ride
curl -X PATCH http://localhost:3000/api/rides//complete \
-H "Authorization: Bearer "
Invalid state transitions that our state machine prevents:
# ❌ Trying to complete a ride that hasn't started
curl -X PATCH http://localhost:3000/api/rides/abc/complete
# Response: { "success": false, "message": "Invalid status transition! ongoing → completed allowed nahi hai bhai!" }
# ❌ Trying to go back from completed to ongoing
# The state machine prevents this at the model level!Key Takeaways
- ✅ State machine defines VALID transitions between statuses — prevents invalid state changes
- ✅ Valid ride flow: requested → accepted → ongoing → completed (terminal), any can cancel
- ✅ Implement state machine at MODEL level (changeStatus method) — security at the data layer
- ✅ Dedicated endpoints per action (accept, start, complete, cancel) — not a generic status updater
- ✅ WebSockets (Socket.io) provide real-time updates — much better than HTTP polling
- ✅ Polling: simple but wasteful (30 req/min/user), WebSockets: efficient (push-based)
- ✅ Always check authorization — only assigned driver can start/complete a ride
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