Build a Full-Stack MERN App

Build a code snippet organizer with React, Express, and MongoDB Atlas.

Introduction

30 Second Summary

Every developer collects useful code snippets over time. Maybe it's a clever CSS trick, a sorting algorithm, or a handy API call. But they end up scattered across random files, old projects, and browser bookmarks you'll never find again.

In this project, you will build a full-stack Code Snippet Vault where you can save, browse, and delete reusable code snippets with professional syntax highlighting. You'll build the React frontend, the Express API, and the MongoDB database from scratch, then deploy everything to the internet with a shareable URL.

What You'll Build

Open your live URL, paste a code snippet, pick the language, and watch it render with coloured syntax highlighting in a sleek dark-themed card. Share that URL with a friend and they can see your snippets too.

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

  • A live web app where you paste any code and instantly see it rendered with professional syntax highlighting in a dark-themed card layout.
  • A REST API you built from scratch that creates, reads, and deletes snippets from a real cloud database. Your data survives page refreshes and browser restarts.
  • A deployed full-stack application on Vercel and Render with a shareable live URL you can put on your resume.
  • Secret Mission: Add a language filter dropdown so you can view only JavaScript, Python, or CSS snippets at a time.

Are there any prerequisites?

You should have basic HTML, CSS, and JavaScript knowledge. You'll also need Node.js installed, VS Code installed, and a GitHub account. Step 1 walks you through everything else.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

You're building a full-stack Code Snippet Vault from scratch. By the end, you'll have a live URL with a working app behind it that proves you can build a React frontend, an Express API, and a MongoDB database, and wire them all together.

Set Up the Project

A full-stack app has three moving parts: a frontend the user sees, a backend that handles logic, and a database that stores data permanently.

In this step, you'll scaffold all three parts of your Code Snippet Vault. Once they exist, every future step produces real, visible progress instead of invisible wiring.

In this step, get ready to:
  • Create a MongoDB Atlas free cluster and copy the connection string.
  • Scaffold the React frontend with Vite.
  • Set up the Express backend.
Create a MongoDB Atlas free cluster

Your app needs a database that lives in the cloud so snippets persist between sessions and across devices. MongoDB Atlas provides a free-forever cluster that handles this perfectly.

  • Return to your MongoDB Atlas dashboard at cloud.mongodb.com.
  • Click Create to open the Deploy your cluster page.
  • Select the M0 option (Free forever).
  • Choose AWS as the cloud provider and pick a region close to you.
  • Leave the cluster name as the default or enter a name like SnippetVault.
  • Click Create to deploy the cluster. It takes about 15 seconds.

The Security Quickstart wizard appears after your cluster deploys.

  • Under Create a Database User, enter a username and a password you will remember.
  • Click Create Database User.
  • Under the IP access list, click Add My Current IP Address.
  • Click Finish and Close, then click Go to Overview.

Now grab the connection string so your Express server can talk to this database later.

  • On the cluster overview page, click the Connect button.
  • Select Drivers as the connection method.
  • Copy the connection string. It looks like this:
mongodb+srv://your-username:your-password@cluster0.xxxxx.mongodb.net/snippet-vault?retryWrites=true&w=majority
  • Paste this connection string into a text file (Notepad, TextEdit, or a sticky note) for safekeeping. You will use it in a later step.
  • Replace <password> in the string with the database user password you just created.

What is a connection string?

A connection string is the address your server uses to find and authenticate with your database. It contains the cluster host, your credentials, and the database name all in one line.

Don't see the M0 Free option?

Atlas allows only one Free cluster per project. If you already have one from a previous project, delete it first or create a new Atlas project. Also try selecting a different cloud provider region, as some regions occasionally have limited free-tier availability.

help me troubleshoot my MongoDB Atlas free cluster creation

Scaffold the React frontend with Vite

Vite is a modern build tool that scaffolds a React project in seconds and gives you instant hot-reload during development. It requires Node.js version 20.19 or higher.

  • Check your Node.js version by running this command in your terminal:
node --version

✔️ I see v22 or higher

You are good to go. Node.js v22+ and v24+ both satisfy Vite's requirements.

ⓧ I see a version lower than 20.19

Vite requires Node.js 20.19 or higher. Node.js 20 reached end-of-life in March 2026, so the best path is to install the current LTS (v24).

  • Visit nodejs.org/en/download and download the LTS installer for your operating system.
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run node --version again to confirm it shows v24 or higher.

ⓧ Command not found

Node.js is not installed on your machine.

  • Visit nodejs.org/en/download and download the LTS installer for your operating system.
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run node --version to confirm the installation succeeded.
  • Open a terminal in VS Code by pressing Cmd+J (macOS) or Ctrl+J (Windows).
  • Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir snippet-vault
cd snippet-vault
  • Scaffold the React frontend by running this command:
npm create vite@latest client -- --template react

You should see output confirming the project was scaffolded, ending with instructions to cd client and install dependencies.

  • Install the project dependencies and the syntax highlighting library by running these commands:
cd client
npm install
npm install react-syntax-highlighter

You should see output listing the number of packages installed with no errors.

What is react-syntax-highlighter?

This library renders code with coloured syntax highlighting inside your React components. It supports dozens of languages and themes out of the box, so your saved snippets look professional without any manual styling.

Seeing permission errors during install?

On macOS, never run npm install with sudo. If you see EACCES errors, fix your npm permissions by following the guide at docs.npmjs.com/resolving-eacces-permissions-errors.

help me fix npm permission errors on my machine

Create the Express backend

Express is a lightweight Node.js framework for building APIs. You will use it to handle HTTP requests and connect to your MongoDB database.

  • Navigate back to the project root and create the server folder by running these commands:
