Build a Bookmark Manager with MERN

Build a full-stack bookmark manager with MongoDB, Express, React, and Node.js.

Introduction

30 Second Summary

You save links all day long. Articles to read later, tools to try, documentation to revisit. But browser bookmarks turn into a graveyard within weeks because there is no way to search, tag, or organize them.

In this project, you will build a full-stack bookmark manager using MongoDB, Express, React, and Node.js. You will wire a React frontend to an Express API backed by a cloud database, and experience the real cross-origin error that every full-stack developer hits along the way.

What You'll Build

You will type a URL into a form, press a button, and watch the bookmark appear on screen as a styled card. Then you will search across all your saved links and see the results filter instantly as you type.

By the end of this project, you'll have:

  • A working add-and-display loop where you enter a bookmark with a title, URL, and tags, then see it appear instantly in a card grid without a page refresh.
  • A real-time search bar that filters your bookmarks by title or tag as you type, with the results updating after a short pause.
  • A cloud-persisted collection stored in MongoDB Atlas that survives server restarts, so you can close everything and come back to find your bookmarks still there.
  • Secret Mission: a multi-tag filtering system with color-coded tag chips that lets you click multiple tags and see only bookmarks matching all of them.

Are there any prerequisites?

You should be comfortable writing JavaScript functions and working with HTML/CSS. A basic understanding of HTTP requests (like what happens when your browser visits a URL) will help too.

You will need Node.js, VS Code, and npm already installed on your machine. A free MongoDB Atlas account is created during the project.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. You're going to wire together all four layers of the MERN stack into a working bookmark manager that saves, displays, and searches links in real time.

Set Up the Project

A full-stack MERN application runs as two separate processes. The backend server handles data and API requests. The frontend dev server renders the user interface.

Before you can wire them together, each server needs to run independently. In this step, you'll create an Express backend and a React frontend, then confirm both can serve responses at the same time.

In this step, get ready to:
  • Create an Express backend server with a health check endpoint.
  • Scaffold a React frontend using Vite.
  • Confirm both servers run simultaneously on different ports.
Create the server folder and install dependencies
  • 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 and create the project folders by running these commands:
cd ~/Desktop
mkdir bookmark-manager
cd bookmark-manager
mkdir server
cd server
  • Initialize the server's package.json by running:
npm init -y

You should see output confirming a package.json file was created in the server folder.

  • Install the backend dependencies by running:
npm install express mongoose cors dotenv

You should see output listing the installed packages with their version numbers.

What did you just install?

  • express is the web framework that handles HTTP requests and routes.
  • mongoose connects your server to a MongoDB database with a clean schema-based API.
  • cors manages cross-origin requests so your frontend can talk to your backend.
  • dotenv loads environment variables from a .env file so you can keep secrets out of your code.

Now you need to configure the project to use modern ES module imports and write the server code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the bookmark-manager folder on your Desktop, then click Open.
  • In the Explorer sidebar on the left, expand the server folder and click on package.json to open it.
  • Replace the entire contents of package.json with this:
{
  "name": "bookmark-manager-server",
  "version": "1.0.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "cors": "^2.8.6",
    "dotenv": "^17.4.2",
    "express": "^5.2.1",
    "mongoose": "^9.9.0"
  }
}

Why replace the whole file?

The critical change is "type": "module". This tells Node.js to use modern import/export syntax instead of the older require() pattern.

You also get a clean start script and a descriptive package name. The dependency versions match what npm install just downloaded.

Without "type": "module", your server will crash with a syntax error when you try to use import statements.

  • Save package.json with Cmd+S (macOS) or Ctrl+S (Windows).
Write the Express server
  • Right-click the server folder in the Explorer sidebar.
  • Click New File and name it server.js.
  • Paste the following code into server.js:
import express from 'express';

// Create the Express app and set the port
const app = express();
const PORT = 5000;

// Parse incoming JSON request bodies
app.use(express.json());

// Health check endpoint to confirm the server is running
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Start the server and log the URL
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

What does this code do?

  • express() creates your application instance, which you attach routes and middleware to.
  • express.json() is middleware that automatically parses JSON from incoming request bodies.
  • app.get('/api/health', ...) defines a route that responds to GET requests at that path. You'll use it to confirm the server is alive.
  • app.listen(PORT, ...) starts the server on port 5000 and prints a confirmation message.
  • Save server.js with Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch back to your terminal. Make sure you are in the server folder, then start the server by running:
node server.js

You should see Server running on http://localhost:5000 printed in your terminal.

  • Open your browser and visit http://localhost:5000/api/health.

You should see the JSON response {"status":"ok"} displayed in the browser.

Not seeing the JSON response?

  • Check your terminal for error messages. If you see a syntax error mentioning import, make sure your package.json contains "type": "module" and you saved the file.
  • If the terminal says the port is already in use, another process is running on port 5000. Close it and try again.
  • Make sure you ran node server.js from inside the server folder, not the bookmark-manager root.

Still stuck? Help me debug why my Express server won't start.

✔️ Awesome, I've got everything!

Great. Double check you saved both package.json and server.js in the server folder.

ⓧ I'd like to double check the full code

Here is the complete server/server.js:

import express from 'express';

// Create the Express app and set the port
const app = express();
const PORT = 5000;

// Parse incoming JSON request bodies
app.use(express.json());

// Health check endpoint to confirm the server is running
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Start the server and log the URL
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

And here is the complete server/package.json:

{
  "name": "bookmark-manager-server",
  "version": "1.0.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "cors": "^2.8.6",
    "dotenv": "^17.4.2",
    "express": "^5.2.1",
    "mongoose": "^9.9.0"
  }
}
  • Back in your terminal, press Ctrl+C to stop the server. You'll restart it later.
Scaffold the React frontend with Vite

