Chapter 5.3☕ 24 min read

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.

01Project Overview — Auto/Cab Booking API

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!

02State Machine — Ride Status Transitions

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.

03Ride Controller — Updating Ride Status Safely

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.

04WebSockets vs Polling — Real-time Updates

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:

FeatureHTTP PollingWebSockets
LatencyUp to polling interval (2s)~50ms (near real-time)
Server loadHigh (many empty responses)Low (only sends when data changes)
BandwidthWasteful (repeated headers/requests)Efficient (minimal overhead)
ConnectionHTTP (request/response)Persistent TCP connection
ImplementationSimple — just setInterval + fetchRequires 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.

05Complete Booking Flow — Request → Accept → Complete

Let's trace the complete ride booking flow from start to finish.

Complete flow:

  1. Rider requests ride → POST /api/rides → status = "requested"
  2. Driver sees available rides → GET /api/rides/available → find rides where status = "requested"
  3. Driver accepts → PATCH /api/rides/:id/accept → status = "accepted", driver assigned
  4. Driver arrives at pickup → Real-time notification to rider via Socket.io
  5. Driver starts ride → PATCH /api/rides/:id/start → status = "ongoing"
  6. Ride happens → Auto meter is running!
  7. Driver completes ride → PATCH /api/rides/:id/complete → status = "completed", fare calculated
  8. Payment → Cash, card, or wallet
  9. 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
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