cd ..
mkdir server
cd server
  • Initialize a new Node.js project and install the backend packages by running these commands:
npm init -y
npm install express mongoose cors

You should see npm init print the generated package.json contents, followed by a confirmation that three packages were installed.

What are these three packages?

  • express handles HTTP requests and routing for your API.
  • mongoose provides a schema-based way to interact with MongoDB from Node.js.
  • cors enables your React frontend (running on a different port) to make requests to this server.

Now update the package.json so it has the correct project name and a start script.

  • Open the snippet-vault folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the snippet-vault folder on your Desktop, then click Open.
  • In the VS Code Explorer sidebar, expand the server folder and click on package.json to open it.
  • Find the "name" field and change its value from "server" to "snippet-vault-server".
  • Find the "scripts" section and replace the "test" script with a "start" script, so it looks like this:
"scripts": {
  "start": "node server.js"
}
  • Save package.json.

Finally, create the empty server file that you will fill in later.

  • Right-click the server folder in the Explorer sidebar.
  • Click New File.
  • Name the file server.js and press Enter.

You should now see server.js listed inside the server folder in the Explorer sidebar.

Don't see the server folder in VS Code?

Make sure you opened the snippet-vault folder (the parent that contains both client and server), not the client folder alone. Use File > Open Folder and navigate to ~/Desktop/snippet-vault.

help me open the correct project folder in VS Code

✔️ Awesome, I've got everything!

Great. Double check that your Explorer sidebar shows both a client folder and a server folder, with server.js and package.json inside server.

ⓧ I'd like to double check the full code

Your server/package.json should look like this (your dependency version numbers may be more specific, like ^4.21.2 instead of ^4. That is fine):

{
  "name": "snippet-vault-server",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "cors": "^2",
    "express": "^4",
    "mongoose": "^8"
  }
}

Your server/server.js file should exist and be empty. You will add code to it in a later step.

Verify the frontend runs

Everything is scaffolded. Before you move on, confirm the React dev server starts successfully.

  • Open a new terminal in VS Code by pressing Cmd+J (macOS) or Ctrl+J (Windows).
  • Navigate to the client folder and start the dev server by running these commands:
cd client
npm run dev

Before you check the browser, what do you expect to see at the URL Vite prints in the terminal?

  • Open http://localhost:5173 in your browser.

You should see the default Vite + React starter page with the Vite and React logos and a counter button. This confirms your frontend scaffolding is working.

What about the terminal?

The npm run dev command keeps running and occupies that terminal tab. This is expected. It watches your files and auto-refreshes the browser when you make changes. Press Ctrl+C to stop it when you need that terminal back.

Page not loading at localhost:5173?

Make sure you ran npm run dev from inside the client folder, not the server or snippet-vault root. Check the terminal output for the exact URL. If port 5173 is in use, Vite picks the next available port and prints it.

help me get the Vite dev server running

Your three-part foundation is ready. You have a cloud database, a React frontend, and an Express backend skeleton. Next up, you will build the snippet display components so you can see your app take shape in the browser.

Build the Snippet Display

In Step 1, you scaffolded a React frontend, an Express backend, and a cloud database. Now it is time to see what this app actually looks like.

Before wiring up any API or database, you will build the React UI with hardcoded sample data. This gives you instant visual feedback and proves your components render correctly before adding real complexity.

In this step, get ready to:
  • Create a SnippetCard component that renders code with syntax highlighting.
  • Create a SnippetList component that displays a collection of cards.
  • Wire up App.jsx with sample data and dark-theme styles.
Create the SnippetCard component

Each snippet in your vault needs a card that shows the title, a language badge, the code with colored syntax highlighting, and any tags. The react-syntax-highlighter library handles the heavy lifting of coloring code.

  • In VS Code, create a new folder called components inside client/src/.
  • Create a new file called SnippetCard.jsx inside the components folder.
  • Add the following code to SnippetCard.jsx. Start with the imports and the top half of the component:
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';

// Renders a single snippet card with syntax-highlighted code
function SnippetCard({ snippet, onDelete }) {
  return (
    <div className="snippet-card">
      <div className="snippet-header">
        <h3>{snippet.title}</h3>
        <div className="snippet-actions">
          <span className="language-badge">{snippet.language}</span>
          <button className="delete-button" onClick={() => onDelete(snippet._id)}>
            Delete
          </button>
        </div>
      </div>
  • Directly below, still in the same file, add the content section and close the component:
      {snippet.description && <p className="snippet-description">{snippet.description}</p>}
      {/* Syntax-highlighted code display */}
      <SyntaxHighlighter language={snippet.language} style={oneDark}>
        {snippet.code}
      </SyntaxHighlighter>
      {snippet.tags && snippet.tags.length > 0 && (
        <div className="snippet-tags">
          {snippet.tags.map((tag, index) => (
            <span key={index} className="tag">{tag}</span>
          ))}
        </div>
      )}
    </div>
  );
}

export default SnippetCard;

What does this code do?

The SyntaxHighlighter component from react-syntax-highlighter takes your raw code string and applies colored syntax highlighting based on the language prop. The oneDark style gives it that polished dark-editor look.

The component conditionally renders the description and tags only when they exist. This prevents empty elements from cluttering the card.

Create the SnippetList component

You need a component that takes an array of snippets and renders a SnippetCard for each one. It also handles the empty state when no snippets exist yet.

  • Create a new file called SnippetList.jsx inside client/src/components/ and add the following code:
import SnippetCard from './SnippetCard';

