Build a Ride Booking App MVP
Build a full-stack ride booking app with a live map, REST API, and MongoDB.
Introduction
30 Second Summary
Every time you open a ride-hailing app and tap to set your pickup, a chain of events fires behind the scenes. Your location hits a server, gets saved in a database, and appears on a driver's screen in real time.
In this project, you will build a simplified ride booking app from scratch using Node.js, Express, MongoDB, and an interactive map library called Leaflet.js. You will wire a frontend map to a backend API backed by a cloud database.
What You'll Build
You will open your browser, see a live city map, click to drop pins for your pickup and dropoff, hit "Request Ride," and watch your ride appear as a color-coded marker that persists even after a page refresh.
By the end of this project, you'll have:
- A clickable interactive map where you set a pickup location (green marker) and dropoff location (red marker) with two clicks.
- A working REST API that accepts ride requests over HTTP and stores them in a cloud MongoDB database.
- A live rides display that loads all previously requested rides from the database on every page refresh, proving your data persists across sessions.
- Secret Mission: A driver dashboard that lists pending rides and lets a driver accept or complete them.
Are there any prerequisites?
This project assumes you have a basic understanding of HTML, CSS, JavaScript, and Node.js. You should also know how HTTP requests work at a high level.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up the Project
Every full-stack app needs three layers: a frontend the user sees, a backend that processes requests, and a database that stores data.
Before you write any application logic, you need a project folder with the right packages installed and a database ready to store data. This step gets all three foundations in place.
In this step, get ready to:
- Initialize a Node.js project and install your backend packages.
- Choose and set up your database (MongoDB Atlas or local SQLite).
- Create a server entry file that connects to the database and serves your app.
Initialize the project and install dependencies
Your backend needs Express to handle HTTP requests, cors to allow your frontend to call the backend, and a database library. Choose either MongoDB (cloud-hosted) or SQLite (local file) and follow the matching tab throughout this project.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder, move into it, and initialize it with npm by running these commands:
mkdir ride-booking-app
cd ride-booking-app
npm init -y
You should see npm print a summary of the new package.json file it just created in your ride-booking-app folder.
- Install your backend packages by running:
MongoDB
npm install express@4.22.2 mongoose@9.7.4 cors@2.8.6
What do these packages do?
- express@4.22.2 is a minimal web framework that handles routing and middleware for your API.
- mongoose@9.7.4 provides schema-based data modeling for MongoDB, making it easier to validate and query your data.
- cors@2.8.6 enables Cross-Origin Resource Sharing so your browser-based frontend can call the backend API.
SQLite
npm install express@4.22.2 better-sqlite3@13.0.1 cors@2.8.6
What do these packages do?
- express@4.22.2 is a minimal web framework that handles routing and middleware for your API.
- better-sqlite3@13.0.1 is a fast, synchronous SQLite driver for Node.js. Your data lives in a local file, no cloud account needed.
- cors@2.8.6 enables Cross-Origin Resource Sharing so your browser-based frontend can call the backend API.
- Create the two subfolders your project needs by running:
mkdir public
mkdir models
- Confirm all files and folders exist by running:
ls
You should see models, node_modules, package-lock.json, package.json, and public in the output.
Don't see those folders?
Make sure you ran cd ride-booking-app before creating the subfolders. If you are in the wrong directory, the folders will be created elsewhere.
help me troubleshoot my project folder structure
Set up your database
MongoDB
Your ride data needs to persist across page refreshes. MongoDB Atlas gives you a free cloud database so you do not need to install anything locally.
- Open https://www.mongodb.com/cloud/atlas/register in your browser.
- Create a free account. No credit card is required.
- Once signed in, create a new project if prompted.
- Click Build a Cluster (or Create if you already have a project) to open the deployment page.
- Select Free cluster as the cluster type.
- Choose any cloud provider and a region near you.
- Name the cluster RideCluster and click Create Deployment.
Atlas takes about 1-3 minutes to provision your cluster. While it deploys, configure access:
- Under authentication, select Username and Password.
- Enter a username and password you will remember, then click Create User.
- Under network access, click Add IP Address and select Allow Access from Anywhere (0.0.0.0/0).
- Click Add Entry.
Now get your connection string:
- In the left sidebar, click Database.
- Click Connect on your RideCluster cluster.
- Choose Drivers as the connection method.
- Copy the connection string and replace <username> and <password> with your database user credentials.
- Save the completed connection string somewhere you can find it in a moment.
Can't find the Connect button?
Your cluster may still be provisioning. Wait until the status shows a green circle and the word Active before clicking Connect.
Help me find my MongoDB Atlas connection string
SQLite
SQLite stores data in a local file on your machine. No accounts, no cloud setup, no connection strings. The database file is created automatically when your server starts.
There is nothing to configure here. Move on to the next substep.
Create the server entry file and verify the connection
With the database ready, you can create the file that ties everything together. This file starts the Express server and connects to your database.
- Switch back to VS Code. Open the ride-booking-app folder by clicking File in the top menu bar.
- Click Open Folder.
- Select the ride-booking-app folder on your Desktop, then click Open.
- Create a new file by clicking File then New File.
- Save it immediately as server.js in the root of ride-booking-app (press Cmd+S on macOS or Ctrl+S on Windows).
- Paste the following code into server.js:
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Start your server by running:
node server.js
Connected to MongoDB Atlas
Server running on http://localhost:3000
Seeing a connection error?
- Check that you replaced <password> in the connection string with your actual database user password (not your Atlas login password).
- Make sure you set Network Access to 0.0.0.0/0 in Atlas. Without this, Atlas blocks your connection.
- Confirm your cluster shows Active status in the Atlas dashboard. New clusters can take a few minutes to become ready.
help me debug my MongoDB Atlas connection error
✔️ Awesome, I've got everything!
Great. Double check you saved server.js and that both messages appeared in your terminal.
ⓧ I'd like to double check the full code
Here is the full server.js for this step. Make sure your file matches this exactly (except for your actual connection string):
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
const MONGODB_URI = 'your-connection-string-here';
mongoose.connect(MONGODB_URI)
.then(() => console.log('Connected to MongoDB Atlas'))
.catch(err => console.error('MongoDB connection error:', err));
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Your backend is alive and connected to a cloud database. Next up, you will build the interactive map that riders use to pick their locations.
Build the Interactive Map Page
Your server is connected to MongoDB Atlas and ready to serve files from the public folder. The next goal is giving users something to actually see and interact with.
The heart of any ride booking app is its map. In this step, you will build an interactive map page using Leaflet.js, an open-source mapping library that loads free OpenStreetMap tiles. By the end, users will click the map to set pickup and dropoff locations with color-coded markers.
In this step, get ready to:
- Create an HTML page that loads the Leaflet.js mapping library from a CDN.
- Style the page with a full-screen map and a dark sidebar panel.
- Add JavaScript that turns map clicks into pickup and dropoff markers.
Create the HTML page with Leaflet.js
Leaflet.js needs two things to work: its CSS file (for tile alignment and controls) and its JavaScript library. You will load both from a CDN and set up the page structure.
- Create a new file called index.html inside the public folder of your ride-booking-app project, then paste the following code into it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RideBook - Request a Ride</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<div id="sidebar">
<h1>RideBook</h1>
<p class="instruction" id="instruction">Click the map to set your pickup location</p>
<div id="ride-controls" class="hidden">
<button id="request-btn">Request Ride</button>
<button id="reset-btn">Reset</button>
</div>
<h2>Requested Rides</h2>
<ul id="rides-list"></ul>
</div>
<div id="map"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script src="app.js"></script>
</body>
</html>
What does this code do?
The <head> loads Leaflet's CSS from its CDN (required for tiles to align properly) and your own style.css for custom layout.
The <body> has two main areas: a #sidebar for controls and ride info, and a #map div where Leaflet renders the interactive map. The Leaflet JS and your app.js load at the bottom so the HTML elements exist before the scripts run.
- Save public/index.html.
- Open your browser and visit http://localhost:3000. You should see the text "RideBook" and "Requested Rides" rendered (unstyled) with an empty area where the map will appear.
Seeing a blank page?
- Make sure your server from Step 1 is still running in the terminal. If it stopped, run node server.js again from inside ride-booking-app.
- Confirm index.html is saved inside the public folder, not in the project root. Express serves files from public/ only.
- Help me debug why my page is blank
Add CSS styling for the layout
The page needs a full-screen flex layout: a dark sidebar on the left and the map filling the remaining space. You will build the stylesheet in stages so you can see the layout take shape.
- Create a new file called style.css inside the public folder, then paste the following layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
height: 100vh;
overflow: hidden;
}
#app {
display: flex;
height: 100vh;
}
#sidebar {
width: 320px;
padding: 20px;
background: #1a1a2e;
color: #ffffff;
overflow-y: auto;
flex-shrink: 0;
}
#sidebar h1 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #00d4aa;
}
What does this code do?
The global reset removes default browser margins. The #app flex container places the sidebar and map side by side. The sidebar is fixed at 320px wide with a dark background, while the map will fill whatever space remains.
- Save public/style.css and refresh your browser.
- You should see a dark sidebar on the left with a green "RideBook" title. The rest of the page is empty white space where the map will go.
- Add the sidebar typography and controls section below the existing code in style.css:
#sidebar h2 {
font-size: 1rem;
margin-top: 20px;
margin-bottom: 10px;
color: #aaaaaa;
text-transform: uppercase;
letter-spacing: 1px;
}
.instruction {
font-size: 0.9rem;
color: #cccccc;
margin-bottom: 15px;
}
#ride-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
#ride-controls.hidden {
display: none;
}
button {
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
}
- Continue adding the button color styles below the code you just pasted:
#request-btn {
background: #00d4aa;
color: #1a1a2e;
flex: 1;
}
#request-btn:hover {
background: #00b894;
}
#reset-btn {
background: #444466;
color: #ffffff;
}
#reset-btn:hover {
background: #555577;
}
#rides-list {
list-style: none;
}
#rides-list li {
background: #16213e;
padding: 12px;
border-radius: 6px;
margin-bottom: 8px;
font-size: 0.85rem;
}
- Save style.css and refresh your browser.
- The sidebar now shows styled instruction text, and the "Requested Rides" heading appears as a muted uppercase label.
- Add the final CSS section for status badges and the map container at the bottom of style.css:
#rides-list li .status {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
#rides-list li .status.pending {
background: #ffc107;
color: #1a1a2e;
}
#rides-list li .status.accepted {
background: #00d4aa;
color: #1a1a2e;
}
#rides-list li .status.completed {
background: #6c757d;
color: #ffffff;
}
#map {
flex: 1;
height: 100vh;
}
What does the #map rule do?
The flex: 1 property tells the map container to expand and fill all remaining horizontal space after the 320px sidebar. Combined with height: 100vh, the map takes the full viewport height. Leaflet requires its container to have an explicit height to render tiles.
- Save style.css and refresh your browser.
- The layout is now finalized. You see a dark sidebar on the left and a large empty area on the right where the map will render once you add JavaScript.
Sidebar taking the full page width?
- Ensure the first CSS rule is * { box-sizing: border-box; }. Without it, padding can push the sidebar wider than 320px.
- Check that #app has display: flex. Without flexbox, the sidebar and map stack vertically instead of side by side.
- Help me fix my sidebar layout
Add map interactivity with JavaScript
Now you will bring the empty map area to life. Leaflet needs you to initialize a map instance, add a tile layer for the background imagery, and define what happens when users click.
- Create a new file called app.js inside the public folder, then paste the map initialization code:
// Initialize the Leaflet map centered on London
const map = L.map('map').setView([51.505, -0.09], 13);
// Add OpenStreetMap tiles as the base layer
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// State variables for tracking markers
let pickupMarker = null;
let dropoffMarker = null;
let clickState = 'pickup';
// Cache DOM element references
const instruction = document.getElementById('instruction');
const rideControls = document.getElementById('ride-controls');
const requestBtn = document.getElementById('request-btn');
const resetBtn = document.getElementById('reset-btn');
const ridesList = document.getElementById('rides-list');
What does this code do?
The L.map('map') call binds Leaflet to the #map div and centers the view on London at zoom level 13. The L.tileLayer adds free OpenStreetMap tiles so you see actual streets and buildings.
The clickState variable tracks whether the next click should place a pickup or dropoff marker. DOM references are cached once so you do not re-query the document on every interaction.
- Save public/app.js and refresh your browser.
- You should now see a full interactive map of London with street tiles loading. You can drag to pan and scroll to zoom.
Map showing as a gray box?
- Check that the Leaflet CSS link in index.html is loaded in the <head> (before any content renders). Missing Leaflet CSS causes tiles to stack incorrectly or not appear at all.
- Open the browser's DevTools console and look for errors. A typo in the #map ID will cause Leaflet to fail silently.
- Ensure your #map CSS rule has height: 100vh. Leaflet requires the container to have an explicit height.
- Help me debug my gray map
- Now add the colored marker icon definitions below the existing code in app.js:
// Define a green icon for pickup markers
const greenIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
// Define a red icon for dropoff markers
const redIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
- Next, add the click handler that places markers when the user clicks the map:
// Handle map clicks to place pickup and dropoff markers
map.on('click', function (e) {
if (clickState === 'pickup') {
if (pickupMarker) map.removeLayer(pickupMarker);
pickupMarker = L.marker(e.latlng, { icon: greenIcon }).addTo(map).bindPopup('Pickup').openPopup();
clickState = 'dropoff';
instruction.textContent = 'Now click to set your dropoff location';
} else if (clickState === 'dropoff') {
if (dropoffMarker) map.removeLayer(dropoffMarker);
dropoffMarker = L.marker(e.latlng, { icon: redIcon }).addTo(map).bindPopup('Dropoff').openPopup();
clickState = 'done';
instruction.textContent = 'Ready! Click "Request Ride" to submit.';
rideControls.classList.remove('hidden');
}
});
What does the click handler do?
The clickState variable acts as a simple state machine: first click places a green pickup marker, second click places a red dropoff marker, and then the "Request Ride" button appears. The instruction text updates at each stage so the user always knows what to do next.
- Save public/app.js and refresh your browser.
- Click anywhere on the map. A green marker labeled "Pickup" should appear. Click a second spot. A red marker labeled "Dropoff" appears, and the "Request Ride" and "Reset" buttons become visible in the sidebar.
- Finally, add the reset function that clears markers and lets the user start over. Paste this below the click handler in app.js:
// Remove both markers and reset state
function resetMarkers() {
if (pickupMarker) map.removeLayer(pickupMarker);
if (dropoffMarker) map.removeLayer(dropoffMarker);
pickupMarker = null;
dropoffMarker = null;
clickState = 'pickup';
instruction.textContent = 'Click the map to set your pickup location';
rideControls.classList.add('hidden');
}
resetBtn.addEventListener('click', resetMarkers);
- Save public/app.js.
- Place two markers on the map, then click the Reset button in the sidebar. Both markers should disappear and the instruction text should return to "Click the map to set your pickup location."
Markers not appearing on click?
- Open DevTools (F12) and check the Console tab for errors. A common issue is a typo in the icon URL that prevents the image from loading.
- Make sure the greenIcon and redIcon definitions are placed before the map.on('click') handler. JavaScript reads top to bottom, so the icons must exist before the handler references them.
- Help me debug my missing markers
Before you move on, do a final check that everything works together.
What do you expect to happen when you refresh the page and click the map twice?
- Refresh http://localhost:3000 in your browser.
- Click once on the map to place a green "Pickup" marker.
- Click a second location to place a red "Dropoff" marker.
- Confirm the sidebar shows the "Request Ride" and "Reset" buttons after the second click.
You should see both colored markers on the map and the buttons visible in the sidebar. The map is fully interactive.
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: public/index.html, public/style.css, and public/app.js.
ⓧ I'd like to double check the full code
Your public/index.html should contain this exact code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RideBook - Request a Ride</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<div id="sidebar">
<h1>RideBook</h1>
<p class="instruction" id="instruction">Click the map to set your pickup location</p>
<div id="ride-controls" class="hidden">
<button id="request-btn">Request Ride</button>
<button id="reset-btn">Reset</button>
</div>
<h2>Requested Rides</h2>
<ul id="rides-list"></ul>
</div>
<div id="map"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script src="app.js"></script>
</body>
</html>
Your public/style.css is long. Scroll through the file and confirm these sections appear in order:
- Global reset (*), body, #app, #sidebar, #sidebar h1 (layout).
- #sidebar h2, .instruction, #ride-controls, button (sidebar elements).
- #request-btn, #reset-btn, #rides-list, #rides-list li (buttons and list).
- .status variants (pending, accepted, completed) and #map (status badges and map).
Your public/app.js should contain these sections in order:
- Map initialization with L.map and L.tileLayer, state variables, and DOM references.
- greenIcon and redIcon definitions.
- map.on('click') handler that places markers based on clickState.
- resetMarkers() function and resetBtn event listener.
Your interactive map page is working. Users can see a real city map, click to set locations, and reset to start over. Next up, you will wire the "Request Ride" button to actually do something with those locations.
Submit a Ride Request (The Vanishing Problem)
Your map is interactive and your markers are landing exactly where you click. The next question is: what happens when someone actually requests a ride?
In this step, you will wire the Request Ride button to capture coordinates and display rides in a list. But there is a catch you will discover at the end.
In this step, get ready to:
- Wire the Request Ride button to capture pickup and dropoff coordinates.
- Display requested rides in the sidebar list with a pending status badge.
- Discover what happens when you refresh the page after requesting rides.
Check the existing rides list structure
Your index.html already has everything the sidebar needs to display rides. You set this up in the previous step.
- Switch back to public/index.html in VS Code and look for the <ul id="rides-list"></ul> element in the sidebar.
What are these elements for?
The #rides-list <ul> is an empty list that will hold ride entries. Your CSS from Step 2 already styles each <li> inside it with a dark card background and the .status badge with color-coded labels for pending, accepted, and completed.
No HTML changes are needed. All the work in this step happens in JavaScript.
Wire the Request Ride button to add rides to the list
When the learner clicks Request Ride, the app needs to grab the coordinates from both markers, create a ride entry, and display it in the sidebar. You will add two pieces of code to app.js.
- In public/app.js, scroll to the bottom and find this line:
resetBtn.addEventListener('click', resetMarkers);
- Add the addRideToList helper function below that line by copying this code:
function addRideToList(ride) {
// Create a list item showing the ride's status and coordinates
const li = document.createElement('li');
li.innerHTML = `
<span class="status ${ride.status}">${ride.status}</span>
Pickup: ${ride.pickup.lat.toFixed(4)}, ${ride.pickup.lng.toFixed(4)}<br>
Dropoff: ${ride.dropoff.lat.toFixed(4)}, ${ride.dropoff.lng.toFixed(4)}
`;
ridesList.prepend(li);
}
What does this code do?
- It creates a new <li> element and fills it with a status badge and the ride's coordinates (rounded to 4 decimal places).
- The ridesList.prepend(li) inserts the ride at the top of the list so the most recent ride always appears first.
- The CSS class .status.pending triggers the yellow badge styling you already have in style.css.
- Now add the click event listener for the Request Ride button directly below the addRideToList function by copying this code:
requestBtn.addEventListener('click', function () {
// Only proceed if both markers have been placed
if (!pickupMarker || !dropoffMarker) return;
// Build a ride object from the marker coordinates
const rideData = {
pickup: { lat: pickupMarker.getLatLng().lat, lng: pickupMarker.getLatLng().lng },
dropoff: { lat: dropoffMarker.getLatLng().lat, lng: dropoffMarker.getLatLng().lng },
status: 'pending'
};
addRideToList(rideData);
resetMarkers();
});
What does this code do?
- The guard if (!pickupMarker || !dropoffMarker) return prevents submitting a ride if the user has not placed both markers.
- It extracts latitude and longitude from each marker using getLatLng() and packages them into a rideData object with a hardcoded status of 'pending'.
- After adding the ride to the sidebar list, it calls resetMarkers() to clear the map so the user can immediately request another ride.
- Save public/app.js.
- Return to your browser at http://localhost:3000 and refresh the page.
- Click the map once to set a pickup (green marker).
- Click the map again to set a dropoff (red marker).
- Click the Request Ride button.
You should see a new entry appear in the sidebar under Requested Rides with a yellow "PENDING" badge and the pickup/dropoff coordinates.
Don't see a ride in the sidebar?
- Make sure you saved app.js and refreshed the browser. Unsaved changes will not load.
- Check that you placed the new code AFTER the resetBtn.addEventListener('click', resetMarkers); line, not inside another function.
- Open DevTools (F12) and check the Console tab for any red error messages.
Why isn't my Request Ride button adding rides to the sidebar?
- Request 2-3 more rides by clicking different locations on the map and hitting Request Ride each time.
Your sidebar should now show multiple ride entries stacked up. This feels like a working app.
Discover the persistence problem
You have a few rides listed in the sidebar. Before you do anything else, predict: what do you think will happen if you refresh the page right now?
- Press Cmd+R (macOS) or Ctrl+R (Windows) to refresh the page.
Every ride you just requested is gone. The sidebar is empty again.
Why did the rides disappear?
Your rides only existed in the browser's JavaScript memory. When you refresh, the browser throws away all variables and starts the script fresh. There is no database or server saving anything.
This is the core problem with client-only apps. Without a backend that persists data, everything resets on every page load. The next step fixes this by building a REST API that saves rides to MongoDB.
✔️ Awesome, I've got everything!
Great. Double check you have saved public/app.js and you are ready to move on.
ⓧ I'd like to double check the full code
Your public/app.js should now have these sections in order. Scroll through your file and confirm each one is present:
- Map initialization with L.map and L.tileLayer.
- Variable declarations (pickupMarker, dropoffMarker, clickState) and DOM references.
- greenIcon and redIcon definitions.
- map.on('click', ...) handler for placing markers.
- resetMarkers() function and resetBtn listener.
- addRideToList(ride) function (new in this step).
- requestBtn.addEventListener('click', ...) handler (new in this step).
You have proved the problem: rides vanish the instant the browser refreshes because nothing saves them. In the next step, you will build a REST API and database model that stores every ride permanently in MongoDB Atlas.
Build the REST API and Database Model
You proved the problem in the last step: rides disappear the moment you refresh the page. That happens because nothing is saving them outside the browser's memory.
In this step, you will build the backend that solves this. You will create a Mongoose data model that defines what a ride looks like, then add two REST API routes to your Express server: one to save new rides to MongoDB and one to retrieve them.
In this step, get ready to:
- Create a Mongoose schema that defines the shape of a ride document.
- Add POST and GET API routes to your Express server.
- Test the API with a real HTTP request and see your data saved in MongoDB Atlas.
Create the Mongoose Ride model
Before your API can save rides, it needs to know what a ride looks like. A Mongoose schema defines the exact fields and types that every ride document must contain.
- Create the file models/Ride.js by running this command:
touch models/Ride.js
- Confirm the file was created by running:
ls models/
You should see Ride.js listed in the output.
- Open models/Ride.js in VS Code and paste the following schema:
const mongoose = require('mongoose');
const rideSchema = new mongoose.Schema({
pickup: {
lat: { type: Number, required: true },
lng: { type: Number, required: true }
},
dropoff: {
lat: { type: Number, required: true },
lng: { type: Number, required: true }
},
status: {
type: String,
enum: ['pending', 'accepted', 'completed'],
default: 'pending'
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Ride', rideSchema);
What does this schema do?
- pickup and dropoff each store a latitude and longitude pair. These are the coordinates from the map clicks.
- status tracks where the ride is in its lifecycle. It can only be one of three values: pending, accepted, or completed. New rides default to pending.
- createdAt timestamps each ride automatically so you can sort by most recent.
- module.exports makes this model importable by your server file.
- Save models/Ride.js.
File not saving?
- Make sure you created the models folder in Step 1. If it does not exist, run mkdir models from your ride-booking-app folder first.
- Check that you are saving to models/Ride.js (capital R), not models/ride.js.
Help me debug my Ride model file
Add API routes to server.js
Your model defines the shape of ride data. Now you need routes that accept incoming rides and retrieve saved ones.
- In server.js, find this line:
const cors = require('cors');
- Add this import directly below it:
const Ride = require('./models/Ride');
- Now scroll down past the mongoose.connect(...) block. Add the following POST route between it and app.listen(...):
app.post('/api/rides', async (req, res) => {
try {
const { pickup, dropoff } = req.body;
const ride = new Ride({ pickup, dropoff });
const savedRide = await ride.save();
res.status(201).json(savedRide);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
What does this route do?
- app.post('/api/rides', ...) listens for POST requests at the /api/rides path.
- It extracts pickup and dropoff from the request body and creates a new Ride document.
- ride.save() writes the document to your MongoDB Atlas cluster.
- It responds with status 201 (Created) and the saved ride data, including the auto-generated _id and status fields.
- Add the GET route directly below the POST route you just added:
app.get('/api/rides', async (req, res) => {
try {
const rides = await Ride.find().sort({ createdAt: -1 });
res.json(rides);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
What does this route do?
- Ride.find() retrieves every ride document from the database.
- .sort({ createdAt: -1 }) orders results with the newest rides first.
- The response is a JSON array of all rides. If no rides exist yet, it returns an empty array [].
- Save server.js.
✔️ Awesome, I've got everything!
Great. Make sure you have saved server.js with both routes added.
ⓧ I'd like to double check the full code
Scroll through server.js and confirm these pieces are present, in this order:
- The four require statements at the top (express, mongoose, cors, and the new Ride model).
- The middleware lines (cors(), express.json(), express.static('public')).
- The MONGODB_URI variable and mongoose.connect(...) block.
- The app.post('/api/rides', ...) route.
- The app.get('/api/rides', ...) route.
- The app.listen(3000, ...) call at the bottom.
Seeing a syntax error when you save?
- Check that each route's closing }); matches its opening app.post( or app.get(. Missing a parenthesis or brace is the most common mistake.
- Make sure the const Ride = require('./models/Ride'); line is above the routes, not inside them.
Help me fix a syntax error in my server.js routes
Test the API
Both routes are in place. Time to restart the server and prove they work.
- Switch back to the terminal where your server is running.
- Press Ctrl+C to stop the server.
- Restart it by running:
node server.js
You should see both "Connected to MongoDB Atlas" and "Server running on http://localhost:3000" printed.
Before you test, think about this: if you visit http://localhost:3000/api/rides in your browser right now, what do you expect to see?
- Open your browser and visit http://localhost:3000/api/rides.
You should see an empty JSON array: []. No rides have been saved yet, so the database returns nothing.
- Open a second terminal window (keep the server running in the first one).
- Send a test ride to your API by running this command:
curl -X POST http://localhost:3000/api/rides -H "Content-Type: application/json" -d "{\"pickup\":{\"lat\":51.505,\"lng\":-0.09},\"dropoff\":{\"lat\":51.51,\"lng\":-0.08}}"
What does this command do?
- curl -X POST sends an HTTP POST request to your server.
- -H "Content-Type: application/json" tells the server the body is JSON data.
- -d provides the JSON body with pickup and dropoff coordinates (latitude and longitude near London).
You should see a JSON response with your ride data, including an auto-generated _id, status: "pending", and a createdAt timestamp.
- Switch back to your browser and refresh http://localhost:3000/api/rides.
The page now shows an array with one ride object. Your ride is saved in MongoDB Atlas and your GET route returned it.
Seeing an empty array even after the POST?
- Check that the POST response did not contain an error. If you see {"error": ...}, your connection string in server.js may be incorrect. Double check the username, password, and cluster name.
- Make sure your MongoDB Atlas network access is set to 0.0.0.0/0. Without this, your local machine cannot reach the cluster.
- On Windows PowerShell, the curl command may be an alias. Try using curl.exe instead, or paste this into your browser console:
fetch('/api/rides', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pickup: { lat: 51.505, lng: -0.09 }, dropoff: { lat: 51.51, lng: -0.08 } })
}).then(r => r.json()).then(console.log)
Help me debug my ride booking API
Your API is alive. Rides are now saved to a real cloud database and served back on demand. Next up, you will connect the frontend map to this backend so that clicking "Request Ride" saves to MongoDB instead of vanishing on refresh.
Connect Frontend to Backend
You have a working map that lets users click to set locations, and a working API that can store and retrieve rides from MongoDB. The problem is they do not talk to each other yet.
In this step, you will wire the frontend to the backend using the Fetch API. The "Request Ride" button will send ride data to your Express server, and the page will load all saved rides when it opens. Rides will finally survive a page refresh.
In this step, get ready to:
- Wire the "Request Ride" button to POST ride data to your API.
- Display saved rides as color-coded markers on the map.
- Load all existing rides from MongoDB when the page opens.
Send ride requests to the API
The current "Request Ride" button adds rides to the sidebar list using browser memory only. You need to replace it with a version that sends ride data to your /api/rides endpoint and also draws each ride on the map.
First, you will add a helper function that draws a ride's pickup and dropoff on the map as colored circles connected by a dashed line.
- In public/app.js, find the closing } of your addRideToList function.
- Add the following function directly below it:
function addRideToMap(ride) {
// Draw a teal circle at the pickup location
L.circleMarker([ride.pickup.lat, ride.pickup.lng], {
radius: 8,
color: '#00d4aa',
fillColor: '#00d4aa',
fillOpacity: 0.7
}).addTo(map).bindPopup('Pickup (Ride ' + ride._id.slice(-4) + ')');
// Draw a red circle at the dropoff location
L.circleMarker([ride.dropoff.lat, ride.dropoff.lng], {
radius: 8,
color: '#e74c3c',
fillColor: '#e74c3c',
fillOpacity: 0.7
}).addTo(map).bindPopup('Dropoff (Ride ' + ride._id.slice(-4) + ')');
// Connect pickup and dropoff with a purple dashed line
L.polyline([
[ride.pickup.lat, ride.pickup.lng],
[ride.dropoff.lat, ride.dropoff.lng]
], { color: '#7c3aed', weight: 2, dashArray: '5, 10' }).addTo(map);
}
What does this code do?
This function visualizes a saved ride on the map using three Leaflet drawing primitives.
- L.circleMarker draws a small filled circle at a coordinate. Teal for pickup, red for dropoff.
- L.polyline connects the two points with a purple dashed line so you can see the ride's route at a glance.
- ride._id.slice(-4) shows the last 4 characters of the MongoDB document ID in each popup, so you can tell rides apart.
Now replace the existing request handler so it sends ride data to the server instead of just adding it to local memory.
- In public/app.js, find this existing code block:
requestBtn.addEventListener('click', function () {
if (!pickupMarker || !dropoffMarker) return;
const rideData = {
pickup: { lat: pickupMarker.getLatLng().lat, lng: pickupMarker.getLatLng().lng },
dropoff: { lat: dropoffMarker.getLatLng().lat, lng: dropoffMarker.getLatLng().lng },
status: 'pending'
};
addRideToList(rideData);
resetMarkers();
});
- Delete that entire block and replace it with this updated version:
requestBtn.addEventListener('click', async function () {
if (!pickupMarker || !dropoffMarker) return;
// Build the ride data from marker positions
const rideData = {
pickup: {
lat: pickupMarker.getLatLng().lat,
lng: pickupMarker.getLatLng().lng
},
dropoff: {
lat: dropoffMarker.getLatLng().lat,
lng: dropoffMarker.getLatLng().lng
}
};
// Send the ride to the server
try {
const response = await fetch('/api/rides', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rideData)
});
const savedRide = await response.json();
addRideToList(savedRide);
addRideToMap(savedRide);
resetMarkers();
} catch (err) {
console.error('Error requesting ride:', err);
}
});
What changed here?
- The function is now async so it can use await to wait for the server's response.
- fetch('/api/rides', { method: 'POST', ... }) sends the pickup and dropoff coordinates to your Express API.
- The server returns the saved ride (with its MongoDB _id and status), which gets added to both the sidebar list and the map.
- The removed `status: 'pending'` field is no longer needed because the Mongoose schema sets it automatically.
- Save public/app.js.
- Switch to your terminal, stop the server with **Ctrl+C**, and restart it by running:
node server.js
- Open http://localhost:3000 in your browser.
- Click the map once to set a green pickup marker.
- Click the map again to set a red dropoff marker.
- Click the Request Ride button.
You should see the ride appear in the sidebar list with a "pending" status badge. You should also see a teal circle at the pickup, a red circle at the dropoff, and a purple dashed line connecting them on the map.
Ride not appearing on the map?
- Open your browser's DevTools console (**Cmd+Option+J** on macOS, **Ctrl+Shift+J** on Windows). Look for any red error messages. A TypeError about addRideToMap means the function was not added correctly.
- Check that the addRideToMap function is placed outside the requestBtn.addEventListener block, not inside it.
- Make sure you restarted the server after saving your changes.
Help me debug why my ride request isn't showing markers on the map.
Load and display existing rides on page load
Rides are now saving to MongoDB when you click the button. But if you refresh the page, the map and sidebar start empty again. You need a function that fetches all saved rides from the API and displays them when the page first loads.
- In public/app.js, scroll to the very bottom of the file and add this function:
// Fetch all rides from the database and display them
async function loadRides() {
try {
const response = await fetch('/api/rides');
const rides = await response.json();
rides.forEach(ride => {
addRideToList(ride);
addRideToMap(ride);
});
} catch (err) {
console.error('Error loading rides:', err);
}
}
// Load rides as soon as the page opens
loadRides();
What does this code do?
- loadRides() calls GET /api/rides to fetch every ride stored in MongoDB.
- It loops through the returned array and calls addRideToList and addRideToMap for each one, populating both the sidebar and the map.
- The bare loadRides(); call at the bottom runs the function immediately when the script loads, so existing rides appear as soon as the page opens.
- Save public/app.js.
- Stop the server with **Ctrl+C** and restart it by running:
node server.js
Before you test, think about what you expect to see. You requested at least one ride earlier in this step. What do you think will happen when you refresh the page?
- Open http://localhost:3000 in your browser (or refresh if it is already open).
You should see all previously requested rides already displayed in the sidebar list and as colored markers on the map. The persistence problem from Step 3 is solved. Rides survive page refreshes because they live in MongoDB, not browser memory.
- Try requesting one more ride to confirm the full loop works: click pickup, click dropoff, click **Request Ride**, then refresh the page.
After the refresh, you should see all rides (including the new one) in both the sidebar and on the map.
Rides not showing after refresh?
- Open DevTools console and check for errors. A 404 on /api/rides means the server is not running or the route is missing.
- Visit http://localhost:3000/api/rides directly in your browser. If you see an empty array [], the test ride from Step 4 used a different database. Request a new ride through the map UI first.
- Make sure loadRides(); is at the very bottom of app.js, outside all other functions.
Help me figure out why rides are not loading on page refresh.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Your public/app.js file is quite long now. Scroll through it and confirm these constructs are present in this order:
- Map initialization (L.map and L.tileLayer).
- Variable declarations (pickupMarker, dropoffMarker, clickState).
- DOM references (instruction, rideControls, requestBtn, resetBtn, ridesList).
- greenIcon and redIcon definitions.
- map.on('click') handler for placing markers.
- resetMarkers() function and resetBtn listener.
- requestBtn.addEventListener('click', async function ()) with fetch('/api/rides', { method: 'POST' }) inside.
- addRideToList(ride) function.
- addRideToMap(ride) function with L.circleMarker and L.polyline.
- loadRides() function followed by the bare loadRides(); call at the very bottom.
Your frontend and backend are fully connected. Rides persist across page refreshes, display on an interactive map, and live in a real cloud database. You have built a complete full-stack ride booking application.
Secret mission
Build a Driver Dashboard
A ride booking app is two-sided. Riders request rides, but right now no driver can see or accept them. Can you build the other half of the equation?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses MongoDB Atlas's free tier and a local Node.js server, so there are no ongoing costs.
Resources you used:
- MongoDB Atlas free cluster (M0) storing your ride data.
- Local Node.js server running on port 3000.
- Local project folder ride-booking-app on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or want to revisit the project soon.
- Your Atlas free cluster costs nothing and stays active indefinitely.
- Your local server only runs while your terminal is open. Close the terminal or press Ctrl+C to stop it at any time.
- Your ride-booking-app folder stays on your Desktop for next time.
Pause - I'll come back to this later
Free up local resources while keeping your cloud data intact.
- Stop the local server by pressing Ctrl+C in the terminal where it is running.
- Atlas automatically pauses your free cluster after 30 days with no connections. No manual action is needed.
- Your project folder remains on your Desktop ready for when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your ride data.
- Stop the local server by pressing Ctrl+C in the terminal.
- Delete your MongoDB Atlas cluster: go to the Database page in Atlas, click the three-dot menu (...) next to your cluster, select Terminate, type the cluster name to confirm, and click Terminate.
- Delete the local project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf ride-booking-app
On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force ride-booking-app
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) and checking that ride-booking-app no longer appears.
That's a Wrap!
That's a Wrap!
Nice work! You just built a full-stack ride booking app from scratch, complete with an interactive map, a REST API, and a cloud database that persists data across page refreshes.
You've learned how to:
- Build an interactive map interface using Leaflet.js and OpenStreetMap tiles where users click to set pickup and dropoff locations.
- Create a REST API with Express.js and design a Mongoose schema connected to a cloud MongoDB Atlas database.
- Wire a frontend to a backend using the Fetch API, solving the data persistence problem so rides survive page refreshes.
- Secret Mission: Built a driver dashboard with ride acceptance and status updates via PATCH requests.
Ready to quiz yourself?