Build a Bookmark Manager with MERN
Build a full-stack bookmark manager with Express, MongoDB, and React.
Introduction
30 Second Summary
Think about how many browser tabs you have open right now. Every interesting link you find either gets buried in a bookmarks folder or lost forever when you close the tab.
In this project, you will build a full-stack Bookmark Manager application using the MERN stack. Your app will have a REST API connected to a cloud database and a React frontend for saving, editing, and deleting bookmarks.
What You'll Build
A full-stack web application with an Express.js backend API connected to MongoDB Atlas, and a React frontend built with Vite.
By the end of this project, you'll have:
- A Node.js/Express REST API with five CRUD endpoints for managing bookmarks.
- A Mongoose model that defines and validates your bookmark data in MongoDB.
- A React frontend with a form to add and edit bookmarks, and a list to display and delete them.
- Secret Mission: Add category filtering so users can view bookmarks by category.
Are there any prerequisites?
This project assumes basic JavaScript knowledge and familiarity with React concepts like components, state, props, and useEffect.
You'll also need Node.js, npm, VS Code, and a free MongoDB Atlas account set up before starting.
Before We Start
Before We Start...
Set Up the Project Structure
Every full-stack project needs a clear folder structure to keep server code and client code organized. The MERN stack uses a pattern called a monorepo, where both applications live in one project directory.
In this step, you'll create the project folder, install the backend dependencies for your Express.js API, and scaffold a React frontend with Vite.
In this step, get ready to:
- Create the project folder with a server/ subdirectory and initialize the root package.json.
- Install the backend packages: express, mongoose, cors, dotenv, and nodemon.
- Scaffold the React client with Vite and install axios.
Create the project folder structure
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and the server subdirectory by running these commands:
mkdir bookmark-manager
cd bookmark-manager
mkdir server
- Confirm the folder was created by running:
ls
You should see server in the output.
- Initialize a root package.json for the monorepo by running:
npm init -y
You should see the contents of your new package.json printed in the terminal.
What is this folder structure?
The bookmark-manager/ folder is the root of your monorepo. It holds a root package.json that ties the project together.
The server/ directory will hold your Express API code. You'll create the client/ directory in a later substep when you scaffold the React app with Vite.
Initialize the server and install backend dependencies
The Express server needs a few packages to handle HTTP requests, connect to MongoDB, and manage environment variables.
- Move into the server directory and initialize it as a Node.js project by running these commands:
cd server
npm init -y
You should see a new package.json printed in the terminal for the server project.
- Install the four backend packages by running:
npm install express mongoose cors dotenv
What are these packages?
- express is the web framework for building your REST API and handling HTTP requests.
- mongoose connects your server to MongoDB and lets you define data schemas with built-in validation.
- cors handles CORS, allowing your React frontend to make requests to the Express server from a different port.
- dotenv loads environment variables from a .env file so you can keep secrets out of your code.
You should see added followed by a package count in the terminal output.
- Install nodemon as a development dependency by running:
npm install --save-dev nodemon
What is nodemon?
Nodemon watches your server files for changes and automatically restarts the server when you save. It is installed as a dev dependency because it is only needed during development, not in production.
Seeing an npm error?
- Check that you are inside the server directory. Run pwd to confirm your current location ends with bookmark-manager/server.
- Make sure you have an active internet connection. npm needs to download packages from the registry.
- Help me fix my npm install error.
Scaffold the React client with Vite
Vite is a modern build tool that scaffolds React projects with fast hot-reloading and minimal configuration. One command creates the entire client/ directory for you.
- Navigate back to the bookmark-manager root folder by running:
cd ..
- Scaffold a new React project in a client folder by running the command below.
- npm may ask to install the create-vite package. Type y and press Enter to confirm.
npm create vite@latest client -- --template react
What does this command do?
This command uses create-vite to generate a React project inside a new client/ directory. The --template react flag tells Vite to use the React starter template, so you skip any interactive prompts.
You should see output confirming the project was scaffolded in client/.
- Move into the client directory and install the base dependencies by running:
cd client
npm install
You should see added followed by a package count in the output.
- Install Axios for making API calls from React by running:
npm install axios
What is Axios?
Axios is an HTTP client library. Your React frontend will use it to send requests to your Express API. It simplifies common tasks like setting headers and parsing JSON responses.
- Navigate back to the project root and verify the full folder structure by running:
cd ..
ls
You should see three items: client, package.json, and server.
Vite command not working?
- Make sure you ran the scaffold command from the bookmark-manager root folder, not from inside server/ or client/.
- If you see a permission error, try running npx create-vite@latest client --template react instead.
- Help me troubleshoot my Vite scaffold command.
Your project structure is ready with both a server and client directory. Next up, you'll build the Express API that powers your bookmark manager.
Build the Express API Server
Your project structure is in place and all dependencies are installed. Now it's time to build the actual backend.
Every full-stack app needs an API that sits between the frontend and the database. In this step, you'll create an Express.js server that accepts requests, a Mongoose model that defines what a bookmark looks like, and a set of REST API routes that handle creating, reading, updating, and deleting bookmarks.
In this step, get ready to:
- Set up the Express server with middleware and environment variables.
- Define the Mongoose Bookmark model with a schema.
- Create five CRUD route endpoints for bookmarks.
Create the Express server and environment file
Before writing any server logic, you need a place to store sensitive configuration like your database connection string. Environment variables keep these values out of your code so they never get committed to version control.
- Create a new file called .env inside your server/ folder.
- Add the following environment variables:
MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5000
What do these variables do?
MONGODB_URI will hold your MongoDB Atlas connection string. You'll replace the placeholder with a real value in Step 3.
PORT tells Express which port to listen on. Port 5000 keeps it separate from the React dev server that runs on port 5173.
Now you'll create the main server file. The Express server needs middleware to parse incoming JSON data and handle cross-origin requests from the React frontend.
- Create a new file called index.js inside your server/ folder.
- Add the imports, app setup, and middleware configuration by copying the following code:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
What does this code do?
- require('dotenv').config() loads the variables from your .env file so you can access them with process.env.
- cors() allows the React frontend (on port 5173) to make requests to this server (on port 5000). Without CORS, browsers block cross-origin requests by default.
- express.json() parses incoming JSON request bodies so your route handlers can read req.body.
The server also needs a health check endpoint and the logic to connect to MongoDB when it starts.
- In server/index.js, add the following code below the middleware lines:
// Health check
app.get('/', (req, res) => {
res.json({ message: 'Bookmark Manager API is running' });
});
// Connect to MongoDB and start server
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?
- The health check route at / gives you a quick way to confirm the server is running by visiting it in a browser.
- mongoose.connect() tries to connect to your MongoDB Atlas database. The server only starts listening for requests after the database connection succeeds.
- The .catch() block logs the error if the connection fails, which helps you debug connection string issues.
- Save server/index.js and server/.env.
You won't start the server yet. You'll connect to MongoDB Atlas and test it in Step 3.
Seeing a red underline on the require statements?
If VS Code shows a warning under require, that is just the editor suggesting ES module syntax. The server uses CommonJS (require()) which is the default for Node.js. The code will run correctly.
help me fix a require warning in my server/index.js file
Define the Bookmark model
A Mongoose model defines the shape of your data. It tells MongoDB exactly what fields a bookmark should have, which ones are required, and what types they use.
- Create a new folder called models inside your server/ folder.
- Create a new file called Bookmark.js inside server/models/.
- Add the Bookmark schema and model by copying the following code:
const mongoose = require('mongoose');
const bookmarkSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Title is required'],
trim: true
},
url: {
type: String,
required: [true, 'URL is required'],
trim: true
},
category: {
type: String,
default: 'General',
trim: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Bookmark', bookmarkSchema);
What does this schema define?
- title and url are required strings. If a request tries to create a bookmark without them, Mongoose rejects it with a custom error message.
- category defaults to 'General' if the user doesn't provide one.
- trim: true removes extra whitespace from the start and end of each field before saving.
- timestamps: true automatically adds createdAt and updatedAt fields to every bookmark document.
- mongoose.model('Bookmark', bookmarkSchema) compiles the schema into a model. Mongoose automatically creates a bookmarks collection in your MongoDB database.
- Save server/models/Bookmark.js.
You won't see a visible change yet. The next substep uses this model to build the route handlers.
✔️ Awesome, I've got everything!
Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
const mongoose = require('mongoose');
const bookmarkSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Title is required'],
trim: true
},
url: {
type: String,
required: [true, 'URL is required'],
trim: true
},
category: {
type: String,
default: 'General',
trim: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Bookmark', bookmarkSchema);
Create the CRUD route endpoints
Routes define what your API does when it receives a request. Each route maps an HTTP method (GET, POST, PUT, DELETE) to a handler function that talks to the database through the Bookmark model.
- Create a new folder called routes inside your server/ folder.
- Create a new file called bookmarks.js inside server/routes/.
- Start by adding the imports and the two GET routes (read operations) by copying the following code:
const express = require('express');
const router = express.Router();
const Bookmark = require('../models/Bookmark');
// GET all bookmarks
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 });
}
});
// GET a single bookmark
router.get('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findById(req.params.id);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json(bookmark);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
What do the GET routes do?
- express.Router() creates a modular set of routes that you can mount on the main app later.
- GET / returns all bookmarks, sorted newest first using .sort({ createdAt: -1 }).
- GET /:id finds one bookmark by its unique _id. If it doesn't exist, the route returns a 404 error.
- Every route handler is wrapped in a try/catch block. If the database query fails, the error message is sent back as a JSON response instead of crashing the server.
- Add the POST, PUT, and DELETE routes below your GET routes in the same file, then export the router:
// POST a new bookmark
router.post('/', async (req, res) => {
const bookmark = new Bookmark({
title: req.body.title,
url: req.body.url,
category: req.body.category
});
try {
const newBookmark = await bookmark.save();
res.status(201).json(newBookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// PUT (update) a bookmark
router.put('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findByIdAndUpdate(
req.params.id,
{
title: req.body.title,
url: req.body.url,
category: req.body.category
},
{ new: true, runValidators: true }
);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json(bookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// DELETE a bookmark
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 });
}
});
module.exports = router;
What do the write routes do?
- POST / creates a new Bookmark from the request body and saves it with .save(). A successful create returns status 201 (Created).
- PUT /:id finds a bookmark by ID and replaces its fields. The option { new: true } tells Mongoose to return the updated document. runValidators: true ensures the update still passes schema validation.
- DELETE /:id removes the bookmark from the database entirely and returns a confirmation message.
- Save server/routes/bookmarks.js.
The routes file is complete. Now you need to tell Express to use it.
- Open server/index.js.
- Find this line near the top of the file:
require('dotenv').config();
- Add this line directly below it to import the routes:
const bookmarkRoutes = require('./routes/bookmarks');
- Now find the middleware section:
// Middleware
app.use(cors());
app.use(express.json());
- Add the routes mount directly below the middleware:
// Routes
app.use('/api/bookmarks', bookmarkRoutes);
What does mounting the router do?
app.use('/api/bookmarks', bookmarkRoutes) tells Express to forward any request starting with /api/bookmarks to your router. Since the router defines its own paths relative to this base, a GET request to /api/bookmarks hits router.get('/') and a DELETE to /api/bookmarks/123 hits router.delete('/:id').
- Save server/index.js.
You won't see changes yet. In the next step, you'll connect to MongoDB Atlas and test every endpoint.
Not sure if your code is right?
Check that bookmarkRoutes in your import matches the path './routes/bookmarks'. The path is relative to where index.js lives, not the project root.
Make sure the routes import line (const bookmarkRoutes = ...) comes after require('dotenv').config() and before const app = express(). Use the double-check tab below to compare your full file.
Help me check if my server/index.js and routes file are set up correctly
Double check your two server files match the complete code below.
✔️ Awesome, I've got everything!
Make sure you've saved both server/index.js and server/routes/bookmarks.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const bookmarkRoutes = require('./routes/bookmarks');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/bookmarks', bookmarkRoutes);
// Health check
app.get('/', (req, res) => {
res.json({ message: 'Bookmark Manager API is running' });
});
// Connect to MongoDB and start server
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);
});
const express = require('express');
const router = express.Router();
const Bookmark = require('../models/Bookmark');
// GET all bookmarks
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 });
}
});
// GET a single bookmark
router.get('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findById(req.params.id);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json(bookmark);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// POST a new bookmark
router.post('/', async (req, res) => {
const bookmark = new Bookmark({
title: req.body.title,
url: req.body.url,
category: req.body.category
});
try {
const newBookmark = await bookmark.save();
res.status(201).json(newBookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// PUT (update) a bookmark
router.put('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findByIdAndUpdate(
req.params.id,
{
title: req.body.title,
url: req.body.url,
category: req.body.category
},
{ new: true, runValidators: true }
);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json(bookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// DELETE a bookmark
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 });
}
});
module.exports = router;
Your entire Express API is built. Next up, you'll connect it to a real MongoDB Atlas database and test every endpoint to make sure it works.
Connect to MongoDB Atlas and Test the API
You now have a complete Express API with routes, a Mongoose model, and middleware. The missing piece is a real database.
In this step, you will connect your server to a MongoDB Atlas cloud database and test every endpoint with curl commands. By the end, you will have proof that your API can create, read, update, and delete bookmarks.
In this step, get ready to:
- Add your MongoDB Atlas connection string to the .env file.
- Start the server and test all five CRUD endpoints using curl.
Add your MongoDB Atlas connection string
Your server code already references process.env.MONGODB_URI to connect to the database. Right now, the .env file has a placeholder value. You need to replace it with a real connection string from your Atlas cluster.
- Open your browser and go to cloud.mongodb.com.
- Sign in to your MongoDB Atlas account.
- In the left sidebar, click Clusters under the Database heading.
Don't have a cluster yet?
If you do not see a cluster on the Clusters page, click Create to deploy a new one. Select the Free tier (M0). Choose any cloud provider and region, then click Create. The cluster takes 1-3 minutes to provision.
Before you can connect, Atlas needs two things: a database user and your IP address on the access list.
- In the left sidebar, click Database Access under the Security heading.
- If you do not already have a database user, click Add New Database User.
- Choose Password as the authentication method.
- Enter a username and a password you will remember. Write the password down because you will need it in a moment.
- Under Built-in Role, select Read and write to any database.
- Click Add User.
- In the left sidebar, click Network Access under the Security heading.
- Click Add IP Address.
- Click Allow Access from Anywhere to add 0.0.0.0/0. This is fine for a local development project.
- Click Confirm.
Now grab the connection string.
- Go back to the Clusters page.
- Click Connect on your cluster.
- Select Drivers as the connection method.
- Copy the connection string that appears. It looks something like mongodb+srv://youruser:<db_password>@cluster0.abc123.mongodb.net/.
You need to replace the <db_password> placeholder in the connection string with the actual password you set for your database user.
- In VS Code, open server/.env.
- Find this line:
MONGODB_URI=your-mongodb-atlas-connection-string-here
- Replace your-mongodb-atlas-connection-string-here with the connection string you just copied (with your real password substituted in). The line should now look like this:
MONGODB_URI=mongodb+srv://youruser:yourpassword@cluster0.abc123.mongodb.net/?retryWrites=true&w=majority
What does this connection string do?
The mongodb+srv:// prefix tells Mongoose to use DNS to discover all the hosts in your Atlas cluster automatically.
Your username and password authenticate the connection. The cluster address (cluster0.abc123.mongodb.net) points to your specific Atlas cluster in the cloud. When the server starts, the mongoose.connect() call in index.js reads this value and opens the connection.
- Save server/.env.
Keep your connection string private
Your .env file contains your database credentials. Never commit this file to Git or share it publicly. If you push this project to GitHub later, add .env to your .gitignore file.
Start the server and test the API endpoints
With a real connection string in place, your server can now connect to MongoDB Atlas when it starts. You will launch the server and then send requests to each of your five endpoints to confirm they work.
- Start your Express server by running this command from the bookmark-manager folder:
npx nodemon server/index.js
What does nodemon do?
Nodemon watches your files for changes and automatically restarts the server whenever you save. This saves you from manually stopping and restarting the server every time you edit code.
You should see two lines in your terminal:
Connected to MongoDB Atlas
Server running on port 5000
Seeing a MongoDB connection error?
- Double-check that you replaced <db_password> in the connection string with your actual database user password.
- Make sure your IP address is on the Atlas access list. Go to Network Access in Atlas and verify 0.0.0.0/0 is listed.
- Check that the MONGODB_URI line in .env has no spaces around the = sign and no quotes around the value.
- If your password contains special characters like @ or #, you need to percent-encode them (e.g. @ becomes %40).
- Still stuck? Help me debug my MongoDB Atlas connection error.
Your server is running. Now open a second terminal window to send test requests. Keep the server running in the first terminal.
Open a second terminal
In VS Code, click the + icon in the top right of the terminal panel to open a new terminal tab. Your server stays running in the first tab while you type curl commands in the second.
- Create a new bookmark by running this curl POST request:
curl -X POST http://localhost:5000/api/bookmarks -H "Content-Type: application/json" -d '{"title": "NextWork", "url": "https://www.nextwork.org", "category": "Learning"}'
You should see a JSON response containing the new bookmark with a generated _id, your title, URL, category, and timestamp fields.
What does the response look like?
The response is a JSON object like this:
{
"_id": "664f1a2b3c4d5e6f7a8b9c0d",
"title": "NextWork",
"url": "https://www.nextwork.org",
"category": "Learning",
"createdAt": "2026-05-04T12:00:00.000Z",
"updatedAt": "2026-05-04T12:00:00.000Z",
"__v": 0
}
Your _id and timestamps will be different. Copy the _id value. You will need it for the next few commands.
Getting connection refused?
- Make sure your server is still running in the first terminal tab. You should see Server running on port 5000 in the output.
- Check that you are running the curl command in a separate terminal tab, not the one running the server.
- Still stuck? Help me debug my curl POST request to localhost:5000.
Windows users: use Git Bash
The curl commands in this step use single quotes around the JSON body. If you are on Windows and see a parsing error, run these commands in Git Bash or PowerShell instead of Command Prompt.
- Fetch all bookmarks by running this GET request:
curl http://localhost:5000/api/bookmarks
You should see a JSON array containing the bookmark you just created.
- Fetch a single bookmark by its ID by running this command:
curl http://localhost:5000/api/bookmarks/[[BOOKMARK_ID="the _id from your POST response"]]
You should see the same bookmark object returned on its own, not inside an array.
- Update the bookmark's title by running this PUT request:
curl -X PUT http://localhost:5000/api/bookmarks/[[BOOKMARK_ID="the _id from your POST response"]] -H "Content-Type: application/json" -d '{"title": "NextWork Projects", "url": "https://www.nextwork.org", "category": "Learning"}'
The response shows the updated bookmark. The title field should now read NextWork Projects instead of NextWork.
- Delete the bookmark by running this DELETE request:
curl -X DELETE http://localhost:5000/api/bookmarks/[[BOOKMARK_ID="the _id from your POST response"]]
You should see {"message":"Bookmark deleted"} in the response.
- Confirm the deletion by fetching all bookmarks one more time:
curl http://localhost:5000/api/bookmarks
You should see an empty array [] since the only bookmark was deleted.
What just happened?
You tested the full CRUD lifecycle. Your curl commands sent HTTP requests to your Express routes. Each route used Mongoose to talk to MongoDB Atlas. The data travelled from your terminal to Express to Mongoose to MongoDB and back.
This is the same data flow your React frontend will use in the next steps. The only difference is that React will send the requests using axios instead of curl.
✔️ Awesome, I've got everything!
Great. Double check you have saved server/.env with your real connection string.
ⓧ I'd like to double check the full code
Here is the full code for each server file. Your .env will have your own connection string instead of the placeholder.
MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5000
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const bookmarkRoutes = require('./routes/bookmarks');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/bookmarks', bookmarkRoutes);
// Health check
app.get('/', (req, res) => {
res.json({ message: 'Bookmark Manager API is running' });
});
// Connect to MongoDB and start server
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);
});
const mongoose = require('mongoose');
const bookmarkSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Title is required'],
trim: true
},
url: {
type: String,
required: [true, 'URL is required'],
trim: true
},
category: {
type: String,
default: 'General',
trim: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Bookmark', bookmarkSchema);
const express = require('express');
const router = express.Router();
const Bookmark = require('../models/Bookmark');
// GET all bookmarks
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 });
}
});
// GET a single bookmark
router.get('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findById(req.params.id);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json(bookmark);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// POST a new bookmark
router.post('/', async (req, res) => {
const bookmark = new Bookmark({
title: req.body.title,
url: req.body.url,
category: req.body.category
});
try {
const newBookmark = await bookmark.save();
res.status(201).json(newBookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// PUT (update) a bookmark
router.put('/:id', async (req, res) => {
try {
const bookmark = await Bookmark.findByIdAndUpdate(
req.params.id,
{
title: req.body.title,
url: req.body.url,
category: req.body.category
},
{ new: true, runValidators: true }
);
if (!bookmark) {
return res.status(404).json({ message: 'Bookmark not found' });
}
res.json(bookmark);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// DELETE a bookmark
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 });
}
});
module.exports = router;
Your API is fully connected and tested. Next up, you will build the React frontend that talks to this API so you can manage bookmarks from a browser instead of the terminal.
Build the React Frontend
Your API is live and every endpoint is working. You tested create, read, update, and delete with curl and saw the data flowing in and out of MongoDB.
Now it's time to build the frontend that actually talks to this API. You'll create React components for listing bookmarks, a form for adding and editing them, and a small API utility that connects everything to your Express server.
In this step, get ready to:
- Set up the API utility and the main App component layout.
- Create the BookmarkList component to display saved bookmarks.
- Create the BookmarkForm component to add and edit bookmarks.
Set up the API utility and app layout
Before writing any components, you need a small utility that knows how to talk to your Express API. You also need to replace the Vite boilerplate in App.jsx with the real bookmark manager layout.
- In VS Code, right-click on the client/src folder in the Explorer sidebar.
- Click New Folder and name it services.
- Right-click on the new services folder and click New File.
- Name the file api.js and paste the following code into it:
import axios from 'axios';
// Create a reusable axios instance with the base API path
const api = axios.create({
baseURL: '/api'
});
export default api;
What does this code do?
This creates a pre-configured Axios instance with baseURL: '/api'. Every request you make with api.get('/bookmarks') will automatically prepend /api to the path.
Using a relative path like /api instead of http://localhost:5000/api means the frontend relies on a proxy to forward requests to Express. You'll configure that proxy in the next step.
- Save api.js.
You won't see a visible change yet. This utility file gets imported by the components you'll create next.
✔️ Awesome, I've got everything!
Great. Make sure api.js is saved and move on.
ⓧ I'd like to double check the full code
import axios from 'axios';
const api = axios.create({
baseURL: '/api'
});
export default api;
Now replace the Vite boilerplate in App.jsx with the real bookmark manager layout. This component will be the central hub that manages all the bookmark data and coordinates the form and list components.
- Open client/src/App.jsx in VS Code.
- Select all the existing boilerplate code (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
- Paste in the following code:
import { useState, useEffect } from 'react';
import api from './services/api';
import BookmarkForm from './components/BookmarkForm';
import BookmarkList from './components/BookmarkList';
import './App.css';
function App() {
// State for all bookmarks and the bookmark currently being edited
const [bookmarks, setBookmarks] = useState([]);
const [currentBookmark, setCurrentBookmark] = useState(null);
// Fetch all bookmarks from the API
const fetchBookmarks = async () => {
try {
const response = await api.get('/bookmarks');
setBookmarks(response.data);
} catch (error) {
console.error('Error fetching bookmarks:', error);
}
};
// Fetch bookmarks when the component first loads
useEffect(() => {
fetchBookmarks();
}, []);
// Re-fetch bookmarks after creating or updating one
const handleBookmarkSaved = () => {
fetchBookmarks();
setCurrentBookmark(null);
};
// Set the bookmark to edit when the user clicks Edit
const handleEdit = (bookmark) => {
setCurrentBookmark(bookmark);
};
// Delete a bookmark by ID, then refresh the list
const handleDelete = async (id) => {
try {
await api.delete(`/bookmarks/${id}`);
fetchBookmarks();
} catch (error) {
console.error('Error deleting bookmark:', error);
}
};
// Clear the edit state when the user clicks Cancel
const handleCancelEdit = () => {
setCurrentBookmark(null);
};
return (
<div className="app">
<h1>Bookmark Manager</h1>
<BookmarkForm
currentBookmark={currentBookmark}
onBookmarkSaved={handleBookmarkSaved}
onCancelEdit={handleCancelEdit}
/>
<BookmarkList
bookmarks={bookmarks}
onEdit={handleEdit}
onDelete={handleDelete}
/>
</div>
);
}
export default App;
What does this code do?
App is the central component that owns all the bookmark data. It holds two pieces of state:
- bookmarks stores the full array of bookmarks fetched from the API.
- currentBookmark tracks which bookmark is being edited. When it's null, the form is in "add" mode.
The handler functions (handleEdit, handleDelete, handleBookmarkSaved) get passed down as props so the child components can trigger state changes without managing data themselves.
- Save App.jsx.
VS Code will show red underlines on the BookmarkForm and BookmarkList imports. Those components don't exist yet. You'll create them in the next two substeps.
Seeing other errors in App.jsx?
- Make sure you deleted all the original Vite boilerplate before pasting. Leftover code can cause syntax errors.
- Check that client/src/services/api.js exists. The import ./services/api depends on it.
Help me fix errors in my App.jsx file
✔️ Awesome, I've got everything!
Make sure App.jsx is saved. The import errors will disappear once you create the two components.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react';
import api from './services/api';
import BookmarkForm from './components/BookmarkForm';
import BookmarkList from './components/BookmarkList';
import './App.css';
function App() {
const [bookmarks, setBookmarks] = useState([]);
const [currentBookmark, setCurrentBookmark] = useState(null);
const fetchBookmarks = async () => {
try {
const response = await api.get('/bookmarks');
setBookmarks(response.data);
} catch (error) {
console.error('Error fetching bookmarks:', error);
}
};
useEffect(() => {
fetchBookmarks();
}, []);
const handleBookmarkSaved = () => {
fetchBookmarks();
setCurrentBookmark(null);
};
const handleEdit = (bookmark) => {
setCurrentBookmark(bookmark);
};
const handleDelete = async (id) => {
try {
await api.delete(`/bookmarks/${id}`);
fetchBookmarks();
} catch (error) {
console.error('Error deleting bookmark:', error);
}
};
const handleCancelEdit = () => {
setCurrentBookmark(null);
};
return (
<div className="app">
<h1>Bookmark Manager</h1>
<BookmarkForm
currentBookmark={currentBookmark}
onBookmarkSaved={handleBookmarkSaved}
onCancelEdit={handleCancelEdit}
/>
<BookmarkList
bookmarks={bookmarks}
onEdit={handleEdit}
onDelete={handleDelete}
/>
</div>
);
}
export default App;
Next, replace the contents of App.css with styles for the bookmark manager. These styles will apply to the form, bookmark cards, and overall layout.
- Open client/src/App.css in VS Code.
- Select all the existing code (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
- Paste in the following styles:
/* Main app container */
.app {
max-width: 700px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
/* Bookmark form styles */
.bookmark-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 2rem;
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.bookmark-form input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.form-buttons {
display: flex;
gap: 0.5rem;
}
.form-buttons button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.form-buttons button[type="submit"] {
background-color: #4a90d9;
color: white;
}
.form-buttons button[type="button"] {
background-color: #ccc;
}
/* Bookmark list styles */
.bookmark-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.bookmark-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.bookmark-info h3 {
margin: 0 0 0.25rem 0;
}
.bookmark-info a {
color: #4a90d9;
word-break: break-all;
font-size: 0.9rem;
}
/* Category badge on each bookmark card */
.category-badge {
display: inline-block;
margin-top: 0.5rem;
padding: 0.15rem 0.5rem;
background-color: #e8f0fe;
color: #1a73e8;
border-radius: 12px;
font-size: 0.8rem;
}
/* Edit and Delete buttons */
.bookmark-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.bookmark-actions button {
padding: 0.35rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.85rem;
}
.bookmark-actions button:hover {
background-color: #f5f5f5;
}
/* Empty state message */
.no-bookmarks {
text-align: center;
color: #888;
font-style: italic;
}
What do these styles do?
- .app centers the layout at a max width of 700px so content doesn't stretch across the full screen.
- .bookmark-form uses flexbox column layout to stack the input fields vertically inside a bordered card.
- .bookmark-card lays out each bookmark as a row with the info on the left and action buttons on the right.
- .category-badge renders the category as a small blue pill-shaped label.
- Save App.css.
You won't see any visual changes yet. The styles will take effect once the React components are created and the dev server is running.
✔️ Awesome, I've got everything!
Make sure App.css is saved. On to the components.
ⓧ I'd like to double check the full code
.app {
max-width: 700px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.bookmark-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 2rem;
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.bookmark-form input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.form-buttons {
display: flex;
gap: 0.5rem;
}
.form-buttons button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.form-buttons button[type="submit"] {
background-color: #4a90d9;
color: white;
}
.form-buttons button[type="button"] {
background-color: #ccc;
}
.bookmark-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.bookmark-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.bookmark-info h3 {
margin: 0 0 0.25rem 0;
}
.bookmark-info a {
color: #4a90d9;
word-break: break-all;
font-size: 0.9rem;
}
.category-badge {
display: inline-block;
margin-top: 0.5rem;
padding: 0.15rem 0.5rem;
background-color: #e8f0fe;
color: #1a73e8;
border-radius: 12px;
font-size: 0.8rem;
}
.bookmark-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.bookmark-actions button {
padding: 0.35rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.85rem;
}
.bookmark-actions button:hover {
background-color: #f5f5f5;
}
.no-bookmarks {
text-align: center;
color: #888;
font-style: italic;
}
Create the BookmarkList component
The BookmarkList component is a presentational component. It receives the bookmarks array as a prop from App and renders each one as a card with Edit and Delete buttons.
- Right-click on the client/src folder in the Explorer sidebar and click New Folder.
- Name the folder components.
- Right-click on the new components folder and click New File.
- Name it BookmarkList.jsx and paste in the following code:
// Display the list of bookmarks as cards
function BookmarkList({ bookmarks, onEdit, onDelete }) {
// Show a message when there are no bookmarks yet
if (bookmarks.length === 0) {
return <p className="no-bookmarks">No bookmarks yet. Add one above!</p>;
}
return (
<div className="bookmark-list">
{bookmarks.map((bookmark) => (
<div key={bookmark._id} className="bookmark-card">
<div className="bookmark-info">
<h3>{bookmark.title}</h3>
{/* Open the bookmarked URL in a new tab */}
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.url}
</a>
<span className="category-badge">{bookmark.category}</span>
</div>
<div className="bookmark-actions">
<button onClick={() => onEdit(bookmark)}>Edit</button>
<button onClick={() => onDelete(bookmark._id)}>Delete</button>
</div>
</div>
))}
</div>
);
}
export default BookmarkList;
What does this component do?
- It receives three props: bookmarks (the array of bookmark objects), onEdit (called when Edit is clicked), and onDelete (called when Delete is clicked).
- If the array is empty, it renders a friendly "No bookmarks yet" message instead of an empty page.
- Each bookmark card shows the title, a clickable URL that opens in a new tab, and a category badge.
- The key={bookmark._id} attribute uses the MongoDB document ID so React can efficiently track each card in the list.
- Save BookmarkList.jsx.
You won't see a change yet. The BookmarkForm import in App.jsx still needs to be resolved. You'll create that component next.
✔️ Awesome, I've got everything!
Make sure BookmarkList.jsx is saved inside the client/src/components/ folder.
ⓧ I'd like to double check the full code
function BookmarkList({ bookmarks, onEdit, onDelete }) {
if (bookmarks.length === 0) {
return <p className="no-bookmarks">No bookmarks yet. Add one above!</p>;
}
return (
<div className="bookmark-list">
{bookmarks.map((bookmark) => (
<div key={bookmark._id} className="bookmark-card">
<div className="bookmark-info">
<h3>{bookmark.title}</h3>
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.url}
</a>
<span className="category-badge">{bookmark.category}</span>
</div>
<div className="bookmark-actions">
<button onClick={() => onEdit(bookmark)}>Edit</button>
<button onClick={() => onDelete(bookmark._id)}>Delete</button>
</div>
</div>
))}
</div>
);
}
export default BookmarkList;
Create the BookmarkForm component
The BookmarkForm component handles both creating new bookmarks and editing existing ones. It uses controlled inputs where React state drives each field's value.
- Right-click on the components folder and click New File.
- Name it BookmarkForm.jsx and paste in the following code:
import { useState, useEffect } from 'react';
import api from '../services/api';
function BookmarkForm({ currentBookmark, onBookmarkSaved, onCancelEdit }) {
// Controlled form state for each input field
const [title, setTitle] = useState('');
const [url, setUrl] = useState('');
const [category, setCategory] = useState('General');
// When editing a bookmark, populate the form with its values
useEffect(() => {
if (currentBookmark) {
setTitle(currentBookmark.title);
setUrl(currentBookmark.url);
setCategory(currentBookmark.category);
} else {
setTitle('');
setUrl('');
setCategory('General');
}
}, [currentBookmark]);
// Handle form submission for both creating and updating
const handleSubmit = async (e) => {
e.preventDefault();
const bookmarkData = { title, url, category };
try {
if (currentBookmark) {
// Update the existing bookmark
await api.put(`/bookmarks/${currentBookmark._id}`, bookmarkData);
} else {
// Create a new bookmark
await api.post('/bookmarks', bookmarkData);
}
// Reset form fields after saving
setTitle('');
setUrl('');
setCategory('General');
onBookmarkSaved();
} catch (error) {
console.error('Error saving bookmark:', error);
}
};
return (
<form onSubmit={handleSubmit} className="bookmark-form">
<input
type="text"
placeholder="Bookmark title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="url"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
required
/>
<input
type="text"
placeholder="Category"
value={category}
onChange={(e) => setCategory(e.target.value)}
/>
<div className="form-buttons">
{/* Show "Update" when editing, "Add" when creating */}
<button type="submit">
{currentBookmark ? 'Update' : 'Add'} Bookmark
</button>
{currentBookmark && (
<button type="button" onClick={onCancelEdit}>
Cancel
</button>
)}
</div>
</form>
);
}
export default BookmarkForm;
What does this component do?
- The useEffect watches currentBookmark. When App passes a bookmark to edit, the form auto-fills with that bookmark's title, URL, and category.
- handleSubmit checks whether currentBookmark exists. If it does, the form sends a PUT request to update. If not, it sends a POST request to create.
- After saving, the form clears itself and calls onBookmarkSaved() so App can re-fetch the updated list from the API.
- The Cancel button only appears when editing. It calls onCancelEdit() to clear currentBookmark in the parent and return the form to "add" mode.
- Save BookmarkForm.jsx.
All three components now exist. The import errors in App.jsx should be resolved.
- Confirm your file structure by running this command in a new terminal:
ls client/src/services client/src/components
You should see api.js listed under services, and BookmarkForm.jsx and BookmarkList.jsx listed under components.
Missing a file?
- Make sure both folders are inside client/src/, not inside client/ or server/.
- Double-check file names are exact: BookmarkList.jsx and BookmarkForm.jsx with capital B and L/F. React import paths are case-sensitive.
Help me fix my React project file structure
✔️ Awesome, I've got everything!
Make sure all files are saved. You're ready to connect the frontend and backend.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react';
import api from '../services/api';
function BookmarkForm({ currentBookmark, onBookmarkSaved, onCancelEdit }) {
const [title, setTitle] = useState('');
const [url, setUrl] = useState('');
const [category, setCategory] = useState('General');
useEffect(() => {
if (currentBookmark) {
setTitle(currentBookmark.title);
setUrl(currentBookmark.url);
setCategory(currentBookmark.category);
} else {
setTitle('');
setUrl('');
setCategory('General');
}
}, [currentBookmark]);
const handleSubmit = async (e) => {
e.preventDefault();
const bookmarkData = { title, url, category };
try {
if (currentBookmark) {
await api.put(`/bookmarks/${currentBookmark._id}`, bookmarkData);
} else {
await api.post('/bookmarks', bookmarkData);
}
setTitle('');
setUrl('');
setCategory('General');
onBookmarkSaved();
} catch (error) {
console.error('Error saving bookmark:', error);
}
};
return (
<form onSubmit={handleSubmit} className="bookmark-form">
<input
type="text"
placeholder="Bookmark title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="url"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
required
/>
<input
type="text"
placeholder="Category"
value={category}
onChange={(e) => setCategory(e.target.value)}
/>
<div className="form-buttons">
<button type="submit">
{currentBookmark ? 'Update' : 'Add'} Bookmark
</button>
{currentBookmark && (
<button type="button" onClick={onCancelEdit}>
Cancel
</button>
)}
</div>
</form>
);
}
export default BookmarkForm;
Your React components are built and ready. Next up, you'll wire the frontend and backend together with a Vite proxy and test the full create, edit, and delete flow in the browser.
Connect Frontend and Backend Together
Your React components are built and ready to talk to the backend. Right now, though, they have no way to reach the Express server. The frontend runs on one port and the backend runs on another.
In this step, you will configure a Vite proxy so the React dev server forwards API requests to Express. Then you will start both servers and test the full create, read, edit, and delete flow in your browser.
In this step, get ready to:
- Understand how data flows between your React components and the Express API.
- Configure the Vite proxy to forward API requests to Express.
- Start both servers and test the full CRUD flow end-to-end.
Understand the shared state pattern
Before starting both servers, take a moment to understand how the three components you built in the previous step work together.
How does data flow in this app?
App.jsx is the single source of truth. It holds the bookmarks array and the currentBookmark being edited. It passes data down to children and receives events back up through callback props.
- When BookmarkForm saves a bookmark, it calls onBookmarkSaved, which triggers fetchBookmarks() in App.jsx to refresh the list.
- When BookmarkList's Edit button is clicked, it calls onEdit, which sets currentBookmark in App. The form then pre-fills with that bookmark's data.
- When Delete is clicked, App calls the API to remove the bookmark and re-fetches the list.
This "lift state up" pattern keeps your components in sync without any of them needing to know about each other directly.
The full data journey
Every operation follows the same path: React component calls Axios, which sends an HTTP request to Express. Express routes the request to the right handler, which uses Mongoose to read or write a document in MongoDB Atlas. The response travels back along the same path until React updates the screen.
Configure the Vite proxy and start both servers
Your React dev server will run on port 5173 and your Express server runs on port 5000. These are two different origins, which means the browser would block API requests due to CORS rules.
Your Express server already has the cors() middleware to allow cross-origin requests. A Vite proxy adds an even cleaner solution: it forwards any request starting with /api to Express behind the scenes. Your Axios calls use the simple relative path /api instead of a full URL.
- In VS Code, open client/vite.config.js from the Explorer sidebar.
- Replace the entire file contents with this proxy configuration:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
// Pin the dev server to port 5173
port: 5173,
proxy: {
// Forward any /api request to the Express backend
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
});
What does this code do?
- port: 5173 locks the Vite dev server to a consistent port so you always know where to find your app.
- The proxy object tells Vite to intercept any request that starts with /api and forward it to http://localhost:5000.
- changeOrigin: true updates the request's Host header so Express sees the request as coming from its own origin.
- Save client/vite.config.js.
Seeing a syntax error when you save?
- Make sure the proxy object is nested inside server, not at the top level of defineConfig.
- Check that every opening brace { has a matching closing brace }. The nested objects here make it easy to miss one.
Help me fix a syntax error in my vite.config.js proxy configuration
Why use a proxy instead of a full URL?
Your api.js file sets baseURL: '/api' as a relative path. In development, the Vite proxy forwards this to Express on port 5000. In production, you would serve the React build from Express directly, so /api would already point to the right place.
A relative path means zero code changes when you deploy.
✔️ Awesome, I've got everything!
Great. Double check you have saved client/vite.config.js before moving on.
ⓧ I'd like to double check the full code
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
});
Now it is time to start both servers and see everything working together.
- Make sure your Express server is still running in the terminal where you started it earlier with npx nodemon server/index.js.
Express server stopped?
If you closed the terminal or the server stopped, open a new terminal, navigate to your project's root folder, and run npx nodemon server/index.js again. You should see Connected to MongoDB Atlas and Server running on port 5000 in the output.
- Open a second terminal. You need one terminal for Express and another for Vite.
- Navigate into the client folder and start the Vite dev server by running these commands:
cd client
npm run dev
You should see Vite print a local URL: http://localhost:5173.
Port 5173 already in use?
If another process is using port 5173, Vite will pick a different port like 5174. Close any other Vite dev servers you have running, then try again.
Help me fix a port conflict when starting the Vite dev server
- Open your browser and go to http://localhost:5173.
You should see the Bookmark Manager heading, a form with three input fields, and a message saying "No bookmarks yet." underneath.
Time to test the full CRUD flow. You will add a bookmark, edit it, and then delete it.
- In the first input field, type NextWork.
- In the second input field, type https://www.nextwork.org.
- In the third input field, type Learning.
- Click the Add Bookmark button.
A bookmark card should immediately appear below the form showing the title, URL as a clickable link, and a "Learning" category badge.
- Click the Edit button on your new bookmark card.
The form should pre-fill with the bookmark's current title, URL, and category. The submit button text should change from "Add Bookmark" to "Update Bookmark", and a Cancel button should appear.
- Change the category from Learning to Work.
- Click Update Bookmark.
The category badge on the card should update to "Work". The form should clear back to empty fields.
- Click the Delete button on the bookmark card.
The card should disappear, and the "No bookmarks yet" message should return.
Bookmarks not appearing after you click Add?
- Open your browser's DevTools console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Look for a red network error like 404 Not Found or 500 Internal Server Error.
- Check that your Express server terminal still shows Connected to MongoDB Atlas. If the connection dropped, restart the server.
- Make sure the Vite proxy in vite.config.js targets http://localhost:5000 and that your Express server is running on that port.
- If you changed vite.config.js while the Vite server was running, stop it with Ctrl+C and run npm run dev again. Proxy changes require a restart.
Help me debug why my bookmarks are not showing up in the browser
Your full-stack MERN app is working end-to-end. Every bookmark you create, edit, or delete travels from React through Express to MongoDB Atlas and back. Next up, you will tackle the Secret Mission and add category filtering to make the app even more useful.
Secret mission
Add Category Filtering
Your bookmark manager works, but as your collection grows, finding the right link gets harder. What if you could filter bookmarks by category with a single click? In this mission, you'll add a filter bar that makes your app genuinely useful.
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 runs locally and uses the MongoDB Atlas free tier, so there are no ongoing costs.
Resources you used:
- Express server running on port 5000 (local).
- React dev server running on port 5173 (local).
- MongoDB Atlas free tier cluster with a bookmarks collection.
- Project folder on your Desktop containing server/ and client/ directories with installed node_modules.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project soon.
- Your Express and React dev servers will keep running until you close the terminal windows.
- Your MongoDB Atlas free tier cluster stays active at no cost.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files and database stay intact.
- Stop the Express server by pressing Ctrl+C in the terminal where it is running.
- Stop the React dev server by pressing Ctrl+C in the terminal where it is running.
- Your MongoDB Atlas free tier cluster will auto-pause after 30 days of inactivity. No manual action needed.
- Your project files remain on your Desktop. When you are ready to resume, run npx nodemon server/index.js from the project root and npm run dev from the client/ folder.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Stop both servers by pressing Ctrl+C in each terminal window.
- Delete the entire project folder from your Desktop.
- To delete your MongoDB Atlas data, log in to MongoDB Atlas, navigate to your cluster, select Browse Collections, and drop the bookmarks collection.
- If you want to remove the entire Atlas cluster, go to your cluster's settings and click Terminate.
That's a Wrap!
That's a Wrap!
Nice work! You just built a full-stack web application from scratch using the MERN stack. You went from an empty folder to a working Express.js API, a MongoDB Atlas database, and a React frontend that ties it all together.
You've learned how to:
- Built an Express.js REST API with five CRUD endpoints and connected it to a cloud MongoDB Atlas database using Mongoose.
- Created a React frontend with reusable components that performs full create, read, update, and delete operations through your own API using Axios.
- Configured CORS and the Vite proxy so your frontend and backend communicate seamlessly during development.
- Secret Mission: Added category filtering so users can view bookmarks by category.
Ready to quiz yourself?