// Displays the full list of snippets or an empty-state message
function SnippetList({ snippets, onDelete }) {
  if (snippets.length === 0) {
    return <p className="empty-message">No snippets yet. Add your first one!</p>;
  }

  return (
    <div className="snippet-list">
      {snippets.map((snippet) => (
        <SnippetCard key={snippet._id} snippet={snippet} onDelete={onDelete} />
      ))}
    </div>
  );
}

export default SnippetList;

What does this code do?

SnippetList maps over the snippets array and renders one SnippetCard per entry. If the array is empty, it shows a friendly message instead of a blank page.

The key={snippet._id} prop helps React track which items change when the list updates later.

  • Confirm both component files exist by running this in the client/src/components/ directory:
ls src/components/

You should see SnippetCard.jsx and SnippetList.jsx listed in the output.

Don't see both files?

Make sure you created the components folder inside client/src/, not inside client/ or the project root. Check that each file has the .jsx extension.

Help me find where my component files should go.

Wire up App.jsx with sample data and styles

Your components are ready but nothing imports them yet. You will replace the default Vite content in App.jsx with sample snippet data and swap the stylesheet so the app renders with a dark theme.

  • Open client/src/App.css in VS Code and delete all existing content. Replace it with the global and layout styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #1a1a2e;
  color: #e0e0e0;
  min-height: 100vh;
}

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

.app-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
}

.app-header h1 {
  font-size: 1.8rem;
  color: #ffffff;
}
  • Directly below in the same file, add the button and snippet list styles:
.add-button {
  background-color: #6c63ff;
  color: white;
  border: none;
  padding: 0.6rem 1.2rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
}

.add-button:hover {
  background-color: #5a52d5;
}

.empty-message {
  text-align: center;
  color: #888;
  margin-top: 4rem;
  font-size: 1.1rem;
}

.snippet-list {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.snippet-card {
  background-color: #16213e;
  border-radius: 10px;
  padding: 1.5rem;
  border: 1px solid #2a2a4a;
}
  • Continue in the same file. Add the snippet card header and badge styles:
.snippet-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.8rem;
}

.snippet-header h3 {
  color: #ffffff;
  font-size: 1.1rem;
}

.snippet-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.language-badge {
  background-color: #6c63ff;
  color: white;
  padding: 0.2rem 0.6rem;
  border-radius: 4px;
  font-size: 0.75rem;
  text-transform: uppercase;
}

.delete-button {
  background-color: transparent;
  color: #ff6b6b;
  border: 1px solid #ff6b6b;
  padding: 0.2rem 0.6rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.75rem;
}
  • Continue in the same file. Add the remaining card detail and tag styles:
.delete-button:hover {
  background-color: #ff6b6b;
  color: white;
}

.snippet-description {
  color: #aaa;
  font-size: 0.9rem;
  margin-bottom: 0.8rem;
}

.snippet-tags {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.8rem;
  flex-wrap: wrap;
}

.tag {
  background-color: #2a2a4a;
  color: #a0a0c0;
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  font-size: 0.75rem;
}
  • Finally, add the form styles at the bottom of App.css. These will be used in a later step:
.add-snippet-form {
  background-color: #16213e;
  padding: 1.5rem;
  border-radius: 10px;
  margin-bottom: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  border: 1px solid #2a2a4a;
}

.add-snippet-form input,
.add-snippet-form select,
.add-snippet-form textarea {
  background-color: #1a1a2e;
  border: 1px solid #2a2a4a;
  color: #e0e0e0;
  padding: 0.7rem;
  border-radius: 6px;
  font-size: 0.9rem;
}

.add-snippet-form textarea {
  font-family: 'Courier New', monospace;
  resize: vertical;
}

.add-snippet-form button {
  background-color: #6c63ff;
  color: white;
  border: none;
  padding: 0.7rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
}
  • Add the final hover state at the very end of App.css:
.add-snippet-form button:hover {
  background-color: #5a52d5;
}
  • Save App.css.

Why so much CSS upfront?

You are adding all the app styles now so you never have to touch this file again. Every component you build in later steps will already have its styles waiting.

  • Now open client/src/App.jsx and delete all existing content. Replace it with the imports and sample data:
import { useState, useEffect } from 'react';
import SnippetList from './components/SnippetList';
import './App.css';

// API URL with fallback for local development
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function App() {
  // Hardcoded sample snippets to test the UI
  const [snippets, setSnippets] = useState([
    {
      _id: '1',
      title: 'Array Map Example',
      language: 'javascript',
      description: 'Transform array elements',
      code: 'const doubled = [1,2,3].map(n => n * 2);',
      tags: ['array', 'functional']
    },
  • Directly below in the same file, add the second sample snippet and the component's return:
    {
      _id: '2',
      title: 'List Comprehension',
      language: 'python',
      description: 'Python list comprehension',
      code: 'doubled = [n * 2 for n in [1, 2, 3]]',
      tags: ['list', 'comprehension']
    }
  ]);

  return (
    <div className="app">
      <header className="app-header">
        <h1>Code Snippet Vault</h1>
      </header>
      <SnippetList snippets={snippets} onDelete={() => {}} />
    </div>
  );
}

export default App;

What does this code do?

The useState hook holds an array of two hardcoded snippet objects. Each has an _id, title, language, code, and tags field matching the shape that MongoDB will return later.

The API_URL constant reads from a Vite environment variable. For now it falls back to http://localhost:5000 since you have not deployed yet. You will use this in a later step when you connect to the real API.

  • Save App.jsx.
  • Before you check the browser, predict: do you think you will see two snippet cards, or an error?
  • Switch to your terminal in the client folder. If the Vite dev server is not still running from Step 1, start it by running:
npm run dev

Open http://localhost:5173 in your browser. You should see two snippet cards on a dark background. One shows a JavaScript Array Map Example with colored syntax, and the other shows a Python List Comprehension. Each card displays a purple language badge and tag pills at the bottom.