Your backend is working. Now you need a frontend to display the bookmarks in a browser. Vite is a modern build tool that scaffolds a React project with instant startup and fast hot-module reloading.

  • Navigate back to the project root by running:
cd ..

You should now be inside the bookmark-manager folder.

  • Scaffold the React frontend by running:
npm create vite@latest client -- --template react

What prompts will you see?

npm may ask you to confirm installing the create-vite package. Type y and press Enter to proceed.

After scaffolding, Vite may ask if you want to install dependencies and start the dev server now. Type N (or press Enter to decline). You'll install and start manually in the next commands.

  • Move into the new client folder and install its dependencies by running:
cd client
npm install

You should see output listing the installed React packages.

  • Start the Vite dev server by running:
npm run dev
  • Open your browser and visit http://localhost:5173.

You should see the Vite + React starter page with a spinning logo and a counter button.

Don't see the Vite starter page?

  • Check the terminal output for the exact URL. Vite sometimes uses a different port if 5173 is already taken.
  • If you see an error about missing dependencies, make sure you ran npm install inside the client folder before npm run dev.
  • If the command wasn't found, confirm you're inside bookmark-manager/client (not bookmark-manager/server).

Still stuck? Help me debug why my Vite dev server won't start.

Verify both servers run simultaneously

Your app needs both servers running at the same time. The backend serves data on port 5000. The frontend serves the UI on port 5173.

  • The Vite server is still running in your current terminal. Leave it running.
  • Open a second terminal window. On macOS, press Cmd+N in Terminal. On Windows, right-click the Terminal icon in your taskbar and select New Window.
  • In the new terminal, navigate to the server folder and start the backend by running:
cd ~/Desktop/bookmark-manager/server
node server.js

You should see Server running on http://localhost:5000 printed again.

Before you check the endpoints, what do you expect to see when you visit each URL?

  • Open http://localhost:5000/api/health in your browser.
  • Open http://localhost:5173 in another tab.

You should see the JSON health response on port 5000 and the React starter page on port 5173. Both running at the same time.

  • Confirm your Node.js version by running this in either terminal:
node --version

You should see v20.x or v22.x. This confirms your environment supports both Express and Vite.

One of the servers isn't responding?

  • Make sure each terminal is in the correct folder. Run pwd to check. The backend terminal should show bookmark-manager/server and the frontend terminal should show bookmark-manager/client.
  • If you accidentally closed a terminal, reopen it and restart the appropriate command (node server.js or npm run dev).

Still stuck? Help me get both my Express and Vite servers running at the same time.

Your environment is ready. You have a backend that can receive requests and a frontend that can render a UI. Next up, you'll build the REST API routes that will power your bookmark manager.

Build the REST API with Express

Your Express server and Vite frontend are both running. Now it's time to give the server something useful to do.

Before connecting a database, you'll build API endpoints with hardcoded data. This proves your routes work in isolation, so when you add persistence later, you'll know any bug comes from the database layer, not the API layer.

In this step, get ready to:
  • Define GET, POST, and DELETE routes that manage an in-memory array of bookmarks.
  • Test your API and discover that restarting the server erases all changes.
  • Add a search endpoint that filters bookmarks by title or tag.
Define the API routes

A REST API organizes your server around resources (bookmarks) and HTTP methods (GET to read, POST to create, DELETE to remove). You'll start with sample data hardcoded in an array so you can see the routes respond immediately.

  • In server.js, find the line that says app.use(express.json()); and add the following code below it:
// Sample bookmarks stored in memory
let bookmarks = [
  { id: '1', title: 'MDN Web Docs', url: 'https://developer.mozilla.org', tags: ['docs', 'web'] },
  { id: '2', title: 'Node.js', url: 'https://nodejs.org', tags: ['node', 'backend'] },
  { id: '3', title: 'React', url: 'https://react.dev', tags: ['react', 'frontend'] }
];

// Return all bookmarks as JSON
app.get('/api/bookmarks', (req, res) => {
  res.json(bookmarks);
});

What does this code do?

  • let bookmarks = [...] creates a mutable array of sample bookmark objects. Each has an id, title, url, and tags field.
  • app.get('/api/bookmarks', ...) tells Express to respond to GET requests at that path by sending the entire array as JSON.
  • Save server.js.
  • Stop your running server with Ctrl+C in the terminal, then restart it by running:
node server.js

Before you open the browser, what do you expect to see at /api/bookmarks?

  • Open your browser and navigate to http://localhost:5000/api/bookmarks.

You should see a JSON array containing the three sample bookmarks with their titles, URLs, and tags.

Seeing an empty page or an error?

  • Make sure you saved server.js before restarting the server.
  • Check that the bookmarks array is placed AFTER app.use(express.json()) and BEFORE the app.listen call at the bottom.
  • If you see Cannot GET /api/bookmarks, the route definition is missing or mistyped. Double-check the path string matches exactly.

Still stuck? Why isn't my /api/bookmarks route returning data?

  • Now add the POST and DELETE routes below the GET /api/bookmarks route you just created:
// Add a new bookmark
app.post('/api/bookmarks', (req, res) => {
  const bookmark = { id: Date.now().toString(), ...req.body };
  bookmarks.push(bookmark);
  res.status(201).json(bookmark);
});

// Delete a bookmark by ID
app.delete('/api/bookmarks/:id', (req, res) => {
  bookmarks = bookmarks.filter(b => b.id !== req.params.id);
  res.json({ message: 'Bookmark deleted' });
});

What does this code do?

  • The POST route generates a unique id using Date.now().toString(), merges it with the request body, pushes the result into the array, and responds with a 201 (created) status.
  • The DELETE route uses req.params.id to grab the ID from the URL path, then filters the array to remove the matching bookmark.
  • Save server.js and restart your server with Ctrl+C then node server.js.

Server won't start?

  • If you see a syntax error, check that each route has matching opening and closing braces and parentheses.
  • Make sure the POST and DELETE routes are placed after the GET route but before the app.listen line.

