Full-Stack App with Express and MongoDB
Build a Tourist Guide app with a REST API, MongoDB Atlas, and a dynamic frontend.
Introduction
30 Second Summary
Every city has hidden gems that locals know about but tourists never find. What if you could build your own app to collect, share, and filter those recommendations?
In this project, you will build a full-stack Tourist Guide web application using Node.js, Express, and MongoDB Atlas. Your app will display destination cards, let users add new places through a form, and filter results by category.
What You'll Build
Open your browser, click a category like Food, and watch the grid instantly update to show only restaurants and markets from cities around the world.
By the end of this project, you'll have:
- A live web app displaying a grid of destination cards with names, locations, descriptions, and star ratings you can browse at a glance.
- Category filter buttons that instantly narrow the grid to Food, Sightseeing, or Nightlife destinations with a single click.
- A working form that saves new destinations to a cloud database. Restart the server and your data is still there.
- Secret Mission: Add a search bar that filters destinations by name in real-time.
Are there any prerequisites?
You should have a basic understanding of HTML, CSS, and JavaScript syntax. No backend experience is required.
You will also need VS Code and Node.js installed on your computer. You will set up a free MongoDB Atlas account (no credit card required) during the project.
Before We Start
You're about to build a full-stack Tourist Guide app that connects a frontend, a REST API, and a cloud database into one working product.
Before writing any application code, you need the foundation in place. In this step, you'll create your project folder, install dependencies, and set up your database. You can choose between MongoDB Atlas (a free cloud database) or SQLite (a local file-based database that needs no account).
In this step, get ready to:
- Confirm Node.js is ready and create your project with Express and your chosen database library.
- Set up your database (MongoDB Atlas in the cloud, or SQLite locally).
- Configure your environment variables and project start script.
Create your project folder and initialize Node.js
Your project needs a home folder, a package.json to track dependencies, and the right packages installed before you can start coding.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Confirm your Node.js version by running this command:
node --version
You should see v24 followed by a version number (for example, v24.18.0).
Don't see v24?
If you see a version lower than v24, visit nodejs.org and download the latest LTS installer. Run the installer, then close and reopen VS Code so the terminal picks up the new version.
If you see "command not found", Node.js is not installed. Download it from nodejs.org and run the installer.
help me install Node.js v24
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir tourist-guide
cd tourist-guide
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the tourist-guide folder, and click Open.
VS Code reloads with tourist-guide as your workspace. The Explorer panel on the left shows the empty folder.
- Open a new terminal in this folder by pressing Ctrl+` (backtick).
- Initialize a Node.js project by running this command:
npm init -y
You should see a JSON summary of your new package.json printed in the terminal.
What does this command do?
npm init -y creates a package.json file with default values. This file tracks your project's name, version, and the packages it depends on.
The -y flag accepts all defaults so you skip the interactive prompts.
- Install Express and your database library by running the command for your chosen database:
MongoDB Atlas
npm install express mongoose
SQLite (local)
npm install express better-sqlite3
MongoDB Atlas
You should see output showing packages added. This downloads Express (your web server framework) and Mongoose (your MongoDB toolkit) into the project.
SQLite (local)
You should see output showing packages added. This downloads Express (your web server framework) and better-sqlite3 (a fast, local SQL database library) into the project.
Seeing permission errors?
On macOS, try prefixing the command with sudo. On Windows, close VS Code and reopen it by right-clicking and selecting Run as administrator.
help me fix npm permission errors
- Create the file structure for your project. Run the command for your operating system:
macOS
mkdir public
touch server.js public/index.html public/style.css public/script.js
Windows
mkdir public
New-Item -ItemType "File" -Path "server.js", "public\index.html", "public\style.css", "public\script.js"
- Confirm the files exist by running:
ls
You should see node_modules, package.json, package-lock.json, public, and server.js in the output.
What is this file structure?
- server.js is where your backend API lives.
- public/ holds your frontend files (HTML, CSS, JavaScript) that the browser loads directly.
- This separation keeps your server code and browser code in different places so they don't get tangled.
Set up your database
MongoDB Atlas
Your app needs a database to store destination data permanently. MongoDB Atlas gives you a free cloud database that you can connect to from anywhere.
- Open your browser and go to mongodb.com/atlas.
- Click Try Free and create an account. No credit card is required.
Once you're signed in, Atlas walks you through creating your first cluster.
- Select the M0 free tier when choosing your cluster type.
- Select any available region (pick the one closest to you for best performance).
- Click Create Deployment (or Create Cluster depending on the version of the Atlas UI you see).
What is an M0 cluster?
M0 is MongoDB Atlas's free tier. It gives you 512 MB of storage, which is more than enough for learning. It stays free forever and never asks for payment details.
Next, you need a database user so your app can authenticate.
- In the security setup, create a database user with a username and password you'll remember.
- Write down the username and password. You'll need them in a moment.
Now configure network access so your computer can reach the database.
- In the network access section, click Add IP Address.
- Select Allow Access from Anywhere (this sets the IP to 0.0.0.0/0). This is fine for development.
- Click Confirm.
Finally, grab your connection string.
- Click the Connect button on your cluster.
- Choose the Drivers option.
- Copy the connection string that looks like mongodb+srv://your-username:<password>@your-cluster.mongodb.net/.
- Keep this string handy. You'll paste it into your project in the next substep.
Replace the password placeholder
The connection string contains <password> as a placeholder. Replace it (including the angle brackets) with the database user password you just created.
SQLite (local)
SQLite stores your data in a single file on your computer. There is no cloud account, no network setup, and no connection string to configure.
The database file is created automatically the first time your server starts. You have nothing to set up here.
Why SQLite?
SQLite is perfect when you want zero setup friction. Your entire database lives in one file (tourist-guide.db) inside your project folder. No accounts, no network access rules, no passwords.
Configure environment and start script
MongoDB Atlas
Your connection string contains a password, so you store it in an environment file that stays out of your code.
- In the VS Code Explorer, right-click the empty area in the tourist-guide folder and select New File.
- Type .env and press Enter.
- Add your MongoDB connection string to the .env file by pasting the following:
MONGODB_URI=your-mongodb-atlas-connection-string-here
- Replace your-mongodb-atlas-connection-string-here with the actual connection string you copied from Atlas (the one starting with mongodb+srv://).
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
Why a .env file?
Environment files keep secrets (like database passwords) separate from your code. If you ever share your code or push it to GitHub, the .env file stays private while the rest of your project is safe to share.
SQLite (local)
SQLite does not need a connection string or environment file. The database file is created automatically in your project folder when the server starts.
- In the VS Code Explorer, right-click the empty area in the tourist-guide folder and select New File.
- Type .env and press Enter. Leave this file empty for now.
Why create an empty .env file?
The start script uses Node's --env-file flag, which requires the file to exist. An empty file works fine when you have no environment variables to load.
Now update your package.json so Node.js loads the .env file when starting your server.
- Open package.json by clicking it in the Explorer panel.
- Find the "scripts" section. It currently looks like this:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
- Replace the entire "scripts" block with this:
"scripts": {
"start": "node --env-file=.env server.js"
}
- While you're in package.json, also update the "main" field from "index.js" to "server.js".
- Update the "description" field to "A Tourist Guide full-stack web application".
- Save package.json.
What does the start script do?
node --env-file=.env server.js tells Node.js to load your environment variables from .env before running server.js. This way your code can read process.env.MONGODB_URI without installing extra packages.
Before you run the next command, what do you think it will show?
- Verify everything is wired up by running this command:
MongoDB Atlas
npm list express mongoose
You should see both express and mongoose listed with their version numbers, confirming they're installed in your project.
SQLite (local)
npm list express better-sqlite3
You should see both express and better-sqlite3 listed with their version numbers, confirming they're installed in your project.
Seeing "empty" or "missing" in the output?
Make sure you ran npm install express mongoose in the tourist-guide folder (not a parent directory). Check your terminal prompt shows tourist-guide as the current folder.
help me fix my npm install issue
✔️ Awesome, I've got everything!
Great. Double check you've saved both package.json and .env.
ⓧ I'd like to double check the full code
Here is what your package.json should look like:
{
"name": "tourist-guide",
"version": "1.0.0",
"description": "A Tourist Guide full-stack web application",
"main": "server.js",
"scripts": {
"start": "node --env-file=.env server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"mongoose": "^9.7.4"
}
}
And here is what your .env should look like (with your actual Atlas connection string replacing the placeholder):
MONGODB_URI=your-mongodb-atlas-connection-string-here
Your project is ready to go. You have Node.js, Express, Mongoose, a MongoDB Atlas cluster, and a clean file structure. Next up, you'll build the server and see your first destinations appear in the browser.
Build the Server and Display Destinations
Your project has all the right files in place, but they are empty. Time to bring this app to life.
Before touching the database, you will build a working server with hardcoded data and a frontend that displays it. This gets you a visual result immediately so you can see your Tourist Guide take shape in the browser.
In this step, get ready to:
- Create an Express server that serves a REST API endpoint and static files.
- Build an HTML page with JavaScript that fetches destinations and renders them as cards.
- Style the page with CSS so destination cards display in a responsive grid.
Create the Express server with hardcoded destination data
Your server needs to do two things: serve your frontend files (HTML, CSS, JS) to the browser, and provide an API endpoint that returns destination data as JSON.
- In VS Code, open server.js and paste the following code:
const express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;
// Parse incoming JSON request bodies
app.use(express.json());
// Serve frontend files from the public folder
app.use(express.static(path.join(__dirname, 'public')));
What does this code do?
- express.json() parses incoming JSON request bodies so you can read form data later.
- express.static(path.join(__dirname, 'public')) tells Express to serve any file inside the public folder directly to the browser. Your HTML, CSS, and JS will live there.
- Add the hardcoded destinations array and API route below the existing code in server.js:
// Sample destination data stored in memory
let destinations = [
{ name: 'Central Park', description: 'A large public park in New York City with walking trails and lakes', category: 'sightseeing', location: 'New York, USA', rating: 5 },
{ name: 'Tsukiji Outer Market', description: 'Famous street food market with fresh seafood and local delicacies', category: 'food', location: 'Tokyo, Japan', rating: 4 },
{ name: 'Berghain', description: 'Legendary techno nightclub known for its industrial setting', category: 'nightlife', location: 'Berlin, Germany', rating: 5 }
];
// Return all destinations as JSON when the frontend requests them
app.get('/api/destinations', (req, res) => {
res.json(destinations);
});
app.listen(PORT, () => {
console.log(`Tourist Guide server running at http://localhost:${PORT}`);
});
What does this code do?
- The destinations array holds three sample places with a name, description, category, location, and rating.
- The GET /api/destinations route sends the entire array back as JSON whenever the frontend asks for it.
- app.listen(PORT) starts the server on port 3000 and prints a confirmation message.
- Save server.js.
- Start the server by running this command in your terminal:
npm start
- You should see this message printed in the terminal:
Tourist Guide server running at http://localhost:3000
- Open your browser and navigate to http://localhost:3000/api/destinations.
- You should see a JSON array with three destination objects displayed in the browser.
Don't see the JSON?
- Check that you saved server.js before running npm start.
- If the terminal shows an error about .env, make sure your .env file exists at the project root. The file must be present even though we are not using the variable yet.
- Make sure nothing else is already using port 3000. Stop other servers first.
Help me debug why my Express server isn't starting.
Build the HTML page that fetches and displays destinations
The server is running and returning JSON. Now you need a page that fetches that data and renders it visually. Keep the server running in your terminal while you edit the frontend files.
- Open public/index.html and paste this page structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tourist Guide</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Tourist Guide</h1>
<p>Discover amazing places to visit</p>
</header>
<main>
<section class="destinations" id="destinations">
</section>
</main>
<script src="script.js"></script>
</body>
</html>
What does this HTML do?
- The <header> gives the page a title and subtitle.
- The <section id="destinations"> is an empty container. JavaScript will fill it with destination cards after fetching data from the API.
- The <script src="script.js"> tag loads at the bottom so the DOM is ready before the script runs.
- Save public/index.html.
- Open public/script.js and paste this JavaScript:
// Fetch destination data from the server API
async function fetchDestinations() {
const response = await fetch('/api/destinations');
const destinations = await response.json();
renderDestinations(destinations);
}
// Build HTML cards and inject them into the page
function renderDestinations(destinations) {
const container = document.getElementById('destinations');
if (destinations.length === 0) {
container.innerHTML = '<p>No destinations found. Add one below!</p>';
return;
}
container.innerHTML = destinations.map(function(dest) {
const stars = '★'.repeat(dest.rating) + '☆'.repeat(5 - dest.rating);
return '<div class="destination-card">' +
'<h3>' + dest.name + '</h3>' +
'<span class="category">' + dest.category + '</span>' +
'<p class="location">' + dest.location + '</p>' +
'<p class="description">' + dest.description + '</p>' +
'<p class="rating">' + stars + '</p>' +
'</div>';
}).join('');
}
// Load destinations as soon as the page opens
fetchDestinations();
What does this JavaScript do?
- fetchDestinations() calls your /api/destinations endpoint and passes the JSON array to renderDestinations().
- renderDestinations() loops through each destination and builds an HTML card with the name, category badge, location, description, and a star rating.
- The last line calls fetchDestinations() immediately so cards appear as soon as the page loads.
- Save public/script.js.
- Refresh http://localhost:3000 in your browser.
- You should see three destination names, locations, and star ratings rendered on the page. They look unstyled (plain black text on white), but the data is flowing from server to browser.
Page is blank or cards don't appear?
- Open the browser console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Look for red error messages.
- If you see a 404 for script.js, make sure the file is inside the public folder (not at the project root).
- Check that the server is still running in the terminal. If you accidentally closed it, run npm start again.
Help me debug why my destination cards aren't showing up.
Now add styling to turn those plain text cards into a polished layout.
- Open public/style.css and paste the page layout and header styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
header {
background-color: #2c3e50;
color: white;
text-align: center;
padding: 2rem;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
main {
max-width: 1000px;
margin: 2rem auto;
padding: 0 1rem;
}
What do these styles do?
The reset (*) removes default browser spacing so everything starts from a clean slate. The body gets a light gray background and a modern system font. The header becomes a dark blue banner, and main centers the content at 1000px max width.
- Save public/style.css and refresh your browser.
- You should see a dark blue header with white text at the top. The page background is now light gray instead of white.
- Add the destination grid and card container styles below the existing code in public/style.css:
.destinations {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.destination-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.destination-card:hover {
transform: translateY(-3px);
}
- Add the card content styles below the existing code in public/style.css:
.destination-card h3 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.destination-card .category {
display: inline-block;
padding: 0.2rem 0.8rem;
background-color: #ecf0f1;
border-radius: 15px;
font-size: 0.8rem;
color: #7f8c8d;
margin-bottom: 0.5rem;
}
.destination-card .location {
color: #7f8c8d;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.destination-card .description {
font-size: 0.9rem;
color: #555;
}
.destination-card .rating {
margin-top: 0.5rem;
color: #f39c12;
}
What do the card styles do?
- .destinations creates a responsive grid that auto-fills columns at a minimum of 280px wide.
- .destination-card gives each card a white background, rounded corners, and a subtle shadow. On hover, it lifts up 3px for a tactile feel.
- The .category badge is a small pill shape with a gray background. The .rating uses gold color for the star characters.
- Save public/style.css and refresh your browser.
- You should now see three destination cards in a responsive grid. Each card has a white background, rounded corners, a shadow, a category badge, location text, description, and gold star ratings.
Cards still look unstyled?
- Make sure you saved style.css after pasting all three blocks of CSS.
- Hard-refresh the browser with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to bypass the cache.
- Check that index.html has <link rel="stylesheet" href="style.css"> in the <head>.
Help me figure out why my CSS styles aren't applying to destination cards.
Test the full flow
Before you run the next command, think about this: what do you expect to see if you stop the server and restart it? Will the three destinations still be there?
- Stop the server by pressing Ctrl+C in the terminal.
- Restart it by running:
npm start
- Refresh http://localhost:3000 in your browser.
- You should see the same three styled destination cards in a grid. The data comes back because it is hardcoded in server.js.
- Open the Network tab in DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows, then click the Network tab).
- Refresh the page once more.
- You should see a request to /api/destinations that returns status 200 with a JSON response body containing three objects.
What is happening under the hood?
When you open http://localhost:3000, Express serves index.html from the public folder. The browser loads script.js, which immediately calls fetchDestinations().
That function makes a GET request to /api/destinations, the server responds with JSON, and renderDestinations() builds the card HTML and injects it into the page.
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files: server.js, public/index.html, public/script.js, and public/style.css.
ⓧ I'd like to double check the full code
Here is the full server.js you should have:
const express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
let destinations = [
{ name: 'Central Park', description: 'A large public park in New York City with walking trails and lakes', category: 'sightseeing', location: 'New York, USA', rating: 5 },
{ name: 'Tsukiji Outer Market', description: 'Famous street food market with fresh seafood and local delicacies', category: 'food', location: 'Tokyo, Japan', rating: 4 },
{ name: 'Berghain', description: 'Legendary techno nightclub known for its industrial setting', category: 'nightlife', location: 'Berlin, Germany', rating: 5 }
];
app.get('/api/destinations', (req, res) => {
res.json(destinations);
});
app.listen(PORT, () => {
console.log(`Tourist Guide server running at http://localhost:${PORT}`);
});
Here is the full public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tourist Guide</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Tourist Guide</h1>
<p>Discover amazing places to visit</p>
</header>
<main>
<section class="destinations" id="destinations">
</section>
</main>
<script src="script.js"></script>
</body>
</html>
Here is the full public/script.js:
async function fetchDestinations() {
const response = await fetch('/api/destinations');
const destinations = await response.json();
renderDestinations(destinations);
}
function renderDestinations(destinations) {
const container = document.getElementById('destinations');
if (destinations.length === 0) {
container.innerHTML = '<p>No destinations found. Add one below!</p>';
return;
}
container.innerHTML = destinations.map(function(dest) {
const stars = '★'.repeat(dest.rating) + '☆'.repeat(5 - dest.rating);
return '<div class="destination-card">' +
'<h3>' + dest.name + '</h3>' +
'<span class="category">' + dest.category + '</span>' +
'<p class="location">' + dest.location + '</p>' +
'<p class="description">' + dest.description + '</p>' +
'<p class="rating">' + stars + '</p>' +
'</div>';
}).join('');
}
fetchDestinations();
Here is the full public/style.css. Scroll up and confirm each section is present in order:
1. The reset, body, header, and main styles. 2. The .destinations grid, .destination-card, and hover styles. 3. The card content styles (h3, .category, .location, .description, .rating).
Your Tourist Guide app is alive. You have a working server, a REST API, and a styled frontend all talking to each other. Next up, you will add a form so users can submit their own destinations.
Add New Destinations via a Form
Your Tourist Guide shows destinations, but users can only look at what you pre-loaded. A real app lets people add their own discoveries.
In this step, you will build an HTML form that submits new destinations to your server. The server will accept the data and add it to the list so it appears instantly in the browser.
In this step, get ready to:
- Add a POST route to the server that accepts new destination data.
- Build and style an add-destination form in the frontend.
- Discover what happens to your data when the server restarts.
Add a POST route to the server
Your server already handles GET requests to send destinations to the browser. Now it needs a POST route to receive new destinations from the form.
- In VS Code, open server.js.
- Find the existing app.get route and add this POST route directly below its closing });:
// Accept new destinations from the form
app.post('/api/destinations', (req, res) => {
destinations.push(req.body);
res.status(201).json(req.body);
});
What does this code do?
- app.post('/api/destinations', ...) listens for POST requests at the same URL the GET route uses.
- destinations.push(req.body) adds the incoming JSON object to the in-memory array.
- res.status(201).json(req.body) sends back a 201 Created status with the saved destination.
- Save server.js.
- Switch to your terminal and stop the running server by pressing Ctrl+C.
- Restart the server by running:
npm start
You should see Tourist Guide server running at http://localhost:3000 printed in the terminal again.
Server won't start?
- Check for a syntax error in server.js. Make sure the POST route has matching parentheses and curly braces.
- Confirm you saved the file before restarting.
Why won't my Express server start after adding the POST route?
Build the add-destination form in the frontend
The backend is ready to receive data. Now you need an HTML form that collects destination details and JavaScript that sends them to your POST route.
- Open public/index.html.
- Find the closing </section> tag of the destinations section, and add this form section directly below it (still inside <main>):
<section class="add-form">
<h2>Add a Destination</h2>
<form id="add-destination-form">
<input type="text" name="name" placeholder="Destination name" required>
<input type="text" name="location" placeholder="Location" required>
<select name="category" required>
<option value="">Select category</option>
<option value="food">Food</option>
<option value="sightseeing">Sightseeing</option>
<option value="nightlife">Nightlife</option>
</select>
<textarea name="description" placeholder="Description" required></textarea>
<input type="number" name="rating" min="1" max="5" placeholder="Rating (1-5)" required>
<button type="submit">Add Destination</button>
</form>
</section>
What does this form include?
- Text inputs for the destination name and location.
- A <select> dropdown with the three category options matching what the server expects.
- A <textarea> for longer descriptions.
- A number input constrained to 1-5 for the rating.
- Every field has required so the browser validates before submitting.
- Save index.html and refresh your browser at http://localhost:3000.
You should see your three destination cards followed by an unstyled form with input fields and a submit button below them.
- Open public/style.css and add the following form styles at the bottom of the file:
.add-form {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.add-form h2 {
margin-bottom: 1rem;
color: #2c3e50;
}
#add-destination-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
#add-destination-form input,
#add-destination-form select,
#add-destination-form textarea {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
}
#add-destination-form textarea {
min-height: 100px;
resize: vertical;
}
- Below that, add the button styles:
#add-destination-form button {
padding: 0.75rem;
background-color: #27ae60;
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
#add-destination-form button:hover {
background-color: #219a52;
}
- Save style.css and refresh your browser.
The form now has a white card background, rounded inputs, and a green submit button.
Form still looks unstyled?
- Make sure you saved style.css before refreshing.
- Confirm the class name add-form in your CSS matches class="add-form" in your HTML exactly.
Why isn't my form CSS applying to the page?
The form looks great, but clicking the submit button does nothing yet. You need JavaScript to capture the form data and send it to your POST route.
- Open public/script.js.
- Replace the entire file contents with this updated version that adds a currentCategory variable and updates fetchDestinations to accept a parameter:
// Track the active filter (used later for category filtering)
let currentCategory = 'all';
// Fetch destinations from the API
async function fetchDestinations(category) {
let url = '/api/destinations';
const response = await fetch(url);
const destinations = await response.json();
renderDestinations(destinations);
}
What changed here?
- currentCategory stores the active filter state. You will use it in a later step for category filtering.
- fetchDestinations now accepts a category parameter and stores the URL in a variable, preparing for query parameters later.
- Below the renderDestinations function (keep it unchanged), replace the fetchDestinations(); call at the bottom with the form submit handler and updated initial call:
// Handle form submission to add a new destination
document.getElementById('add-destination-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
data.rating = Number(data.rating);
const response = await fetch('/api/destinations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
e.target.reset();
fetchDestinations(currentCategory);
}
});
fetchDestinations('all');
What does the form handler do?
- e.preventDefault() stops the browser from reloading the page on submit.
- FormData collects all form field values, and Object.fromEntries converts them into a plain object.
- data.rating = Number(data.rating) converts the rating from a string to a number so it matches the server's expected format.
- After a successful POST, the form resets and fetchDestinations re-fetches the full list to show the new card.
- Save script.js and refresh your browser.
- Fill out the form with a test destination: enter Test Cafe for name, Sydney, Australia for location, select Food for category, type a short description, and enter 4 for rating.
- Click Add Destination.
A fourth destination card appears in the grid with your submitted details, and the form fields clear automatically.
Card not appearing after submit?
- Open DevTools (F12) and check the Console tab for errors. A common mistake is a typo in the form ID.
- Confirm the id="add-destination-form" in your HTML matches the getElementById call in your JavaScript exactly.
- Check the Network tab in DevTools. If the POST request shows a 404, make sure you restarted the server after adding the POST route.
Why isn't my form submission creating a new destination card?
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files: server.js, index.html, style.css, and script.js.
ⓧ I'd like to double check the full code
Scroll up and confirm each code block is present in order. Your files should contain:
- server.js: the POST route below the GET route.
- index.html: the form section below the destinations section, inside <main>.
- style.css: form container styles, then input styles, then button styles at the bottom of the file.
- script.js: let currentCategory at the top, updated fetchDestinations(category), the unchanged renderDestinations function, the form submit listener, and fetchDestinations('all') at the bottom.
Here is the full server.js for comparison:
const express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
let destinations = [
{ name: 'Central Park', description: 'A large public park in New York City with walking trails and lakes', category: 'sightseeing', location: 'New York, USA', rating: 5 },
{ name: 'Tsukiji Outer Market', description: 'Famous street food market with fresh seafood and local delicacies', category: 'food', location: 'Tokyo, Japan', rating: 4 },
{ name: 'Berghain', description: 'Legendary techno nightclub known for its industrial setting', category: 'nightlife', location: 'Berlin, Germany', rating: 5 }
];
app.get('/api/destinations', (req, res) => {
res.json(destinations);
});
app.post('/api/destinations', (req, res) => {
destinations.push(req.body);
res.status(201).json(req.body);
});
app.listen(PORT, () => {
console.log(`Tourist Guide server running at http://localhost:${PORT}`);
});
Experience the data loss problem
You just added a destination and it appeared in the grid. Now think about what will happen if you stop and restart the server.
Before you try this, predict: will the destination you just added still be there after a restart?
- In your terminal, stop the server by pressing Ctrl+C.
- Restart it by running:
npm start
- Refresh your browser at http://localhost:3000.
The destination you added is gone. Only the original three remain.
Why did the data disappear?
The destinations array lives in your server's memory. When Node.js stops, that memory is wiped.
When you restart, the server re-reads server.js from scratch and recreates the array with only the three hardcoded items. This is the exact problem that a database solves.
You have confirmed the critical limitation of storing data in a JavaScript variable. In the next step, you will connect MongoDB Atlas so that every destination you add persists permanently in the cloud.
Connect MongoDB Atlas for Persistent Storage
In the previous step, you discovered a critical problem: any destination you add disappears the moment the server restarts. That is because the data lives in a JavaScript variable that only exists while the process is running.
MongoDB Atlas solves this by storing your destinations in a cloud database. Data written to MongoDB persists permanently, no matter how many times you restart your server.
In this step, get ready to:
- Add a Mongoose schema and connect to your MongoDB Atlas cluster.
- Update the API routes to read and write from the database.
- Test that destinations persist across server restarts.
Add your MongoDB Atlas connection string
Your .env file currently has a placeholder value for the connection string. You need to replace it with the real connection string from your Atlas cluster.
- Return to the MongoDB Atlas dashboard in your browser.
- Click Connect on your cluster.
- Select Drivers as the connection method.
- Copy the connection string provided (it starts with mongodb+srv://).
- In VS Code, open .env and replace the entire placeholder value so the file reads:
MONGODB_URI=mongodb+srv://your-username:your-password@your-cluster.mongodb.net/tourist-guide
- Replace your-username, your-password, and your-cluster with the actual values from your Atlas connection string.
- Save .env.
Where is my password?
The password is the one you created for your database user in Step 1, not your Atlas account password. If your password contains special characters, they must be percent-encoded in the connection string.
Add the Mongoose schema and database connection
Mongoose is a library that lets you define the shape of your data (a schema) and interact with MongoDB using JavaScript methods. You will now rewrite server.js to use Mongoose instead of the hardcoded array.
- In server.js, select all the code and delete it.
- Replace it with the following code that sets up Express and defines the destination schema:
const express = require('express');
const mongoose = require('mongoose');
const path = require('path');
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Define the shape of a destination document
const destinationSchema = new mongoose.Schema({
name: { type: String, required: true },
description: { type: String, required: true },
category: { type: String, required: true, enum: ['food', 'sightseeing', 'nightlife'] },
location: { type: String, required: true },
rating: { type: Number, min: 1, max: 5, default: 3 }
});
// Create a model from the schema
const Destination = mongoose.model('Destination', destinationSchema);
What does this code do?
- mongoose.Schema defines the exact fields each destination must have, including their types and validation rules.
- The enum on category restricts values to only food, sightseeing, or nightlife.
- mongoose.model creates the Destination model you will use to read and write documents in MongoDB.
- Add the following code directly below the model to seed the database and connect to Atlas:
// Seed the database with sample data if empty
async function seedData() {
const count = await Destination.countDocuments();
if (count === 0) {
await Destination.insertMany([
{ name: 'Central Park', description: 'A large public park in New York City with walking trails and lakes', category: 'sightseeing', location: 'New York, USA', rating: 5 },
{ name: 'Tsukiji Outer Market', description: 'Famous street food market with fresh seafood and local delicacies', category: 'food', location: 'Tokyo, Japan', rating: 4 },
{ name: 'Berghain', description: 'Legendary techno nightclub known for its industrial setting', category: 'nightlife', location: 'Berlin, Germany', rating: 5 },
{ name: 'Eiffel Tower', description: 'Iconic iron tower with panoramic city views from observation decks', category: 'sightseeing', location: 'Paris, France', rating: 5 },
{ name: 'Borough Market', description: 'Historic food market with artisan producers and street food stalls', category: 'food', location: 'London, UK', rating: 4 },
{ name: 'Skybar Lebua', description: 'Rooftop bar on the 63rd floor with stunning city views', category: 'nightlife', location: 'Bangkok, Thailand', rating: 4 }
]);
console.log('Database seeded with sample destinations');
}
}
// Connect to MongoDB and seed data on success
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
console.log('Connected to MongoDB Atlas');
return seedData();
})
.catch(err => console.error('MongoDB connection error:', err));
// Start the server
app.listen(PORT, () => {
console.log(`Tourist Guide server running at http://localhost:${PORT}`);
});
What does this code do?
- seedData() checks if the database is empty and inserts 6 sample destinations only on first run.
- mongoose.connect() uses the connection string from your .env file to reach your Atlas cluster.
- The .then() chain calls seedData() only after the connection succeeds, so data inserts never run against a disconnected database.
- Save server.js.
- Run your server to verify the database connection by running:
npm start
Before you look at the terminal output, what messages do you expect to see printed?
You should see three lines: Tourist Guide server running at http://localhost:3000, Connected to MongoDB Atlas, and Database seeded with sample destinations.
Seeing a MongoDB connection error?
- Double-check that your connection string in .env has the correct username, password, and cluster address. Typos in any of these cause authentication failures.
- Make sure your IP address is on the Atlas Network Access list. If you used 0.0.0.0/0 during setup, this should not be an issue.
- Confirm your database user password does not contain unencoded special characters like @ or #.
Ask for help: Why am I getting a MongoDB connection error in my Tourist Guide app?
Update the routes to use Mongoose
The server connects to MongoDB, but the API routes do not exist yet. You need to add GET and POST routes that query the database instead of a hardcoded array.
- Stop the server with Ctrl+C.
- In server.js, find the comment // Start the server and add the following code directly above it:
// Return all destinations from the database
app.get('/api/destinations', async (req, res) => {
const destinations = await Destination.find();
res.json(destinations);
});
// Save a new destination to the database
app.post('/api/destinations', async (req, res) => {
const destination = new Destination(req.body);
const saved = await destination.save();
res.status(201).json(saved);
});
What does this code do?
- Destination.find() queries MongoDB and returns every destination document in the collection.
- new Destination(req.body) creates a new document that Mongoose validates against your schema before saving.
- destination.save() writes the document to MongoDB permanently. It stays there even when the server stops.
- Save server.js and restart the server by running:
npm start
- Open http://localhost:3000 in your browser.
You should see 6 destination cards displayed in the grid, all pulled from your MongoDB Atlas database.
Page loads but no cards appear?
- Open browser DevTools (F12) and check the Console tab for errors. A 404 on /api/destinations means the route code is not in the right location in your file.
- Make sure the route code sits above the app.listen call, not below it.
Ask for help: My Tourist Guide page loads but no destination cards appear. How do I debug this?
Test persistence across server restarts
The real test is whether data survives a restart. This is the exact scenario that broke in the previous step.
- In your browser, scroll down to the Add a Destination form.
- Fill it out with any destination (for example: name Golden Gate Bridge, location San Francisco, USA, category Sightseeing, description Iconic suspension bridge with stunning bay views, rating 5).
- Click Add Destination.
- Confirm the new card appears in the grid (you should now see 7 cards total).
- Stop the server with Ctrl+C.
- Restart it by running:
npm start
- Refresh http://localhost:3000 in your browser.
Do you think your added destination will still be there, or will it vanish like before?
Your destination is still there. All 7 cards display because the data lives in MongoDB Atlas, not in server memory.
Why does seedData not re-insert every time?
The seedData() function checks countDocuments() first. Since the collection already has documents after the first run, the seed logic is skipped on subsequent starts.
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js and .env.
ⓧ I'd like to double check the full code
Your server.js should contain the following sections in this order. Scroll up and confirm each chunk is present:
- Requires (express, mongoose, path) and app setup with express.json() and express.static().
- destinationSchema with 5 fields (name, description, category, location, rating) and the Destination model.
- seedData() function that inserts 6 sample destinations if the collection is empty.
- mongoose.connect() with .then() and .catch().
- GET /api/destinations route using Destination.find().
- POST /api/destinations route using new Destination() and .save().
- app.listen() on port 3000.
Your .env file should have a single line: MONGODB_URI= followed by your real Atlas connection string.
Your Tourist Guide now has a real cloud database behind it. Next up, you will add category filter buttons so users can browse only the destinations they care about.
Filter Destinations by Category
Your Tourist Guide now has persistent storage. Destinations survive server restarts, and the database holds a growing collection of places.
With six (or more) destinations in the database, scrolling through every card is already tedious. Users need a way to narrow the list to what interests them. In this step, you will add category filter buttons that pass query parameters from the frontend to the backend to the database query.
In this step, get ready to:
- Add query parameter handling to the GET route in your server.
- Build category filter buttons in the frontend that talk to the API.
- Test that clicking a category shows only matching destinations.
Add query parameter handling to the GET route
Query parameters are the key-value pairs that appear after a ? in a URL. When the frontend requests /api/destinations?category=food, the server needs to read that parameter and filter the database results.
- In server.js, find the current GET route:
app.get('/api/destinations', async (req, res) => {
const destinations = await Destination.find();
res.json(destinations);
});
- Replace the entire route handler with this updated version that checks for a category query parameter:
app.get('/api/destinations', async (req, res) => {
// Build a filter object based on query parameters
const filter = {};
if (req.query.category) {
filter.category = req.query.category;
}
// Pass the filter to Mongoose - empty object returns all
const destinations = await Destination.find(filter);
res.json(destinations);
});
What does this code do?
Express automatically parses query parameters from the URL and makes them available on req.query. So a request to /api/destinations?category=food gives you req.query.category === 'food'.
When no category parameter is provided, filter stays as an empty object {}. Passing an empty object to Destination.find({}) returns all documents, which is the "show everything" behavior.
- Save server.js.
- Switch to your terminal and stop the server with Ctrl+C.
- Restart the server by running:
npm start
- Open your browser and visit http://localhost:3000/api/destinations?category=food directly.
- You should see only the food destinations (Tsukiji Outer Market and Borough Market) returned as JSON.
Seeing all destinations instead of just food?
- Make sure you replaced the entire route handler, not just the body. The app.get('/api/destinations', ...) line should appear only once in your file.
- Check that you saved server.js and restarted the server. Changes to server code require a restart.
- Confirm the URL has a ? before category=food, not a /.
Help me debug my query parameter filtering
Add category filter buttons to the frontend
The backend now supports filtering. Next, you need buttons in the browser that let users pick a category without manually editing the URL.
- In public/index.html, find the opening <main> tag.
- Add this filter section directly below <main> and above the destinations section:
<section class="filters">
<button class="filter-btn active" data-category="all">All</button>
<button class="filter-btn" data-category="food">Food</button>
<button class="filter-btn" data-category="sightseeing">Sightseeing</button>
<button class="filter-btn" data-category="nightlife">Nightlife</button>
</section>
What does this code do?
Each button stores its category value in a data-category attribute. JavaScript can read this later with btn.dataset.category to know which category the user clicked.
The first button starts with the active class because "All" is the default view when the page loads.
- Save public/index.html and refresh your browser.
- You should see four plain text buttons (All, Food, Sightseeing, Nightlife) above the destination cards. They will not be styled yet.
Don't see the buttons?
- Make sure the filter section is inside <main> and above the <section class="destinations"> element.
- Check for unclosed tags. Each <button> needs a matching </button>.
Now update the fetchDestinations function to actually send the category to the server. In public/script.js, find the current fetchDestinations function and replace it with this updated version:
async function fetchDestinations(category) {
let url = '/api/destinations';
// Append category filter to URL if not showing all
if (category && category !== 'all') {
url += '?category=' + category;
}
const response = await fetch(url);
const destinations = await response.json();
renderDestinations(destinations);
}
What does this code do?
The function now checks whether the category is something other than 'all'. If so, it appends ?category=food (or whichever value) to the URL before fetching. When category is 'all', no query parameter is sent, and the server returns everything.
- Next, add click handlers for the filter buttons. In public/script.js, add this code between the renderDestinations function and the form submit handler:
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
// Remove active class from all buttons
document.querySelectorAll('.filter-btn').forEach(function(b) {
b.classList.remove('active');
});
// Set clicked button as active and fetch filtered results
btn.classList.add('active');
currentCategory = btn.dataset.category;
fetchDestinations(currentCategory);
});
});
What does this code do?
- It loops over every button with class .filter-btn and attaches a click listener to each one.
- When clicked, it removes active from all buttons, then adds it to the one that was clicked.
- It reads the button's data-category value and calls fetchDestinations with that category.
- The currentCategory variable tracks which filter is active so the form submit handler can re-fetch with the correct filter after adding a destination.
- Save public/script.js and refresh your browser.
- Click the Food button. The grid should update to show only Tsukiji Outer Market and Borough Market.
- Click All and all six destinations should reappear.
Clicking buttons does nothing?
- Open your browser's DevTools console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for JavaScript errors.
- Make sure the click handler code is placed after the renderDestinations function and before the form submit handler.
- Check that the class name in your HTML (filter-btn) matches the selector in JavaScript (.filter-btn).
Help me debug my filter button click handlers
The buttons work but they look like plain unstyled text. Add some CSS to make them look like proper filter pills.
- In public/style.css, add these styles after the main rule block:
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.5rem 1.5rem;
border: 2px solid #2c3e50;
background: white;
color: #2c3e50;
border-radius: 25px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.filter-btn.active,
.filter-btn:hover {
background-color: #2c3e50;
color: white;
}
What does this code do?
- The .filters container uses flexbox to lay the buttons out in a horizontal row with spacing.
- Each .filter-btn gets a pill shape with border-radius: 25px and a dark border to match the header color.
- The .active and :hover states invert the colors so the selected button is clearly highlighted.
- Save public/style.css and refresh your browser.
- The filter buttons should now appear as dark-bordered pills above the destination grid. The All button should be filled dark (active state).
Buttons not styled?
- Make sure you saved public/style.css. Unsaved changes will not appear in the browser.
- Check that the class name filter-btn in your CSS matches the class in your HTML buttons exactly.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved: public/index.html, public/script.js, and public/style.css.
ⓧ I'd like to double check the full code
Scroll up and confirm you have each of these pieces in place, in order:
- In server.js: the updated GET route with the filter object and req.query.category check.
- In public/index.html: the <section class="filters"> block with four buttons directly below <main>.
- In public/script.js: the updated fetchDestinations with the if (category && category !== 'all') check, followed by the querySelectorAll('.filter-btn').forEach click handler block.
- In public/style.css: the .filters, .filter-btn, and .filter-btn.active rules.
Test the filtering
Before you click through all the categories, take a moment to predict what you expect to see.
- How many destinations do you think will appear when you click Sightseeing? Think about which of the six seeded destinations have that category.
- Click the Sightseeing button.
You should see exactly two cards: Central Park and Eiffel Tower. Both have category: 'sightseeing' in the database.
- Click Food. You should see Tsukiji Outer Market and Borough Market.
- Click Nightlife. You should see Berghain and Skybar Lebua.
- Click All. All six destinations should return to the grid.
What is a query parameter?
A query parameter is a way to send extra information to the server inside the URL. The format is ?key=value appended after the path. You can chain multiple with &, like ?category=food&sort=rating.
They are the standard way to pass filtering, sorting, and pagination preferences from a frontend to a REST API.
- Try one more test: add a new destination using the form with a specific category (e.g. "food").
- Click that category's filter button and confirm your newly added destination appears in the filtered results.
Your Tourist Guide now lets users drill into exactly what they are looking for. Next up, the Secret Mission challenges you to add real-time search so users can find destinations by name.
Secret mission
Add Search Functionality
Category buttons are great, but what if a user knows exactly what they're looking for? Your mission: add a search bar that filters destinations by name in real-time, working alongside the existing category filters.
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 the MongoDB Atlas free tier and runs locally, so there are no ongoing costs.
Resources you used:
- MongoDB Atlas free cluster (M0 tier, stores your destination data in the cloud).
- Local tourist-guide project folder on your Desktop (contains server.js, public/ folder, node_modules/, and config files).
Keep everything running
No action needed. Choose this if you plan to keep building on this project or want to revisit it soon.
- Your Atlas free cluster stays active at no cost. Atlas will auto-pause it after 30 days of inactivity.
- Your local tourist-guide folder remains on your Desktop ready to use.
- To start the server again later, open a terminal in the tourist-guide folder and run npm start.
Pause - I'll come back to this later
Free up local resources while keeping your cloud data intact.
- Stop the Node.js server if it is still running by pressing Ctrl+C in the terminal.
- Your Atlas cluster requires no action. Atlas automatically pauses free clusters after 30 days of inactivity. Your data stays safe and you can resume at any time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Delete your MongoDB Atlas cluster:
- Go to the Clusters page in your Atlas dashboard.
- Click the ellipsis (...) next to your cluster name.
- Click Terminate from the drop-down menu.
- Type your cluster name to confirm, then click Terminate.
Delete your local project files by running these commands in your terminal:
macOS
cd ~/Desktop
rm -rf tourist-guide
Windows
cd ~\Desktop
Remove-Item -Recurse -Force tourist-guide
Nice Work!
Nice Work!
Nice work! You just built a full-stack Tourist Guide web application from scratch using Express 5, MongoDB Atlas, and vanilla JavaScript.
You went from an empty folder to a working app where users can browse destinations, filter by category, add new listings, and have everything persist in a cloud database.
What you learned:
- Built a REST API with GET and POST endpoints that accept query parameters and return JSON responses.
- Connected to MongoDB Atlas for persistent storage that survives server restarts, replacing fragile in-memory data.
- Wired a frontend to a backend API using fetch, rendering dynamic content from database queries and handling form submissions.
- Secret Mission: Added real-time search functionality using MongoDB $regex queries combined with category filters.
Ready to quiz yourself?