Seeing a blank page or errors?

  • Open your browser's DevTools console (F12). If you see a module import error mentioning SnippetList or SnippetCard, double-check that your files are in client/src/components/ and that the filenames match exactly.
  • If the syntax highlighting is missing (code appears as plain text), make sure the import path is react-syntax-highlighter/dist/esm/styles/prism with no typos.
  • If styles are not applied (white background, no card layout), confirm you saved App.css and that App.jsx imports './App.css'.

Help me debug why my snippet cards are not rendering.

✔️ Awesome, I've got everything!

Great. Double check you have saved both App.jsx and App.css, and that your browser shows two styled snippet cards.

ⓧ I'd like to double check the full code

Your project has four files that changed in this step. Scroll up and confirm each code block is present in the correct file, in order:

  • client/src/components/SnippetCard.jsx contains the imports, the component with the header section, and the content section ending with export default SnippetCard.
  • client/src/components/SnippetList.jsx contains the import, the function with the empty check and the map, and the export.
  • client/src/App.css contains all six CSS sections pasted in order (global, buttons, card header, card details, form container, form hover).
  • client/src/App.jsx contains the imports, API_URL, the useState with two sample snippets, and the return rendering SnippetList.

Your UI is alive. You have two beautiful snippet cards rendering with professional syntax highlighting and a dark theme. Next up, you will build the Express API and connect it to MongoDB so your snippets can persist beyond a page refresh.

Build the API and Connect

Your React UI looks great with those syntax-highlighted cards. But the snippet data is hardcoded inside your component. If you refresh the page tomorrow, nothing will have changed. Real applications need a backend API and a database to persist data between sessions.

In this step, you will build an Express REST API backed by MongoDB, then swap the hardcoded snippets for a real fetch call. Along the way, you will discover a security mechanism that browsers enforce on cross-origin requests.

In this step, get ready to:
  • Build an Express server with Mongoose schema and REST routes.
  • Test the API standalone and confirm it connects to MongoDB Atlas.
  • Connect the React frontend to the live API and observe what happens.
Build the Express server

The server needs to do four things: accept JSON requests, connect to MongoDB, define what a snippet looks like, and listen for incoming traffic.

  • Open server/server.js in VS Code and add the following foundation code:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

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

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

// Connect to MongoDB using the URI from environment variables
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.error('MongoDB connection error:', err));

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

const Snippet = mongoose.model('Snippet', snippetSchema);

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

What does this code do?

This sets up the entire server skeleton. express.json() lets the server understand JSON bodies in requests. mongoose.connect opens a connection to your cloud database using a URI you will provide in a moment.

The schema defines the exact fields a snippet document can have. Mongoose uses this schema to validate data before saving it to MongoDB.

Before you can start the server, it needs a connection string to reach your MongoDB Atlas cluster.

  • Create a new file called .env inside your server folder.
  • Paste the following line, replacing the placeholder with the connection string you copied from MongoDB Atlas:
MONGODB_URI=mongodb+srv://your-username:your-password@cluster0.xxxxx.mongodb.net/snippet-vault?retryWrites=true&w=majority

Where do I find my connection string?

In your MongoDB Atlas dashboard, click **Connect** on your cluster, choose **Drivers**, and copy the connection string. Replace <password> with the database user password you created during setup.

  • Start the server to confirm it connects to MongoDB by running this command from your server folder:
node server.js

You should see two lines printed in your terminal:

Server running on port 5000
Connected to MongoDB

Seeing a connection error?

  • Double-check the username, password, and cluster hostname in your .env file. A single typo will cause the connection to fail.
  • Make sure your IP address is whitelisted in MongoDB Atlas under **Network Access**. If you are unsure, allow access from anywhere (0.0.0.0/0) for development.
  • Confirm you are running the command from inside the server folder, not the project root.

Need more help? Why can't my Express server connect to MongoDB Atlas?

  • Stop the server by pressing Ctrl+C in your terminal.

Now you will add the API routes that let clients create, read, and delete snippets.

  • In server/server.js, find the comment // Start the server and add these GET routes directly above it:
