Build a Bookmark Manager with MERN
Build a full-stack bookmark manager with MongoDB, Express, React, and Node.js.
Introduction
30 Second Summary
Every time you save a link to read later, you're relying on a full-stack application. A frontend captures your click, an API processes the request, and a database stores it for next time.
In this project, you will build a full-stack Bookmark Manager using MongoDB, Express, React, and Node.js. You'll connect a React frontend to a cloud database through a REST API you build yourself.
What You'll Build
A full-stack Bookmark Manager application where you can save, view, and delete web bookmarks through a React interface backed by an Express API and MongoDB Atlas database.
By the end of this project, you'll have:
- A Node.js/Express REST API with GET, POST, and DELETE endpoints.
- A MongoDB Atlas cloud database with a Mongoose schema for storing bookmarks.
- A React frontend built with Vite that displays bookmarks and lets you add or remove them.
- Secret Mission: Add an edit feature so users can update bookmark titles and URLs.
Are there any prerequisites?
You'll need basic JavaScript knowledge (variables, functions, arrays, objects). No prior backend or database experience is required.
Build the Express API
Your environment is ready and your database is waiting in the cloud. Now it's time to build the backend server that will talk to MongoDB Atlas.
Every full-stack app needs an API layer that sits between the frontend and the database. In this step, you'll build that layer using Express and connect it to your cloud database using Mongoose.
In this step, get ready to:
- Initialize the server project and install dependencies.
- Create the Express server with a database connection.
- Define a Mongoose data model for bookmarks.
Initialize the server project and install dependencies
Before writing any server code, you need a package.json file that tracks your project's dependencies. You also need a secure way to store your database connection string without hardcoding it.
- Switch to the terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Navigate into your server folder by running this command:
cd server
- Create a package.json file by running:
npm init -y
What does this command do?
The npm init -y command creates a package.json file with default values. This file is the manifest for your Node.js project. It tracks which packages your project depends on.
- Install the four packages your server needs by running:
npm install express mongoose cors dotenv
What are these packages?
- express is the web framework for building your API endpoints.
- mongoose connects your server to MongoDB and provides data modeling.
- cors allows your frontend (on a different port) to talk to your backend.
- dotenv loads environment variables from a .env file so you don't hardcode secrets.
- Confirm the installation succeeded by running:
ls
You should see node_modules, package.json, and package-lock.json in the output.
Not seeing node_modules?
Make sure you ran npm install from inside the server folder, not the project root. Run pwd (macOS/Linux) or cd (Windows) to check your current directory.
help me troubleshoot why npm install didn't create a node_modules folder in my server directory
- Now open package.json in VS Code's Explorer sidebar (click the file to open it).
- Replace the entire content of package.json with the following:
{
"name": "mern-bookmarks-server",
"version": "1.0.0",
"description": "MERN Bookmark Manager API",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.2.1",
"mongoose": "^9.6.2"
}
}
What did we change?
The npm init -y command generates placeholder values. You replaced them with a cleaner version that has a descriptive name, and a start script so you can launch your server with npm start later.
Your dependency versions may differ slightly from what's shown. That's fine as long as the packages are listed.
- Save package.json.
- Now create a new file called .env inside the server folder. Right-click the server folder in the Explorer sidebar and select New File.
- Name it .env and paste the following content:
MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5000
What is a .env file?
A .env file stores sensitive configuration values (like database passwords) outside your code. The dotenv package reads this file and makes the values available through process.env.
- Replace your-mongodb-atlas-connection-string-here with the connection string you copied from MongoDB Atlas in the previous step.
- Save .env.
Don't share your .env file
Your .env file contains your database credentials. Never commit it to Git or share it publicly.
Create the Express server entry file
Your server needs an entry point that sets up Express, connects to MongoDB, and starts listening for requests. This single file wires everything together.
- Create a new file called index.js inside the server folder. Right-click the server folder in the Explorer sidebar and select New File.
- Name it index.js and paste the following code:
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
import bookmarkRoutes from './routes/bookmarks.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
app.use('/api/bookmarks', bookmarkRoutes);
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
console.log('Connected to MongoDB Atlas');
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch((error) => {
console.error('MongoDB connection error:', error);
});
What does this code do?
- Lines 1-5 import the packages you installed plus a routes file you'll create in the next step.
- dotenv.config() loads your .env variables into process.env.
- app.use(cors()) enables cross-origin requests so your React frontend can reach this server.
- app.use(express.json()) tells Express to parse incoming JSON request bodies.
- mongoose.connect() opens a connection to MongoDB Atlas. The server only starts listening after the database connects successfully.
The routes file doesn't exist yet
Line 5 imports from ./routes/bookmarks.js which you'll create in the next step. The server won't start until that file exists. That's expected.
- Save index.js.
Create the Mongoose Bookmark model
A model defines the shape of your data. It tells MongoDB what fields a bookmark has and what rules they follow.
- Create a new folder called models inside the server folder. Right-click the server folder in the Explorer sidebar and select New Folder.
- Inside models, create a new file called Bookmark.js.
- Paste the following code into Bookmark.js:
import mongoose from 'mongoose';
const bookmarkSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
url: {
type: String,
required: true,
trim: true
},
createdAt: {
type: Date,
default: Date.now
}
});
const Bookmark = mongoose.model('Bookmark', bookmarkSchema);
export default Bookmark;
What does this model do?
- bookmarkSchema defines three fields: title (required text), url (required text), and createdAt (auto-generated timestamp).
- trim: true automatically removes leading and trailing whitespace from the title and URL.
- mongoose.model('Bookmark', bookmarkSchema) creates a model that Mongoose uses to read and write bookmark documents in MongoDB.
- Save Bookmark.js.
- Confirm your file structure by checking the Explorer sidebar. Inside server, you should see models/Bookmark.js, index.js, .env, package.json, and the node_modules folder.
File not showing in the sidebar?
Make sure you created Bookmark.js inside the models folder, not directly in server. The path should be server/models/Bookmark.js.
help me check that my server folder structure is correct for the MERN bookmarks project
✔️ Awesome, I've got everything!
Great. Make sure all your files are saved before moving on.
ⓧ I'd like to double check the full code
Here's what each file should contain at this point.
{
"name": "mern-bookmarks-server",
"version": "1.0.0",
"description": "MERN Bookmark Manager API",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.2.1",
"mongoose": "^9.6.2"
}
}
MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5000
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
import bookmarkRoutes from './routes/bookmarks.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
app.use('/api/bookmarks', bookmarkRoutes);
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
console.log('Connected to MongoDB Atlas');
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch((error) => {
console.error('MongoDB connection error:', error);
});
import mongoose from 'mongoose';
const bookmarkSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
url: {
type: String,
required: true,
trim: true
},
createdAt: {
type: Date,
default: Date.now
}
});
const Bookmark = mongoose.model('Bookmark', bookmarkSchema);
export default Bookmark;
Your Express server is built and your data model is ready. Next up, you'll create the API routes that let the outside world create, read, and delete bookmarks.
Add REST API Routes
Your Express server is connected to MongoDB Atlas, and your Bookmark model defines the shape of your data. Now you need a way for the outside world to actually interact with that data.
In this step, you'll create REST API routes that let clients fetch, create, and delete bookmarks. These routes are the bridge between your React frontend (coming in the next step) and your MongoDB database.
In this step, get ready to:
- Create route handlers for GET, POST, and DELETE operations.
- Start the Express server and test the API in your browser.
Create the bookmark routes file
Your server/index.js already imports routes from ./routes/bookmarks.js. That file doesn't exist yet, so you'll create it now with all three route handlers.
- In VS Code's Explorer sidebar, right-click the server folder and select New Folder.
- Name the folder routes.
- Right-click the new routes folder and select New File.
- Name the file bookmarks.js.
- Add the imports, router setup, and the GET route by pasting this code into bookmarks.js:
import express from 'express';
import Bookmark from '../models/Bookmark.js';
// Create a modular route handler
const router = express.Router();
// GET / - Fetch all bookmarks, newest first
router.get('/', async (req, res) => {
try {
const bookmarks = await Bookmark.find().sort({ createdAt: -1 });
res.json(bookmarks);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
What does this code do?
express.Router() creates a mini route handler that you can attach to a path in your main app. This keeps your route logic separate from your server setup.
The GET route uses Bookmark.find() to retrieve every bookmark from MongoDB, then .sort({ createdAt: -1 }) puts the newest ones first. If anything goes wrong, it responds with a 500 status and the error message.
You won't see output yet. The next chunk adds the remaining routes.
- Add the POST and DELETE routes below the GET route in bookmarks.js:
// POST / - Create a new bookmark
router.post('/', async (req, res) => {
const bookmark = new Bookmark({
title: req.body.title,
url: req.body.url
});
try {
const newBookmark = await bookmark.save();
res.status(201).json(newBookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// DELETE /:id - Delete a bookmark by its ID
router.delete('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findByIdAndDelete(req.params.id);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json({ message: 'Bookmark deleted' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
export default router;
What does this code do?
The POST route creates a new Bookmark document from the request body's title and url fields, saves it to MongoDB, and responds with a 201 (created) status. If validation fails (like a missing title), it returns a 400 error.
The DELETE route uses Bookmark.findByIdAndDelete(req.params.id) to find and remove a bookmark in one step. The :id in the route path becomes req.params.id in your handler. If no bookmark matches that ID, it returns a 404.
export default router makes this router available for index.js to import and mount at /api/bookmarks.
- Save bookmarks.js.
What is CRUD?
CRUD stands for Create, Read, Update, Delete. These are the four basic operations for working with stored data. Your routes cover three of the four: GET (Read), POST (Create), and DELETE (Delete). You'll add Update in the Secret Mission.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import express from 'express';
import Bookmark from '../models/Bookmark.js';
const router = express.Router();
router.get('/', async (req, res) => {
try {
const bookmarks = await Bookmark.find().sort({ createdAt: -1 });
res.json(bookmarks);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
router.post('/', async (req, res) => {
const bookmark = new Bookmark({
title: req.body.title,
url: req.body.url
});
try {
const newBookmark = await bookmark.save();
res.status(201).json(newBookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findByIdAndDelete(req.params.id);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json({ message: 'Bookmark deleted' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
export default router;
Start the server and test your API
Your index.js already imports and mounts these routes at /api/bookmarks (you set that up in the previous step). All that's left is to start the server and confirm everything connects.
- In your terminal, make sure you're inside the server folder, then start the Express server by running:
node index.js
- You should see two messages in your terminal:
Connected to MongoDB Atlas
Server running on port 5000
Not seeing those messages?
- If you see a MongoDB connection error, double check that your MONGODB_URI in .env is correct. Make sure you replaced the password placeholder with your actual database user password.
- If you see an error about import statements or modules, add "type": "module" to your server/package.json (at the top level, next to "name"). This tells Node.js to use ES module syntax.
- If you see an error that the routes/bookmarks.js file is not found, make sure the file is inside a routes folder within server.
Help me debug my Express server startup error
- Test the GET endpoint by opening your browser and navigating to:
http://localhost:5000/api/bookmarks
You should see an empty JSON array: []. This is correct because you haven't added any bookmarks yet.
Why is an empty array a good sign?
An empty array means the full chain is working: your browser sent a GET request to Express, Express queried MongoDB through Mongoose, MongoDB returned zero documents, and Express sent that result back as JSON. Every layer connected successfully.
Seeing an error in the browser?
- Make sure your server is still running in the terminal. If you closed it, run node index.js again from the server folder.
- Check that the URL includes port 5000 and the path is exactly /api/bookmarks.
Why can't I see my API response in the browser?
Your API is live and responding to requests. Next up, you'll build the React frontend that talks to these endpoints and gives users an actual interface for managing their bookmarks.
Create the React Frontend
Your Express API is up and running, which means your backend can store and retrieve bookmarks from MongoDB Atlas.
Now you need a way for users to actually interact with those bookmarks. In this step, you'll build a React frontend that talks to your API, letting users see their bookmarks, add new ones, and delete the ones they no longer need.
In this step, get ready to:
- Scaffold a React app using Vite inside the client folder.
- Build a component that fetches and displays bookmarks from your API.
- Add delete functionality so users can remove bookmarks.
Scaffold the React app with Vite
Vite is a modern build tool that scaffolds React projects quickly and provides instant hot-reload during development.
- Open a new terminal in VS Code by pressing Ctrl+` (backtick key, top-left of keyboard).
- Make sure you are in the mern-bookmarks project root (not inside server) by running:
cd ~/Desktop/mern-bookmarks
- Scaffold a new React app inside the client folder by running:
npm create vite@latest client -- --template react
What prompts will I see?
Since you specified both the directory name (client) and the template (react) in the command, Vite skips most interactive prompts.
You may see a prompt asking whether to install dependencies and start the dev server now. Select No for this prompt. You will install dependencies manually in the next command.
- Move into the client folder and install dependencies by running:
cd client
npm install
- Confirm the scaffold succeeded by running:
ls src
You should see files including App.jsx, App.css, and main.jsx listed in the output.
Don't see these files?
Make sure you ran the scaffold command from the mern-bookmarks root, not from inside the server folder. If the client folder is empty, delete it and re-run the scaffold command from the project root.
help me scaffold a Vite React app inside my client folder
Build the bookmark fetching and add form
The scaffolded App.jsx contains placeholder content. You will replace it entirely with a component that connects to your Express API.
- Open client/src/App.jsx in VS Code.
- Select all the existing content (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
- Paste the following code that sets up state and fetches bookmarks from the API:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
// State for the list of bookmarks and form inputs
const [bookmarks, setBookmarks] = useState([])
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
// Fetch all bookmarks from the Express API
const fetchBookmarks = async () => {
try {
const response = await fetch('/api/bookmarks')
const data = await response.json()
setBookmarks(data)
} catch (error) {
console.error('Error fetching bookmarks:', error)
}
}
// Load bookmarks when the component first renders
useEffect(() => {
fetchBookmarks()
}, [])
What does this code do?
useState creates three pieces of state: bookmarks holds the array from the database, and title / url hold what the user types in the form.
fetchBookmarks calls your Express GET endpoint and stores the returned bookmarks in state. The useEffect with an empty dependency array [] runs this function once when the page loads.
- Directly below the useEffect block, add the function that handles form submission:
// Handle adding a new bookmark via POST request
const addBookmark = async (e) => {
e.preventDefault()
if (!title || !url) return
try {
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, url })
})
if (response.ok) {
setTitle('')
setUrl('')
fetchBookmarks()
}
} catch (error) {
console.error('Error adding bookmark:', error)
}
}
What does addBookmark do?
e.preventDefault() stops the form from reloading the page. The function sends a POST request with the title and URL as JSON to your Express API.
If the request succeeds, it clears the form fields and calls fetchBookmarks() again to refresh the list with the newly added bookmark.
- Below addBookmark, add the JSX that renders the form and bookmark list:
return (
<div className="app">
<h1>Bookmark Manager</h1>
<form onSubmit={addBookmark} className="bookmark-form">
<input
type="text"
placeholder="Bookmark title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="url"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<button type="submit">Add Bookmark</button>
</form>
<ul className="bookmark-list">
{bookmarks.map((bookmark) => (
<li key={bookmark._id} className="bookmark-item">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.title}
</a>
</li>
))}
</ul>
</div>
)
}
export default App
What does the JSX render?
The form calls addBookmark on submit. Each input is controlled by React state, so the component tracks what the user types in real time.
The bookmarks.map() loop creates a list item for each bookmark. Each item displays the title as a clickable link that opens in a new tab.
- Save App.jsx.
The component won't display correctly yet because it needs styles. You will add those next.
- Open client/src/App.css in VS Code.
- Select all the existing content and delete it.
- Paste the following styles for the bookmark manager layout:
.app {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.bookmark-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 2rem;
}
.bookmark-form input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.bookmark-form button {
padding: 0.75rem;
background-color: #4a90d9;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.bookmark-form button:hover {
background-color: #357abd;
}
.bookmark-list {
list-style: none;
padding: 0;
}
.bookmark-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.bookmark-item a {
color: #4a90d9;
text-decoration: none;
}
.bookmark-item a:hover {
text-decoration: underline;
}
.delete-btn {
padding: 0.4rem 0.8rem;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.delete-btn:hover {
background-color: #c0392b;
}
What do these styles do?
.app centers the content with a max width. .bookmark-form stacks the inputs vertically with spacing between them.
.bookmark-item uses flexbox to place the link and the delete button on opposite sides. The .delete-btn styles are ready for the delete button you will add next.
- Save App.css.
Seeing a blank page or errors?
Make sure you deleted ALL of the original scaffolded content from App.jsx before pasting. Leftover imports or component code will cause conflicts.
help me fix errors in my React bookmark component
Add a delete button to each bookmark
Users need a way to remove bookmarks they no longer want. You will add a deleteBookmark function and a delete button to each list item.
- In App.jsx, find the closing } of the addBookmark function (just above the return statement).
- Add the following deleteBookmark function directly below it:
// Handle deleting a bookmark via DELETE request
const deleteBookmark = async (id) => {
try {
const response = await fetch(`/api/bookmarks/${id}`, {
method: 'DELETE'
})
if (response.ok) {
fetchBookmarks()
}
} catch (error) {
console.error('Error deleting bookmark:', error)
}
}
What does deleteBookmark do?
It sends a DELETE request to your Express API with the bookmark's _id in the URL. If the server responds successfully, it calls fetchBookmarks() to refresh the list without the deleted item.
- In the return statement, find the <li> inside the bookmarks.map(). Locate this line:
</a>
</li>
- Add a delete button between the closing </a> and the closing </li>, so the full list item looks like this:
<li key={bookmark._id} className="bookmark-item">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.title}
</a>
<button onClick={() => deleteBookmark(bookmark._id)} className="delete-btn">
Delete
</button>
</li>
What changed in the list item?
You added a <button> that calls deleteBookmark with the bookmark's _id when clicked. The arrow function () => deleteBookmark(bookmark._id) ensures the correct ID is passed for each bookmark in the list.
- Save App.jsx.
Your React component is now complete with full CRUD capabilities on the frontend (create, read, and delete).
✔️ Awesome, I've got everything!
Great. Double check you have saved both App.jsx and App.css.
ⓧ I'd like to double check the full code
Here is the complete App.jsx:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [bookmarks, setBookmarks] = useState([])
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
const fetchBookmarks = async () => {
try {
const response = await fetch('/api/bookmarks')
const data = await response.json()
setBookmarks(data)
} catch (error) {
console.error('Error fetching bookmarks:', error)
}
}
useEffect(() => {
fetchBookmarks()
}, [])
const addBookmark = async (e) => {
e.preventDefault()
if (!title || !url) return
try {
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, url })
})
if (response.ok) {
setTitle('')
setUrl('')
fetchBookmarks()
}
} catch (error) {
console.error('Error adding bookmark:', error)
}
}
const deleteBookmark = async (id) => {
try {
const response = await fetch(`/api/bookmarks/${id}`, {
method: 'DELETE'
})
if (response.ok) {
fetchBookmarks()
}
} catch (error) {
console.error('Error deleting bookmark:', error)
}
}
return (
<div className="app">
<h1>Bookmark Manager</h1>
<form onSubmit={addBookmark} className="bookmark-form">
<input
type="text"
placeholder="Bookmark title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="url"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<button type="submit">Add Bookmark</button>
</form>
<ul className="bookmark-list">
{bookmarks.map((bookmark) => (
<li key={bookmark._id} className="bookmark-item">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.title}
</a>
<button onClick={() => deleteBookmark(bookmark._id)} className="delete-btn">
Delete
</button>
</li>
))}
</ul>
</div>
)
}
export default App
And the complete App.css:
.app {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.bookmark-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 2rem;
}
.bookmark-form input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.bookmark-form button {
padding: 0.75rem;
background-color: #4a90d9;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.bookmark-form button:hover {
background-color: #357abd;
}
.bookmark-list {
list-style: none;
padding: 0;
}
.bookmark-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.bookmark-item a {
color: #4a90d9;
text-decoration: none;
}
.bookmark-item a:hover {
text-decoration: underline;
}
.delete-btn {
padding: 0.4rem 0.8rem;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.delete-btn:hover {
background-color: #c0392b;
}
Your React frontend is ready. Next up, you will connect it to your Express backend using a Vite proxy and test the full stack end-to-end.
Connect Frontend to Backend and Test
Your React frontend and Express backend are built, but they still live on different ports. The Vite dev server runs on port 5173, while Express serves the API on port 5000.
When your React code calls fetch('/api/bookmarks'), that request goes to port 5173 by default. Without a proxy, Vite has no idea where your API lives. In this step, you will bridge the gap with a Vite proxy and verify the full stack end-to-end.
In this step, get ready to:
- Configure a Vite proxy to forward API requests to Express.
- Run both servers simultaneously.
- Add, view, and delete bookmarks through the full-stack app.
Add the Vite proxy configuration
During development, the React frontend and Express backend run as separate processes on different ports. A proxy tells Vite to intercept any request starting with /api and forward it to your Express server on port 5000.
- In VS Code, open client/vite.config.js from the client folder.
- Replace the entire file contents with the following code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// Configure Vite with the React plugin and a dev server proxy
export default defineConfig({
plugins: [react()],
server: {
// Forward /api requests to the Express backend
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})
What does this code do?
The server.proxy block intercepts any request whose path starts with /api. Instead of serving it from Vite, it forwards the request to http://localhost:5000 where your Express server is listening.
The changeOrigin: true option updates the request's Host header to match the target server. This prevents issues when the backend checks the origin of incoming requests.
- Save client/vite.config.js.
Why a proxy instead of hardcoding the URL?
Your React code uses relative paths like fetch('/api/bookmarks') instead of fetch('http://localhost:5000/api/bookmarks'). This keeps the code portable. In production, you would serve both frontend and backend from the same origin, so relative paths just work.
✔️ Awesome, I've got everything!
Great. Make sure you have saved client/vite.config.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete client/vite.config.js file:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})
Run both servers and test the full stack
You need two processes running at the same time: Express serving the API and Vite serving the React frontend. You will use two separate terminal windows for this.
- Switch back to the terminal where your Express server is running. You should still see Connected to MongoDB Atlas and Server running on port 5000 in the output.
Express server not running?
If you closed the terminal or stopped the server, restart it by navigating to your server folder and running node index.js again. You should see the Connected to MongoDB Atlas message before proceeding.
help me restart my Express server
- Open a second terminal window by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows) in VS Code.
- Navigate to the client folder and start the Vite dev server by running these commands:
cd client
npm run dev
What should I see?
Vite prints a local URL in the terminal, typically http://localhost:5173/. This is the address where your React app is served.
- Open your browser and go to http://localhost:5173.
You should see the Bookmark Manager heading, a form with two input fields, and an empty bookmark list.
Seeing a blank page or errors in the console?
Make sure your Express server is still running in the other terminal. Check the browser console (F12) for network errors. If you see a failed request to /api/bookmarks, the proxy may not be configured correctly.
help me debug my Vite proxy not connecting to Express
Now test each operation to confirm the full data flow is working.
- Type NextWork Docs in the Bookmark title field.
- Type https://www.nextwork.org in the URL field.
- Click Add Bookmark.
Your new bookmark should appear in the list below the form as a clickable link. This confirms that the POST request reached Express, saved to MongoDB Atlas, and the updated list was fetched back to React.
- Add a second bookmark with the title Vite Docs and URL https://vitejs.dev.
- Verify both bookmarks appear in the list, with the newest one on top.
- Click the Delete button next to Vite Docs.
The bookmark disappears immediately. Only NextWork Docs remains. Your full-stack MERN application is working end-to-end.
You just built a complete full-stack MERN application. Data flows from your React UI through the Vite proxy to Express, gets stored in MongoDB Atlas, and comes back to render on screen. Next up, take on a Secret Mission to add edit functionality.
Secret mission
Add Edit Functionality
Your bookmark manager can create, read, and delete bookmarks. But what happens when you save a bookmark with a typo in the title, or the URL changes? Right now, you'd have to delete it and re-add it. Add a PUT route and inline edit form so users can update bookmarks in place.
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 (M0), so there are no ongoing costs even if you leave your cluster running.
Resources you used:
- MongoDB Atlas M0 cluster (cloud database storing your bookmarks).
- Express server running on port 5000.
- Vite dev server running on port 5173.
- Local mern-bookmarks project folder on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively building or want to extend the project further.
- Your MongoDB Atlas free cluster has no ongoing costs. It will auto-pause after 30 days of inactivity (zero connections).
- Your local project files remain on your Desktop in the mern-bookmarks folder.
- Node.js remains installed on your machine for future projects.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and cloud database stay intact for when you return.
- In each terminal window where a server is running (Express and Vite), press Ctrl+C to stop the process.
- Your MongoDB Atlas free cluster will auto-pause after 30 days of inactivity. No action needed on your part.
- Your local files stay in the mern-bookmarks folder, ready for you to resume.
Resuming later
When you come back, open two terminal windows. In the first, navigate to the server folder and run node index.js. In the second, navigate to the client folder and run npm run dev.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your bookmark data, local files, and cloud cluster.
- In each terminal window where a server is running, press Ctrl+C to stop the process.
Delete your MongoDB Atlas cluster by following these steps:
- Go to cloud.mongodb.com and sign in.
- Navigate to your project's Database Deployments page.
- Click the three-dot menu on your cluster, then select the option to terminate the cluster.
- Confirm the deletion when prompted.
Delete your local project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/mern-bookmarks
On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\mern-bookmarks
Nice Work!
Nice Work!
You did it. You built a full-stack MERN application from scratch, connecting a React UI to a cloud database through your own REST API.
Here's what you accomplished:
- Built a REST API with Express and Node.js featuring GET, POST, and DELETE endpoints.
- Connected to a cloud MongoDB Atlas database using Mongoose for schema validation and data modeling.
- Created a React frontend with Vite that consumes your API and achieves full-stack data flow: UI to API to Database and back again.
- Added edit functionality with PUT requests and inline editing for full CRUD operations.
Ready to quiz yourself?