Help me fix my Express server syntax error.

Test the API and discover data loss

Your GET route works in the browser, but POST and DELETE need a different tool. You'll use curl in a second terminal window to send a POST request.

  • Open a new terminal window (keep the server running in the first one).
  • Add a new bookmark by running this command:
curl -X POST http://localhost:5000/api/bookmarks -H "Content-Type: application/json" -d '{"title": "GitHub", "url": "https://github.com", "tags": ["code", "git"]}'

Windows quoting

If you're on Windows and the command fails, open Git Bash (installed with Git) and run it there. Alternatively, replace the single quotes with escaped double quotes: -d "{\"title\": \"GitHub\"}".

You should see a JSON response with your new bookmark, including a generated id value.

  • Switch to your browser and refresh http://localhost:5000/api/bookmarks.

You should now see four bookmarks in the array. The GitHub entry you just added appears alongside the original three.

Now here's the important question. What do you think will happen to that fourth bookmark if you restart the server?

  • Go to the terminal running your server and press Ctrl+C to stop it.
  • Start it again by running:
node server.js
  • Refresh http://localhost:5000/api/bookmarks in your browser.

The GitHub bookmark is gone. You're back to three items. The array resets every time the server restarts because it only lives in memory.

Why did my data disappear?

Variables in a running Node.js process exist only in RAM. When the process stops, that RAM is freed and all data is lost. This is exactly why databases exist. In the next step, you'll connect MongoDB so your bookmarks survive restarts.

Add a search endpoint

A bookmark collection is only useful if you can find things quickly. You'll add a route that accepts a search term as a query parameter and filters bookmarks by title or tag.

  • In server.js, find the GET /api/bookmarks route you created earlier. Add the following code directly below it (above the POST route):
// Search bookmarks by title or tags
app.get('/api/bookmarks/search', (req, res) => {
  const query = (req.query.q || '').toLowerCase();
  const results = bookmarks.filter(b =>
    b.title.toLowerCase().includes(query) ||
    b.tags.some(t => t.toLowerCase().includes(query))
  );
  res.json(results);
});

What does this code do?

  • req.query.q grabs the q parameter from the URL (e.g. /api/bookmarks/search?q=react).
  • The filter checks each bookmark's title and every item in its tags array for a case-insensitive match.
  • Only bookmarks that match are returned in the response.
  • Save server.js and restart the server with Ctrl+C then node server.js.

What do you expect to see if you search for the term "react"?

  • Open your browser and navigate to http://localhost:5000/api/bookmarks/search?q=react.

You should see a JSON array containing only the React bookmark. The other two are filtered out because neither their title nor tags contain "react".

  • Try another search: http://localhost:5000/api/bookmarks/search?q=docs.

This returns the MDN Web Docs bookmark because "docs" appears in its tags array.

Search returns all bookmarks instead of filtered results?

  • Make sure the search route path is /api/bookmarks/search and it is placed ABOVE the POST route in your file.
  • Check that you're using req.query.q (not req.params.q). Query parameters use req.query.

Why is my search endpoint returning wrong results?

✔️ Awesome, I've got everything!

Great. Double-check you've saved server.js and your server is running without errors.

ⓧ I'd like to double check the full code

Your server.js file should contain these sections in order. Scroll up to confirm each is present:

  • The import express line, app and PORT constants, and app.use(express.json()) (from Step 1).
  • The let bookmarks = [...] array with three sample objects.
  • GET /api/health (from Step 1).
  • GET /api/bookmarks returning the array.
  • GET /api/bookmarks/search with the filter logic.
  • POST /api/bookmarks creating a new bookmark.
  • DELETE /api/bookmarks/:id removing by ID.
  • The app.listen call at the bottom (from Step 1).

Your API handles reading, creating, deleting, and searching bookmarks. The only problem is that everything vanishes the moment the server stops. Next up, you'll connect a cloud database so your bookmarks persist permanently.

Connect MongoDB Atlas

You saw the problem at the end of the last step. Every time you restart your Express server, the bookmarks array resets to its hardcoded starting state. Any bookmark you added through the API vanishes.

A real application needs a database that persists data independently of the server process. In this step, you will connect a free cloud MongoDB database to your Express API using Mongoose, so your bookmarks survive restarts, crashes, and redeployments.

In this step, get ready to:
  • Create a free MongoDB Atlas cluster and get your connection string.
  • Define a Mongoose schema and model for bookmarks.
  • Replace all hardcoded routes with real database queries.
Create a MongoDB Atlas account and free cluster

MongoDB Atlas provides a free cloud database that you can provision in minutes. No credit card is required, and the free tier never expires.

  • Open your browser and go to mongodb.com/cloud/atlas/register.
  • Sign up with your email, Google, or GitHub account.
  • After verifying your email and logging in, click Build a Database.
  • Select the Free tier (M0).
  • Choose a cloud provider (AWS, Google Cloud, or Azure) and the region closest to you.
  • Leave the cluster name as the default or customise it, then click Create Deployment.
  • Wait 1-3 minutes for provisioning to complete.

What is MongoDB Atlas?

MongoDB Atlas is a managed cloud database service. It hosts your MongoDB database on remote servers, so your data lives independently of your local machine.

The free tier gives you 512 MB of storage and up to 500 connections. That is far more than this project needs.

Next, create a database user and allow your machine to connect.

  • In the Security Quickstart panel, choose Username and Password authentication.
  • Enter a username (for example, bookmarks-user).
  • Click Autogenerate Secure Password and copy the generated password somewhere safe.
  • Click Create User.
  • Under network access, click Add My Current IP Address so Atlas allows connections from your machine.
  • Click Finish and Close.

Now grab your connection string.

  • On your cluster page, click Connect.
  • Select Drivers.
  • Copy the connection string that starts with mongodb+srv://.
  • Replace the <password> placeholder in the string with the password you copied earlier.