// GET all snippets, newest first
app.get('/api/snippets', async (req, res) => {
  try {
    const snippets = await Snippet.find().sort({ createdAt: -1 });
    res.json(snippets);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

// GET a single snippet by ID
app.get('/api/snippets/:id', async (req, res) => {
  try {
    const snippet = await Snippet.findById(req.params.id);
    if (!snippet) return res.status(404).json({ message: 'Snippet not found' });
    res.json(snippet);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

What do these routes do?

The first route responds to GET /api/snippets by querying all documents in the Snippet collection, sorted by newest first. The second responds to GET /api/snippets/:id and returns a single snippet matching the provided ID.

Both routes wrap the database call in a try/catch so that any unexpected database error returns a proper 500 response instead of crashing the server.

  • Still above the // Start the server comment, add the POST and DELETE routes below the GET routes you just added:
// POST a new snippet
app.post('/api/snippets', async (req, res) => {
  try {
    const snippet = new Snippet(req.body);
    const saved = await snippet.save();
    res.status(201).json(saved);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

// DELETE a snippet by ID
app.delete('/api/snippets/:id', async (req, res) => {
  try {
    const snippet = await Snippet.findByIdAndDelete(req.params.id);
    if (!snippet) return res.status(404).json({ message: 'Snippet not found' });
    res.json({ message: 'Snippet deleted' });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

What do these routes do?

The POST route creates a new Snippet document from the request body, validates it against your schema, and saves it to MongoDB. If a required field is missing, Mongoose rejects it with a 400 error.

The DELETE route finds a snippet by ID and removes it from the database in a single operation using findByIdAndDelete. If the ID does not match any document, it returns a 404.

  • Save server/server.js.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

Your server/server.js should have these sections in this exact order. Scroll through your file and confirm each is present:

  • Imports (express, mongoose, cors) and app setup with express.json().
  • mongoose.connect call with .then() and .catch().
  • snippetSchema definition and Snippet model.
  • GET /api/snippets and GET /api/snippets/:id routes.
  • POST /api/snippets and DELETE /api/snippets/:id routes.
  • app.listen at the very bottom.
Test the API standalone
  • Start the server again by running:
node server.js

You should see the same two success messages as before.

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

You should see an empty JSON array: []. This confirms your API is running, connected to MongoDB, and responding to requests. The array is empty because you have not created any snippets yet.

Seeing an error in the browser?

  • If the page says it cannot connect, confirm the terminal still shows "Server running on port 5000". The server may have crashed due to a syntax error.
  • If you see a JSON error message rather than [], check that your MongoDB URI is correct in the .env file.

Need more help? My Express API is not returning the expected response at /api/snippets.

Connect React to the API

Your API works in isolation. Now you will point the React frontend at it so the app loads real data instead of the hardcoded sample snippets.

  • In client/src/App.jsx, find the useState call that contains the hardcoded snippet array.
  • Find this line:
  const [snippets, setSnippets] = useState([
    {
      _id: '1',
      title: 'Array Map Example',
  • Delete the entire useState([...]) block (from const [snippets all the way to the closing ]);) and replace it with the following code:
  const [snippets, setSnippets] = useState([]);

  // Fetch snippets from the API
  const fetchSnippets = async () => {
    try {
      const response = await fetch(`${API_URL}/api/snippets`);
      const data = await response.json();
      setSnippets(data);
    } catch (error) {
      console.error('Error fetching snippets:', error);
    }
  };

  // Load snippets when the component first renders
  useEffect(() => {
    fetchSnippets();
  }, []);

What does this code do?

useState([]) starts with an empty array instead of fake data. The fetchSnippets function calls your Express API and stores whatever comes back into state.

useEffect with an empty dependency array [] runs the fetch exactly once, when the component first mounts. This is the standard React pattern for loading data on page load.

  • Save client/src/App.jsx.

Before you check the browser, what do you think will happen? The React app on port 5173 is making a request to the Express server on port 5000. Will the browser allow it?

  • Open your browser at http://localhost:5173 (your Vite dev server should still be running from the previous step).
  • Open the browser's developer console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).

You will see a red error in the console that says Access to fetch has been blocked by CORS policy. The page shows the empty-state message instead of your snippets.

What is CORS?

CORS stands for Cross-Origin Resource Sharing. Browsers block requests between different origins (like port 5173 to port 5000) unless the server explicitly says "I allow this." Your Express server has not done that yet.

This is a real security feature every full-stack developer encounters. In the next step, you will add one line of middleware to fix it.

Not seeing the CORS error?

  • Make sure both servers are running simultaneously: the Vite dev server on port 5173 and the Express server on port 5000. You need two separate terminal windows.
  • Make sure you saved App.jsx after removing the hardcoded data. Vite auto-refreshes on save.
  • Check that the console tab is open in DevTools, not the Network tab.

Need more help? Why am I not seeing a CORS error when my React app fetches from localhost:5000?

Your API is working perfectly on its own, and your React app is correctly trying to fetch from it. The only problem is that the browser is blocking the request. In the next step, you will fix this with a single line of code and then build the form to create real snippets.

Fix CORS and Complete CRUD

In the last step, you connected your React frontend to the Express API and hit a wall. The browser blocked the request because your frontend (port 5173) and backend (port 5000) are on different origins.

This is a CORS error, and every full-stack developer encounters it. A one-line middleware fix unlocks the connection, and then you can build the form that lets you create and delete real snippets stored in MongoDB.

In this step, get ready to:
  • Fix the CORS error so your React app can talk to your Express API.
  • Build a form component to create new code snippets.
  • Wire up delete functionality so snippets can be removed.
Add CORS middleware

Your browser blocked the fetch request because the Express server does not include an Access-Control-Allow-Origin header in its responses. The cors package (already installed) adds this header automatically when you activate it as middleware.

  • In server/server.js, find this line:
app.use(express.json());
  • Add app.use(cors()); directly above it, so the two lines look like this:
app.use(cors());
app.use(express.json());

Why does order matter?

Express runs middleware in the order you define it. Placing cors() before express.json() ensures the CORS headers are set on every response, including error responses, before any route logic runs.

  • Save server/server.js.
  • Stop the Express server in your terminal with Ctrl+C, then restart it by running:
node server.js

Before you check the browser, what do you expect to see now that CORS is enabled?

  • Switch back to your browser where the React app is running at http://localhost:5173 and refresh the page.

The CORS error is gone. You should see the empty-state message "No snippets yet. Add your first one!" instead of a console error.

Still seeing the CORS error?

  • Make sure app.use(cors()) appears before your route definitions, not after them.
  • Confirm you restarted the Express server after saving. Changes to Node.js files require a restart.
  • Check that the cors package is listed in server/package.json under dependencies.

Why is my React app still showing a CORS error after adding the middleware?

Build the AddSnippetForm component

With the connection working, you need a way to create snippets. You will build a form component that collects a title, language, code, description, and tags, then POSTs the data to your API.

  • Create a new file AddSnippetForm.jsx inside client/src/components/ and add the form logic by pasting this code:
import { useState } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function AddSnippetForm({ onSnippetAdded }) {
  const [title, setTitle] = useState('');
  const [code, setCode] = useState('');
  const [language, setLanguage] = useState('javascript');
  const [tags, setTags] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();

    const newSnippet = {
      title,
      code,
      language,
      tags: tags.split(',').map(tag => tag.trim()).filter(tag => tag),
      description
    };

    try {
      const response = await fetch(`${API_URL}/api/snippets`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newSnippet)
      });

      if (response.ok) {
        onSnippetAdded();
      }
    } catch (error) {
      console.error('Error adding snippet:', error);
    }
  };

What does this code do?

  • Five useState hooks track each form field independently.
  • The tags string gets split on commas, trimmed, and filtered to remove empty entries before sending.
  • On successful POST, the onSnippetAdded callback tells the parent component to refresh the snippet list.
  • Now add the JSX return and export directly below the handleSubmit function in the same file:
  return (
    <form className="add-snippet-form" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Snippet title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        required
      />
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="javascript">JavaScript</option>
        <option value="python">Python</option>
        <option value="html">HTML</option>
        <option value="css">CSS</option>
        <option value="java">Java</option>
        <option value="cpp">C++</option>
        <option value="typescript">TypeScript</option>
      </select>
      <textarea
        placeholder="Paste your code here..."
        value={code}
        onChange={(e) => setCode(e.target.value)}
        required
        rows={8}
      />

What are these form elements?

  • The <select> dropdown gives the learner a fixed list of languages that matches what react-syntax-highlighter supports.
  • The <textarea> with rows={8} gives enough vertical space to paste multi-line code.
  • Each input's value and onChange create a controlled component pattern so React always knows the current field value.
  • Still in the same file, add the remaining form fields and close out the component directly below the <textarea>:
      <input
        type="text"
        placeholder="Description (optional)"
        value={description}
        onChange={(e) => setDescription(e.target.value)}
      />
      <input
        type="text"
        placeholder="Tags (comma-separated, e.g. react, hooks, state)"
        value={tags}
        onChange={(e) => setTags(e.target.value)}
      />
      <button type="submit">Save Snippet</button>
    </form>
  );
}

export default AddSnippetForm;
  • Save AddSnippetForm.jsx.

Seeing a syntax error in your editor?

  • Make sure the three code chunks are pasted in order with no gaps. The file should start with import { useState } and end with export default AddSnippetForm;.
  • Check that you have matching curly braces for the handleSubmit function and the AddSnippetForm function.

Help me fix a syntax error in my AddSnippetForm component.

✔️ Awesome, I've got everything!

Great. Make sure you have saved AddSnippetForm.jsx before moving on.

ⓧ I'd like to double check the full code

Your AddSnippetForm.jsx should contain three sections in order: the logic chunk (imports through handleSubmit), the select/textarea chunk, and the description/tags/button chunk. Scroll up and confirm each piece is present with no duplicated or missing lines.

Add delete functionality and toggle the form

The form component exists but nothing renders it yet. You need to update App.jsx to import the form, add a toggle button, and wire up the delete handler so SnippetCard's Delete button actually works.

  • In client/src/App.jsx, find this import line:
import SnippetList from './components/SnippetList';
  • Add the AddSnippetForm import directly below it, so the two lines look like this:
import SnippetList from './components/SnippetList';
import AddSnippetForm from './components/AddSnippetForm';
  • Next, find this line inside the App function:
  const [snippets, setSnippets] = useState([]);
  • Add a new state variable directly below it:
  const [showForm, setShowForm] = useState(false);
  • Now add the handleSnippetAdded and handleDelete functions. Paste them directly below the useEffect block:
  const handleSnippetAdded = () => {
    fetchSnippets();
    setShowForm(false);
  };

  const handleDelete = async (id) => {
    try {
      await fetch(`${API_URL}/api/snippets/${id}`, { method: 'DELETE' });
      fetchSnippets();
    } catch (error) {
      console.error('Error deleting snippet:', error);
    }
  };

What do these handlers do?

  • handleSnippetAdded refreshes the list after a successful POST and hides the form.
  • handleDelete sends a DELETE request to the API, then refreshes the list so the removed snippet disappears immediately.
  • Finally, replace the entire return statement in App.jsx with this updated JSX:
  return (
    <div className="app">
      <header className="app-header">
        <h1>Code Snippet Vault</h1>
        <button
          className="add-button"
          onClick={() => setShowForm(!showForm)}
        >
          {showForm ? 'Cancel' : '+ Add Snippet'}
        </button>
      </header>
      {showForm && <AddSnippetForm onSnippetAdded={handleSnippetAdded} />}
      <SnippetList snippets={snippets} onDelete={handleDelete} />
    </div>
  );

What changed in the JSX?

  • A toggle button in the header switches between "+ Add Snippet" and "Cancel" based on the showForm state.
  • The && conditional renders AddSnippetForm only when the learner clicks the button.
  • handleDelete is now passed as the onDelete prop to SnippetList (replacing the empty function from before).
  • Save App.jsx.

Before you test, what do you think will happen when you click "+ Add Snippet", fill in the form, and click Save?

  • Switch back to your browser at http://localhost:5173 and refresh the page.
  • Click the + Add Snippet button in the header.
  • Enter Hello World as the title.
  • Select JavaScript from the language dropdown.
  • Paste console.log('Hello World'); in the code textarea.
  • Click Save Snippet.

The form disappears and your new snippet appears in the list with syntax highlighting. Refresh the page. The snippet is still there because it is stored in MongoDB.

  • Click the Delete button on your snippet. It disappears immediately.

Snippet not appearing after Save?

  • Open the browser console and check for network errors. A 400 error means a required field (title, code, or language) was missing.
  • Verify your Express server is still running in the other terminal window. If it crashed, restart it with node server.js.
  • Make sure API_URL in AddSnippetForm.jsx matches http://localhost:5000 (or your VITE_API_URL environment variable).

Help me debug why my snippet is not saving to the database.

✔️ Awesome, I've got everything!

Great. Make sure both App.jsx and AddSnippetForm.jsx are saved before continuing.

ⓧ I'd like to double check the full code

Here is the complete App.jsx at this point:

import { useState, useEffect } from 'react';
import SnippetList from './components/SnippetList';
import AddSnippetForm from './components/AddSnippetForm';
import './App.css';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function App() {
  const [snippets, setSnippets] = useState([]);
  const [showForm, setShowForm] = useState(false);

  const fetchSnippets = async () => {
    try {
      const response = await fetch(`${API_URL}/api/snippets`);
      const data = await response.json();
      setSnippets(data);
    } catch (error) {
      console.error('Error fetching snippets:', error);
    }
  };

  useEffect(() => {
    fetchSnippets();
  }, []);
  const handleSnippetAdded = () => {
    fetchSnippets();
    setShowForm(false);
  };

  const handleDelete = async (id) => {
    try {
      await fetch(`${API_URL}/api/snippets/${id}`, { method: 'DELETE' });
      fetchSnippets();
    } catch (error) {
      console.error('Error deleting snippet:', error);
    }
  };

  return (
    <div className="app">
      <header className="app-header">
        <h1>Code Snippet Vault</h1>
        <button
          className="add-button"
          onClick={() => setShowForm(!showForm)}
        >
          {showForm ? 'Cancel' : '+ Add Snippet'}
        </button>
      </header>
      {showForm && <AddSnippetForm onSnippetAdded={handleSnippetAdded} />}
      <SnippetList snippets={snippets} onDelete={handleDelete} />
    </div>
  );
}

export default App;

And here is the complete server/server.js with the CORS line added:

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

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

app.use(cors());
app.use(express.json());

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

Full CRUD is working. You can create snippets, view them with syntax highlighting, and delete the ones you no longer need. Next up, you will deploy your app so anyone can access it from a live URL.

Deploy Your App

Your app works perfectly on your machine, but a portfolio project only proves your skills if someone else can click a link and see it live.

Deploying the frontend to Vercel and the backend to Render gives you a permanent public URL at zero cost. In this step, you'll push your code to GitHub and deploy both services with free-tier hosting.

In this step, get ready to:
  • Push your code to a GitHub repository.
  • Deploy the Express backend to Render.
  • Deploy the React frontend to Vercel.
Push your code to GitHub

Before either hosting service can deploy your app, they need access to your source code. Git tracks your changes and GitHub stores them in the cloud where Render and Vercel can pull from.

  • Make sure your terminal is in the project root folder (the one that contains both client/ and server/).
  • Create a .gitignore file in the project root by running this command:
printf 'node_modules/\n.env\n' > .gitignore

What does this file do?

The .gitignore file tells Git which files to skip when committing. You never want to push node_modules/ (thousands of dependency files) or .env (your secret database password) to a public repository.

  • Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit"

You should see output listing all the files being tracked, ending with a commit message confirmation.

  • Open GitHub in your browser and create a new repository.
  • Name it snippet-vault (or any name you prefer).
  • Leave it set to Public and do not add a README or .gitignore (you already have one).
  • Click Create repository.
  • Copy the HTTPS URL GitHub shows you, then push your code by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/snippet-vault.git"]]
git branch -M main
git push -u origin main

Refresh your GitHub repository page in the browser. You should see your client/ and server/ folders listed with all your project files.

Push rejected or authentication failed?

If Git asks for a password and rejects it, GitHub no longer accepts password authentication. You need a Personal Access Token. Go to GitHub Settings then Developer settings then Personal access tokens to generate one. Use the token as your password when prompted.

Help me set up a GitHub personal access token

Deploy the backend to Render

Render will pull your code from GitHub, install dependencies, and run your Express server on a public URL. The free tier costs nothing and wakes up automatically when someone visits your API.

  • Open https://dashboard.render.com/register and sign up with your GitHub account.
  • In the Render Dashboard, click + New in the top-right corner.
  • Select Web Service.
  • Connect your GitHub account if prompted, then find and select your snippet-vault repository.
  • Click Connect.

The service creation form appears. Fill in these fields:

  • Set Name to snippet-vault-api (this becomes part of your public URL).
  • Set Root Directory to server.
  • Set Build Command to npm install.
  • Set Start Command to node server.js.
  • Scroll down and choose the Free instance type.
  • Expand the Advanced section and click Add Environment Variable.
  • Set the key to MONGODB_URI and paste your MongoDB Atlas connection string as the value (the same one from your local .env file).
  • Click Create Web Service.

Render begins building and deploying your server. This takes one to two minutes.

Why set the Root Directory?

Your GitHub repo contains both client/ and server/. By setting the root directory to server, Render only looks at the backend code when it runs npm install and node server.js.

  • Wait for the deploy status to show Live in the Render dashboard.
  • Copy the URL Render assigned to your service (it looks like https://snippet-vault-api-xxxx.onrender.com).
  • Open that URL in your browser with /api/snippets appended to the end.

You should see an empty JSON array [] in the browser. Your API is live on the internet.

First request is slow

Render's free tier spins down after 15 minutes of no traffic. The first request after inactivity takes about one minute to wake up. Subsequent requests are fast.

Seeing a deploy failed error?

Check the Logs tab in Render. The most common issue is a missing or incorrect MONGODB_URI environment variable. Make sure you copied the full connection string including the password.

Help me diagnose my Render deployment failure

Deploy the frontend to Vercel

Vercel hosts your React frontend and auto-detects Vite projects. You need to add one small config file first so that direct URL access works correctly with client-side routing.

  • In your terminal, create the Vercel config file by running this command:
printf '{\n  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]\n}\n' > client/vercel.json

What does this rewrite rule do?

Single-page apps handle routing in the browser. Without this rule, visiting any path other than / directly would return a 404 from Vercel's server. The rewrite sends all requests to index.html so React can handle the route.

  • Commit and push this new file to GitHub by running these commands:
git add .
git commit -m "Add vercel.json for SPA routing"
git push

Now your GitHub repo has everything Vercel needs.

  • Open https://vercel.com and sign up with your GitHub account.
  • On the Vercel dashboard, click Add New... and select Project.
  • Find your snippet-vault repository in the list and click Import.

The project configuration page appears. Fill in these settings:

  • Set Root Directory to client.
  • Expand Environment Variables.
  • Add a variable with the key VITE_API_URL and set the value to your Render backend URL (for example https://snippet-vault-api-xxxx.onrender.com). Do not include a trailing slash.
  • Click Deploy.

Why VITE_API_URL?

Vite only exposes environment variables that start with VITE_ to your client-side code. Your App.jsx already reads import.meta.env.VITE_API_URL to know where the API lives. Setting this on Vercel means your deployed frontend talks to your deployed backend instead of localhost.

Vercel auto-detects Vite and builds your project. This takes about one minute.

Before you open the URL, think about what you expect to see. Will the app load with an empty snippet list, or will it show the snippets you created during local development?

  • Once the deploy finishes, click the Visit button (or open your .vercel.app URL in a new incognito window).
  • Click + Add Snippet and create a test snippet with any title, language, and code.
  • Click Save Snippet.
  • Refresh the page.

Your snippet is still there after refresh. The data persists because it lives in MongoDB Atlas, not in your browser. Anyone with this URL can see your app and the snippets you've saved.

Snippets not loading on the deployed site?

Open the browser's developer console (F12) and check for network errors. The most common issue is a missing or incorrect VITE_API_URL environment variable. Make sure the value matches your Render URL exactly and does not have a trailing slash.

If you see a CORS error, confirm your Render backend is running (visit the Render URL directly). The first request wakes it up after inactivity.

Help me debug the connection between my frontend and backend

Your full-stack app is now live on the internet with a shareable URL. Next up, a bonus challenge to make your snippet vault even more useful.

Secret mission

Add a Language Filter

Your vault is filling up with snippets in different languages. What if you could filter to show only JavaScript, only Python, or only CSS? In this mission, you'll add server-side filtering with query parameters and a dropdown that narrows results instantly.

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 only free-tier services, so there are no ongoing costs regardless of what you choose.

Resources you used:

  • MongoDB Atlas free cluster (stores your snippets in the cloud).
  • Render free web service (hosts your Express API).
  • Vercel Hobby deployment (hosts your React frontend).
  • GitHub repository (stores your source code).
  • Local project files on your Desktop (the client and server folders).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your live URL available.

  • All three services (MongoDB Atlas, Render, Vercel) are on free tiers with no credit card required.
  • Render will spin down your backend after 15 minutes of no inbound traffic. It spins back up automatically on the next request (takes about one minute).
  • MongoDB Atlas auto-pauses your free cluster after 30 days of zero connections. Your data persists and the cluster resumes when you reconnect.
  • Vercel keeps your frontend deployed indefinitely on the Hobby plan at no cost.

Pause - I'll come back to this later

No manual action needed. All services pause themselves automatically when idle.

  • Render spins down your backend after 15 minutes of no traffic. It wakes up on the next request.
  • MongoDB Atlas auto-pauses after 30 days of zero connections. Resume it manually from the Atlas dashboard when you are ready to continue.
  • Vercel has no pause state. Your frontend stays deployed but costs nothing on the Hobby plan.
  • Your local files remain on your Desktop until you delete them yourself.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

Delete the Render web service
  • Open the Render Dashboard at dashboard.render.com.
  • Click your web service to open its details.
  • Click Settings in the left sidebar.
  • Scroll to the bottom and click Delete Web Service.
  • Type the service name to confirm, then click Delete.
Delete the Vercel project
  • Open the Vercel dashboard at vercel.com/dashboard.
  • Select your project.
  • Click Settings in the sidebar.
  • Scroll to the bottom of the General page and click Delete in the Delete Project section.
  • Enter the project name to confirm, then click Continue.
Terminate the MongoDB Atlas cluster
  • Open the Atlas dashboard at cloud.mongodb.com.
  • Go to the Clusters page for your project (click Database in the sidebar).
  • Click the ... menu next to your cluster.
  • Click Terminate.
  • Type the cluster name to confirm, then click Terminate.
Delete the GitHub repository
  • Open your repository on GitHub.
  • Click Settings (under the repository name).
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Confirm by typing the repository name, then click Delete this repository.
Delete local project files
  • Delete the project folder from your Desktop by running these commands in your terminal:

macOS

cd ~/Desktop
rm -rf snippet-vault

Windows

cd ~\Desktop
rmdir /s /q snippet-vault

That's a Wrap!

That's a Wrap!

Nice work! You just built and deployed a full-stack MERN application from scratch.

You've learned how to:

  • Build a React frontend from scratch using Vite, with reusable components, props, useState, and useEffect hooks.
  • Create an Express.js REST API with full CRUD routes backed by a Mongoose schema and MongoDB Atlas for persistent cloud storage.
  • Discover and fix a real-world CORS issue, then deploy the full-stack app to Vercel and Render with a shareable live URL.
  • Secret Mission: Added server-side filtering with MongoDB query parameters so users can view snippets by language.

Ready to quiz yourself?