Now store that connection string in your project. Create a .env file inside your server folder with the following content:

MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5000
  • Replace your-mongodb-atlas-connection-string-here with the full mongodb+srv://... string you copied (including your real password).
  • Save .env.

Why a .env file?

Your connection string contains your database username and password. Storing credentials in a .env file keeps them out of your source code, so you never accidentally commit secrets to Git.

  • Confirm your new file exists by running this from inside the server folder:
ls -a

You should see .env listed alongside server.js and package.json.

Don't see .env in the list?

Files starting with a dot are hidden by default. Make sure you used ls -a (with the -a flag) to show hidden files. Also confirm you created the file inside the server folder, not the project root.

help me find my .env file

Create the Mongoose model

Mongoose lets you define a schema that describes what a bookmark document looks like in MongoDB. This enforces structure on your data and provides a clean API for queries.

  • Create a models folder inside your server directory by running:
mkdir models
  • Create a new file called Bookmark.js inside server/models and add the following code:
import mongoose from 'mongoose';

// Define the shape of a bookmark document
const bookmarkSchema = new mongoose.Schema({
  title: { type: String, required: true },
  url: { type: String, required: true },
  tags: [String],
  createdAt: { type: Date, default: Date.now }
});

// Compile the schema into a model
const Bookmark = mongoose.model('Bookmark', bookmarkSchema);

export default Bookmark;

What does this schema do?

  • title and url are required strings. MongoDB will reject any bookmark missing these fields.
  • tags is an array of strings for categorising bookmarks.
  • createdAt defaults to the current date so you can sort bookmarks by newest first.
  • Save models/Bookmark.js.
  • Confirm the file exists by running:
ls models

You should see Bookmark.js listed.

Seeing 'No such file or directory'?

Make sure you ran mkdir models from inside the server folder before creating the file. Run pwd to check your current directory.

help me create the models folder in the right place

Replace hardcoded routes with Mongoose queries

Your model is ready, but the server still uses the old hardcoded array. Now you will rewrite server.js to connect to MongoDB and use real database queries for every route.

  • Replace the entire contents of server.js with this code (the imports, config, connection, and health route):
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
import Bookmark from './models/Bookmark.js';

// Load environment variables from .env
dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

// Allow requests from the Vite dev server
app.use(cors({ origin: 'http://localhost:5173' }));
app.use(express.json());

// Connect to MongoDB Atlas
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.error('MongoDB connection error:', err));

// Health check route
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

What changed here?

  • dotenv.config() loads your MONGODB_URI from the .env file into process.env.
  • mongoose.connect() opens a persistent connection to your Atlas cluster.
  • cors({ origin: 'http://localhost:5173' }) allows your React frontend (which runs on port 5173) to make requests to this server. You will need this in the next step.
  • The hardcoded bookmarks array is gone. From now on, all data lives in MongoDB.
  • Save server.js.
  • Stop your running server with Ctrl+C, then restart it:
node server.js

You should see Connected to MongoDB printed in your terminal. That confirms your .env credentials are correct and your Atlas cluster is reachable.

Seeing a MongoDB connection error?

Check that your MONGODB_URI in .env contains your real password (not the <password> placeholder). Also verify you added your current IP address to the Atlas network access list.

If your password contains special characters like @ or #, they must be URL-encoded in the connection string (for example, @ becomes %40).

help me fix my MongoDB connection error

The connection works. Now add the CRUD routes that query MongoDB instead of an array.

  • In server.js, find the health check route. Add this code between the health route and app.listen:
// Get all bookmarks, newest first
app.get('/api/bookmarks', async (req, res) => {
  try {
    const bookmarks = await Bookmark.find({}).sort({ createdAt: -1 });
    res.json(bookmarks);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Search bookmarks by title or tag
app.get('/api/bookmarks/search', async (req, res) => {
  try {
    const query = req.query.q || '';
    const regex = new RegExp(query, 'i');
    const bookmarks = await Bookmark.find({
      $or: [
        { title: regex },
        { tags: regex }
      ]
    }).sort({ createdAt: -1 });
    res.json(bookmarks);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

What do these queries do?

  • Bookmark.find({}) retrieves every document in the bookmarks collection.
  • .sort({ createdAt: -1 }) orders results with the newest bookmark first.
  • The search route uses a case-insensitive regex with $or to match either the title or any tag.
  • Each route is wrapped in try/catch so database errors return a proper 500 response instead of crashing the server.
  • Still in server.js, add the POST and DELETE routes below the search route (still above app.listen):
// Create a new bookmark
app.post('/api/bookmarks', async (req, res) => {
  try {
    const bookmark = await Bookmark.create({
      title: req.body.title,
      url: req.body.url,
      tags: req.body.tags || []
    });
    res.status(201).json(bookmark);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Delete a bookmark by ID
app.delete('/api/bookmarks/:id', async (req, res) => {
  try {
    await Bookmark.findByIdAndDelete(req.params.id);
    res.json({ message: 'Bookmark deleted' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

What do POST and DELETE do now?

  • Bookmark.create() inserts a new document into MongoDB and returns it (including the auto-generated _id).
  • Bookmark.findByIdAndDelete() finds a document by its MongoDB _id and removes it in one operation.
  • Validation errors (like a missing title) return a 400 status because they are client mistakes, not server failures.
  • Save server.js.
  • Stop the server with Ctrl+C and restart it:
node server.js

You should see Connected to MongoDB again. Now visit http://localhost:5000/api/bookmarks in your browser.

You will see an empty array [] because your MongoDB database has no bookmarks yet. The hardcoded data is gone.

  • Add a test bookmark by running this in a new terminal window:
curl -X POST http://localhost:5000/api/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"title": "MDN Web Docs", "url": "https://developer.mozilla.org", "tags": ["docs", "web"]}'

You should see a JSON response with your new bookmark, including an _id field that MongoDB generated.

On Windows PowerShell?

PowerShell does not support single quotes around JSON the same way. You can use the browser console instead. Open http://localhost:5000 in your browser, open DevTools with F12, go to the Console tab, and run:

fetch('http://localhost:5000/api/bookmarks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'MDN Web Docs', url: 'https://developer.mozilla.org', tags: ['docs', 'web'] })
}).then(r => r.json()).then(console.log)

Before you check your browser, predict what will happen. If you stop the server and restart it, will this bookmark still be there?

  • Stop the server with Ctrl+C.
  • Start it again:
node server.js
  • Visit http://localhost:5000/api/bookmarks in your browser.

Your bookmark is still there. The data persists because it now lives in MongoDB Atlas, not in your server's memory.

Bookmark disappeared after restart?

If the bookmark array is empty after restarting, your POST request may not have succeeded. Check the terminal where you ran the command for any error messages.

Also verify your MONGODB_URI in .env points to a valid cluster. If the connection string has a typo, Mongoose may connect silently but writes go nowhere.

help me debug why my bookmarks are not persisting

✔️ Awesome, I've got everything!

Great. Double check you have saved both server.js and models/Bookmark.js, then move on.

ⓧ I'd like to double check the full code

Your server/server.js is built from three chunks above. Scroll up and confirm each one is present in order:

  • The top section: imports, dotenv.config(), app setup, mongoose.connect(), and the health route.
  • The GET and search routes using Bookmark.find().
  • The POST and DELETE routes using Bookmark.create() and Bookmark.findByIdAndDelete().
  • app.listen() at the very bottom.

Your server/models/Bookmark.js should look like this:

import mongoose from 'mongoose';

const bookmarkSchema = new mongoose.Schema({
  title: { type: String, required: true },
  url: { type: String, required: true },
  tags: [String],
  createdAt: { type: Date, default: Date.now }
});

const Bookmark = mongoose.model('Bookmark', bookmarkSchema);

export default Bookmark;

Your server/.env should contain:

MONGODB_URI=your-mongodb-atlas-connection-string-here
PORT=5000

Your API now has a real database behind it. Next up, you will build the React frontend that talks to this API and gives your bookmark manager a proper user interface.

Build the React Frontend

Your Express API works perfectly. You can hit every endpoint from the browser address bar or curl. But real users need a real interface.

In this step, you will build a React frontend that talks to your API over HTTP. Along the way, you will learn about CORS (Cross-Origin Resource Sharing), the browser security policy that controls whether a frontend on one port can fetch data from a server on a different port.

In this step, get ready to:
  • Build a React component that fetches and displays bookmarks from your API.
  • Add a form to create bookmarks and a button to delete them.
  • Style the app with CSS so it looks like a real product.
Build the bookmark list component

Your React app currently shows the default Vite starter page. You will replace it with a component that fetches bookmarks from your Express API and renders them as cards.

  • Switch to your VS Code window with the client folder open.
  • Open client/src/App.jsx and select all the existing code, then delete it.
  • Replace the file contents with the following code. Start with the imports, state setup, and the main fetch function:
import { useState, useEffect, useRef } from 'react';
import './App.css';

const API_URL = 'http://localhost:5000/api';

function App() {
  const [bookmarks, setBookmarks] = useState([]);
  const [title, setTitle] = useState('');
  const [url, setUrl] = useState('');
  const [tags, setTags] = useState('');
  const [searchQuery, setSearchQuery] = useState('');
  const [loading, setLoading] = useState(true);
  const searchTimeout = useRef(null);

  useEffect(() => {
    fetchBookmarks();
  }, []);

  async function fetchBookmarks() {
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/bookmarks`);
      const data = await res.json();
      setBookmarks(data);
    } catch (err) {
      console.error('Failed to fetch bookmarks:', err);
    }
    setLoading(false);
  }

What does this code do?

API_URL stores the base address of your Express server. Every fetch call uses this constant so you only update the URL in one place.

useEffect with an empty dependency array runs fetchBookmarks() once when the component first loads. The function calls your GET /api/bookmarks endpoint and stores the response in state.

  • Directly below the fetchBookmarks function, add the search and event handler functions:
  async function searchBookmarks(query) {
    if (!query.trim()) {
      fetchBookmarks();
      return;
    }
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/bookmarks/search?q=${encodeURIComponent(query)}`);
      const data = await res.json();
      setBookmarks(data);
    } catch (err) {
      console.error('Search failed:', err);
    }
    setLoading(false);
  }

  function handleSearchChange(e) {
    const value = e.target.value;
    setSearchQuery(value);
    if (searchTimeout.current) clearTimeout(searchTimeout.current);
    searchTimeout.current = setTimeout(() => {
      searchBookmarks(value);
    }, 300);
  }

  function handleTagClick(tag) {
    setSearchQuery(tag);
    searchBookmarks(tag);
  }

What is CORS and why does this fetch work?

Your React app runs on http://localhost:5173 and your Express API runs on http://localhost:5000. Browsers treat these as different origins because the ports differ. By default, a browser blocks any fetch() call from one origin to another.

Without the cors middleware on your server, the browser would show an error in the console like "Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource." Your Express server already has cors({ origin: 'http://localhost:5173' }) configured from a previous step, which adds the Access-Control-Allow-Origin header to every response and tells the browser to allow the request.

Build the add bookmark form

With the data-fetching layer in place, you need a way to create and delete bookmarks from the UI.

  • Below the handleTagClick function, add the form submission handler:
  async function handleSubmit(e) {
    e.preventDefault();
    if (!title.trim() || !url.trim()) return;
    try {
      const res = await fetch(`${API_URL}/bookmarks`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: title.trim(),
          url: url.trim(),
          tags: tags.split(',').map(t => t.trim()).filter(t => t)
        })
      });
      if (res.ok) {
        setTitle('');
        setUrl('');
        setTags('');
        fetchBookmarks();
      }
    } catch (err) {
      console.error('Failed to add bookmark:', err);
    }
  }
  • Directly below handleSubmit, add the delete handler:
  async function handleDelete(id) {
    try {
      await fetch(`${API_URL}/bookmarks/${id}`, { method: 'DELETE' });
      setBookmarks(bookmarks.filter(b => b._id !== id));
    } catch (err) {
      console.error('Failed to delete bookmark:', err);
    }
  }

What do these handlers do?

handleSubmit sends a POST request with the bookmark data as JSON. It splits the tags string on commas, trims whitespace, and filters out empty entries. After a successful response, it clears the form inputs and refetches the full list.

handleDelete sends a DELETE request for a specific bookmark ID, then immediately removes it from the local state so the card disappears without waiting for a full refetch.

  • Now add the component's return statement. Start with the form section below handleDelete:
  return (
    <div className="app">
      <h1>Bookmark Manager</h1>

      <form onSubmit={handleSubmit} className="add-form">
        <input
          type="text"
          placeholder="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="Tags (comma-separated)"
          value={tags}
          onChange={e => setTags(e.target.value)}
        />
        <button type="submit">Add Bookmark</button>
      </form>
  • Below the </form> closing tag, add the search input, loading states, and the start of the bookmark grid:
      <input
        type="text"
        placeholder="Search bookmarks..."
        value={searchQuery}
        onChange={handleSearchChange}
        className="search-input"
      />

      {loading && <p className="status">Loading...</p>}
      {!loading && bookmarks.length === 0 && <p className="status">No bookmarks found</p>}

      <div className="bookmarks-grid">
        {bookmarks.map(bookmark => (
          <div key={bookmark._id} className="bookmark-card">
            <h3>{bookmark.title}</h3>
            <a href={bookmark.url} target="_blank" rel="noopener noreferrer">
              {bookmark.url}
            </a>
  • Finally, add the tag chips, delete button, and closing tags to complete the component:
            <div className="tags">
              {bookmark.tags.map(tag => (
                <span
                  key={tag}
                  className="tag"
                  onClick={() => handleTagClick(tag)}
                >
                  {tag}
                </span>
              ))}
            </div>
            <button onClick={() => handleDelete(bookmark._id)} className="delete-btn">
              Delete
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;
  • Save App.jsx.
  • Switch to your browser at http://localhost:5173. You should see the heading "Bookmark Manager", a form with three inputs and an "Add Bookmark" button, a search bar, and any bookmarks you previously added displayed as cards.

Seeing a blank page or errors?

Check the Vite terminal for syntax errors. A missing closing bracket or brace will prevent the component from compiling.

Make sure your Express server is still running in the other terminal on port 5000. If the page loads but shows "Loading..." forever, the fetch may be failing silently.

Open your browser's DevTools console and look for error messages. A network error means the server is down. A JSON parse error means the server returned HTML instead of JSON.

help me debug my React bookmark manager

Add basic styling

The component works, but it looks like raw HTML. A CSS file transforms it into something you would actually want to use.

  • Open client/src/App.css and delete all the existing Vite default styles.
  • Replace the file contents with the following global resets and layout styles:
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #f5f5f5;
  color: #333;
}

.app {
  max-width: 900px;
  margin: 0 auto;
  padding: 2rem;
}

h1 {
  text-align: center;
  margin-bottom: 2rem;
  color: #1a1a1a;
}
  • Below those styles, add the form and button styles:
.add-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
  padding: 1rem;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.add-form input {
  flex: 1;
  min-width: 150px;
  padding: 0.6rem 1rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 0.9rem;
}

.add-form button {
  padding: 0.6rem 1.5rem;
  background: #4f46e5;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
}

.add-form button:hover {
  background: #4338ca;
}
  • Add the search input, status message, and grid layout styles:
.search-input {
  width: 100%;
  padding: 0.8rem 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 1rem;
  margin-bottom: 1.5rem;
}

.status {
  text-align: center;
  color: #666;
  padding: 2rem;
}

.bookmarks-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
}
  • Add the bookmark card styles:
.bookmark-card {
  background: white;
  padding: 1.2rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.bookmark-card h3 {
  font-size: 1rem;
  color: #1a1a1a;
}

.bookmark-card a {
  color: #4f46e5;
  font-size: 0.85rem;
  word-break: break-all;
  text-decoration: none;
}

.bookmark-card a:hover {
  text-decoration: underline;
}
  • Finally, add the tag chip and delete button styles:
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.tag {
  background: #e0e7ff;
  color: #4338ca;
  padding: 0.2rem 0.6rem;
  border-radius: 12px;
  font-size: 0.75rem;
  cursor: pointer;
}

.tag:hover {
  background: #c7d2fe;
}

.delete-btn {
  align-self: flex-start;
  margin-top: 0.5rem;
  padding: 0.3rem 0.8rem;
  background: #fee2e2;
  color: #dc2626;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
}

.delete-btn:hover {
  background: #fecaca;
}
  • Save App.css.
  • Return to your browser at http://localhost:5173. The page should auto-refresh and you should see a clean layout with a white form card at the top, a full-width search bar below it, and any existing bookmarks displayed as rounded white cards in a responsive grid.

Styles not applying?

Make sure App.jsx imports './App.css' at the top. Without this import, Vite will not bundle the CSS.

Check that the class names in your CSS match the class names in your JSX exactly. For example, className="add-form" in JSX must match .add-form in CSS.

help me fix my CSS styles in the bookmark manager

Before you move on, do a prediction check. What do you expect to see when you fill out the form and click "Add Bookmark"?

  • Enter a title like React Docs, a URL like https://react.dev, and a tag like react in the form fields.
  • Click Add Bookmark.

A new card should appear instantly in the grid below the search bar. The form fields clear themselves. No page refresh needed. Your bookmark is now stored in MongoDB and will survive a server restart.

✔️ 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

Your App.jsx should contain these sections in order (scroll up and confirm each is present):

  • Imports (useState, useEffect, useRef) and API_URL constant.
  • Six state variables plus searchTimeout ref.
  • useEffect calling fetchBookmarks() on mount.
  • fetchBookmarks, searchBookmarks, handleSearchChange, handleTagClick functions.
  • handleSubmit and handleDelete functions.
  • Return statement with heading, form, search input, loading/empty states, and bookmarks grid.
  • export default App at the bottom.

Your App.css should contain these selectors in order:

  • *, body, .app, h1 (global resets and layout).
  • .add-form, .add-form input, .add-form button, .add-form button:hover (form styles).
  • .search-input, .status, .bookmarks-grid (search and grid layout).
  • .bookmark-card, .bookmark-card h3, .bookmark-card a, .bookmark-card a:hover (card styles).
  • .tags, .tag, .tag:hover, .delete-btn, .delete-btn:hover (tag and delete button styles).

Your bookmark manager now has a working frontend. You can add bookmarks, see them rendered as cards, and delete the ones you no longer need. Next up, you will add real-time search so you can instantly filter your growing collection.

Add Search and Filtering

Your frontend is connected to your API, bookmarks persist in MongoDB, and the UI renders cards with tags and delete buttons.

A bookmark manager without search is useless once you have more than a handful of links. This step ties the full loop together: user types in the React frontend, React calls the Express API, Express queries MongoDB, and results flow back to the screen.

In this step, get ready to:
  • Test real-time debounced search against your MongoDB data.
  • Use clickable tag chips to filter bookmarks instantly.
  • Verify loading and empty state feedback in the UI.
Add test data and search your bookmarks

To see search in action, you need several bookmarks with different titles and tags in your database.

  • Switch back to your browser at http://localhost:5173.
  • Use the add bookmark form to create at least 4 bookmarks with varied titles and tags.
  • For example, add one with the title React Docs and tags react, frontend.
  • Add another with the title Express Guide and tags backend, node.
  • Add one more with the title MongoDB Tutorial and tags database, backend.

You should see all your bookmarks appear as cards in the grid below the form.

  • Now type react into the search bar above the bookmark cards.
  • Wait about a third of a second (the debounce delay) and watch the bookmark list filter down to only cards whose title or tags contain "react".

How does the full-stack search flow work?

When you type in the search bar, here is what happens across all four layers of your MERN app:

  • handleSearchChange updates the input state and sets a 300ms setTimeout. This is debouncing. It prevents a network request on every single keystroke.
  • After 300ms of no typing, searchBookmarks fires a GET request to /api/bookmarks/search?q=react.
  • Express receives the request, builds a case-insensitive regex, and queries MongoDB with a $or filter checking both title and tags.
  • MongoDB returns matching documents. Express sends them as JSON. React updates state and the UI re-renders with only the matching cards.
  • Clear the search bar completely.
  • Confirm all your bookmarks reappear. When the input is empty, fetchBookmarks() is called to restore the full list.

Search not filtering results?

  • Make sure both servers are still running. Check the terminal running node server.js and the terminal running npm run dev.
  • Open your browser's developer console and look for network errors. If you see a CORS error, restart your Express server.
  • help me debug why my search requests are not reaching the Express server
Click tag chips to filter by topic

Typing a search works, but sometimes you just want to click a tag you see and instantly find all bookmarks with that same tag.

  • Find a bookmark card that has a tag chip (the small pill-shaped label below the URL).
  • Click that tag chip.
  • Observe two things: the search bar is now populated with the tag text, and the bookmark list shows only cards whose title or tags match that term.

What happens when you click a tag?

The handleTagClick function does two things: it sets searchQuery to the tag text (updating the input visually) and immediately calls searchBookmarks(tag) to filter the results.

This skips the debounce delay because the learner made a deliberate click. There is no need to wait for more input.

  • Try clicking a different tag chip, like backend, and confirm the results update to show all bookmarks tagged with that term.
Verify loading and empty states

Good apps show feedback while data is loading, and a clear message when there are no results. Your app handles both.

  • Type a search term that does not match any of your bookmarks, like xyznoexist.
  • After the debounce delay, you should see the message "No bookmarks found" where the card grid was.

How do loading and empty states work?

The loading state is set to true at the start of every fetch and back to false when the response arrives. While true, the UI renders "Loading..." instead of the card grid.

The empty state checks two conditions: !loading AND bookmarks.length === 0. This prevents "No bookmarks found" from flashing while data is still being fetched.

  • Clear the search bar completely and confirm all your bookmarks reappear.

Before you test the final end-to-end flow, predict what will happen: if you type a tag name that you used on multiple bookmarks, how many cards will appear?

  • Type backend into the search bar and wait for the debounce to fire.

You should see only the bookmarks whose title or tags contain "backend". If you added Express Guide (tagged backend, node) and MongoDB Tutorial (tagged database, backend), both appear. The full loop works: keystroke in React, debounced request to Express, regex query in MongoDB, filtered results rendered as cards.

Seeing all bookmarks instead of filtered results?

  • Check that the search route in your server responds to /api/bookmarks/search (not /api/search). The path must match what the frontend requests.
  • Open the Network tab in DevTools and confirm the request URL includes ?q=backend at the end.
  • help me debug why my search endpoint is not filtering bookmarks correctly

✔️ Awesome, I've got everything!

Your search, tag clicking, and loading states are all working. The full MERN data flow is proven end-to-end.

ⓧ I'd like to double check the full code

Here is the complete App.jsx file. Confirm your code matches:

import { useState, useEffect, useRef } from 'react';
import './App.css';

const API_URL = 'http://localhost:5000/api';

function App() {
  const [bookmarks, setBookmarks] = useState([]);
  const [title, setTitle] = useState('');
  const [url, setUrl] = useState('');
  const [tags, setTags] = useState('');
  const [searchQuery, setSearchQuery] = useState('');
  const [loading, setLoading] = useState(true);
  const searchTimeout = useRef(null);
  useEffect(() => {
    fetchBookmarks();
  }, []);

  async function fetchBookmarks() {
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/bookmarks`);
      const data = await res.json();
      setBookmarks(data);
    } catch (err) {
      console.error('Failed to fetch bookmarks:', err);
    }
    setLoading(false);
  }
  async function searchBookmarks(query) {
    if (!query.trim()) {
      fetchBookmarks();
      return;
    }
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/bookmarks/search?q=${encodeURIComponent(query)}`);
      const data = await res.json();
      setBookmarks(data);
    } catch (err) {
      console.error('Search failed:', err);
    }
    setLoading(false);
  }
  function handleSearchChange(e) {
    const value = e.target.value;
    setSearchQuery(value);
    if (searchTimeout.current) clearTimeout(searchTimeout.current);
    searchTimeout.current = setTimeout(() => {
      searchBookmarks(value);
    }, 300);
  }

  function handleTagClick(tag) {
    setSearchQuery(tag);
    searchBookmarks(tag);
  }
  async function handleSubmit(e) {
    e.preventDefault();
    if (!title.trim() || !url.trim()) return;
    try {
      const res = await fetch(`${API_URL}/bookmarks`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: title.trim(),
          url: url.trim(),
          tags: tags.split(',').map(t => t.trim()).filter(t => t)
        })
      });
      if (res.ok) {
        setTitle('');
        setUrl('');
        setTags('');
        fetchBookmarks();
      }
    } catch (err) {
      console.error('Failed to add bookmark:', err);
    }
  }
  async function handleDelete(id) {
    try {
      await fetch(`${API_URL}/bookmarks/${id}`, { method: 'DELETE' });
      setBookmarks(bookmarks.filter(b => b._id !== id));
    } catch (err) {
      console.error('Failed to delete bookmark:', err);
    }
  }
  return (
    <div className="app">
      <h1>Bookmark Manager</h1>

      <form onSubmit={handleSubmit} className="add-form">
        <input
          type="text"
          placeholder="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="Tags (comma-separated)"
          value={tags}
          onChange={e => setTags(e.target.value)}
        />
        <button type="submit">Add Bookmark</button>
      </form>
      <input
        type="text"
        placeholder="Search bookmarks..."
        value={searchQuery}
        onChange={handleSearchChange}
        className="search-input"
      />

      {loading && <p className="status">Loading...</p>}
      {!loading && bookmarks.length === 0 && <p className="status">No bookmarks found</p>}
      <div className="bookmarks-grid">
        {bookmarks.map(bookmark => (
          <div key={bookmark._id} className="bookmark-card">
            <h3>{bookmark.title}</h3>
            <a href={bookmark.url} target="_blank" rel="noopener noreferrer">
              {bookmark.url}
            </a>
            <div className="tags">
              {bookmark.tags.map(tag => (
                <span
                  key={tag}
                  className="tag"
                  onClick={() => handleTagClick(tag)}
                >
                  {tag}
                </span>
              ))}
            </div>
            <button onClick={() => handleDelete(bookmark._id)} className="delete-btn">
              Delete
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;

You have a fully working search-and-filter loop flowing across all four layers of the MERN stack. Next up, the Secret Mission challenges you to build multi-tag filtering with color-coded chips.

Secret mission

Multi-Tag Filtering with Color-Coded Tags

Your single-tag search works, but real bookmark managers let you filter by multiple tags at once. Can you build a multi-tag filtering system where clicking two tag chips shows only bookmarks that match both, with each tag assigned a consistent color?

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, so there are no ongoing costs.

Resources you used:

  • MongoDB Atlas free cluster (stores your bookmark data in the cloud).
  • Local project files (server/ and client/ folders on your Desktop).

Keep everything running

No action needed. Choose this if you plan to keep building on this project.

  • Your MongoDB Atlas free cluster stays running at no cost. It auto-pauses after 30 days of zero connections, and you can resume it anytime.
  • Your local project files remain on your Desktop ready for future edits.
  • Node.js and npm remain installed on your machine for other projects.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and database stay intact for when you return.

  • In each terminal window running your servers, press Ctrl+C to stop the process.
  • Your Atlas cluster auto-pauses after 30 days of inactivity. No manual action needed.
  • When you come back, run node server.js in the server/ folder and npm run dev in the client/ folder to pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is irreversible for your database data.

  • Stop both servers by pressing Ctrl+C in each terminal window.
  • Delete your MongoDB Atlas cluster by going to cloud.mongodb.com. Open the Database page for your project. Click the ellipsis ... next to your cluster, then click Terminate. Confirm when prompted. This permanently deletes all your bookmark data.
  • Delete your local project files. On macOS or Linux, run this from your Desktop:
cd ~/Desktop
rm -rf server client

On Windows, run this from your Desktop:

cd ~\Desktop
Remove-Item -Recurse -Force server, client

Nice Work!

Nice Work!

You just built a full-stack bookmark manager using the complete MERN stack. Your app saves, searches, and filters bookmarks through a React frontend talking to an Express API backed by a cloud MongoDB database.

You learned how to:

  • Build a REST API with Express and define routes for GET, POST, DELETE, and search operations that persist data in a cloud MongoDB database using Mongoose.
  • Scaffold a React frontend with Vite and connect it to your backend API, diagnosing and fixing the CORS cross-origin error that every full-stack developer encounters.
  • Implement real-time search that flows from a React input through Express to MongoDB and back, with debounced requests and proper loading states.
  • Secret Mission: Built a multi-tag filtering system with color-coded chips and MongoDB $all queries for filtering bookmarks by multiple tags simultaneously.

Ready to quiz yourself?