Build a Full-Stack Bookmark App

Build a React + Express app to save, view, and delete bookmarks.

Introduction

30 Second Summary

Think about how many browser tabs you have open right now with links you want to save "for later." Bookmarks get buried, and sticky notes pile up.

In this project, you will build a Link Vault. It is a personal bookmark manager that lets you save, view, and delete links through a clean web interface powered by a React frontend and an Express backend.

What You'll Build

A full-stack web application with a React frontend and an Express backend that communicate over HTTP.

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

  • A running Express API with three endpoints that store and serve your bookmarks.
  • A React app that displays your saved links, lets you add new ones, and delete existing ones.
  • Both pieces running simultaneously on your machine, communicating via REST API calls.
  • Secret Mission: Add a real-time search feature that filters your links as you type.

Are there any prerequisites?

You need basic programming knowledge (variables, functions, loops, conditionals). No web development experience is required.

You should have VS Code installed and a GitHub account ready. Experience with the Terminal is helpful but not essential.

Install Node.js

Your Link Vault has two pieces: a backend server and a frontend app. Both are built with JavaScript, and both need Node.js to run.

In this step, you will install Node.js v24 LTS on your machine and confirm it is working. Node.js also ships with npm (Node Package Manager), which you will use later to install libraries like Express and React.

In this step, get ready to:
  • Install Node.js v24 LTS on your machine.
  • Verify Node.js and npm are working in Terminal.
Check your Node.js version

You may already have Node.js from a previous project. Let's check before installing anything new.

  • Open a Terminal window by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
  • Check your Node.js version by running this command:
node --version

✔️ I see version 24 or higher

You should see something like v24.16.0 (or any v24.x.x number). Node.js v24 LTS is ready to go. Skip ahead to the next substep to verify npm.

ⓧ I see an older version

If you see a version number lower than 24 (like v20.x.x or v22.x.x), you need to upgrade. The simplest way is to download the official installer, which replaces your current version.

  • Open https://nodejs.org/en/download in your browser.
  • Click the macOS tab and download the .pkg installer for Node.js v24 LTS.
  • Run the downloaded installer and follow the prompts. It will replace your older version.
  • Close and reopen your Terminal window so the new version takes effect.
  • Run node --version again. You should now see v24.16.0 or higher.

Already use Homebrew?

If you manage packages with Homebrew, you can install Node.js v24 LTS by running:

brew install node@24

This formula is keg-only, so you may need to follow the on-screen instructions to add it to your PATH.

ⓧ Command not found

If you see a message like "command not found", Node.js is not installed yet. Let's install it now.

  • Open https://nodejs.org/en/download in your browser.
  • Click the macOS tab and download the .pkg installer for Node.js v24 LTS.
  • Run the downloaded installer and follow the prompts to complete the installation.
  • Close and reopen your Terminal window so the new installation is recognized.
  • Run node --version again. You should now see v24.16.0 or higher.

Already use Homebrew?

If you prefer Homebrew, you can install Node.js v24 LTS by running:

brew install node@24

This formula is keg-only, so you may need to follow the on-screen instructions to add it to your PATH.

What is Node.js?

Node.js is a JavaScript runtime that lets you run JavaScript outside of a web browser. Without it, JavaScript can only run inside browser tabs.

In this project, Node.js powers your Express backend server AND your React development server. One runtime, two jobs.

Verify npm is ready

Node.js ships with npm, the package manager you will use to install Express, React, and other libraries throughout this project.

  • Confirm npm is available by running this command:
npm --version

You should see a version number (like 11.x.x). Any version that appears means npm is working.

Not seeing a version number?

If npm --version returns "command not found", your Node.js installation may not have completed correctly. Try closing all Terminal windows and opening a fresh one. If the problem persists, re-run the Node.js installer from the previous substep.

help me troubleshoot why npm is not found after installing Node.js

Your development environment is ready. Next up, you will build your Express API and create the backend for your Link Vault.

Build Your Express API

Your tools are installed and your project folders are ready. Now it's time to build the piece that stores and serves your bookmarks.

Every web app needs a server that listens for requests and sends back data. You'll build a REST API using Express that can list all your saved links, add new ones, and delete ones you no longer need.

In this step, get ready to:
  • Initialize a Node.js project and install your dependencies.
  • Write an Express server with three API endpoints.
  • Test your API using a browser and curl commands.
Initialize your Node.js project

Before writing any server code, you need a package.json file that tells Node.js about your project and its dependencies.

  • Switch back to your terminal and navigate into the server folder by running this command:
cd server
  • Create the package.json file by running:
npm init -y

What does this command do?

The npm init -y command creates a package.json file with default values. This file is like a recipe card for your project. It lists the project name, version, and most importantly, what packages (libraries) your project depends on.

  • Install Express and the cors package by running:
npm install express cors
  • Confirm the installation succeeded by running:
ls node_modules

You should see a list of folders printed in the terminal, including express and cors.

What is CORS?

CORS stands for Cross-Origin Resource Sharing. Browsers block requests between different origins (different ports count as different origins) by default.

Your React app will run on one port and your Express server on another. The cors package tells the browser it's safe to allow requests between them.

Seeing permission errors?

If you see EACCES or permission denied errors, make sure you're running the command inside the server folder and not in a system directory. Run pwd to check your current location.

help me fix npm permission errors when installing packages

Create your Express server

Now you'll write the actual server code. This single file sets up your API and defines what happens when someone requests your data.

  • In VS Code, create a new file called index.js inside your server folder.
  • Right-click the server folder in the Explorer sidebar and select New File.
  • Type index.js and press Enter.

You'll build this file in three chunks. Start with the server setup and seed data.

  • Add the server setup and starter data by copying this code into index.js:
const express = require('express');
const cors = require('cors');

const app = express();
const PORT = 3001;

// Enable CORS so the React frontend can talk to this server
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());

// In-memory data store with two starter links
let links = [
  { id: 1, title: 'Express.js Docs', url: 'https://expressjs.com' },
  { id: 2, title: 'React Documentation', url: 'https://react.dev' },
];
let nextId = 3;

What does this code do?

  • require('express') and require('cors') load the packages you installed.
  • express() creates your app instance. Every route and setting gets attached to this.
  • app.use(cors()) and app.use(express.json()) are middleware. They run on every request before your route handlers.
  • The links array is your in-memory data store. It holds all saved bookmarks. The nextId counter tracks what ID to give the next new link.

You won't see a change yet. The next chunk adds the route that serves this data.

  • Add the GET and POST endpoints below your data store by copying this code at the bottom of index.js:
// GET /links - return all saved links
app.get('/links', (req, res) => {
  res.json(links);
});

// POST /links - add a new link
app.post('/links', (req, res) => {
  const { title, url } = req.body;

  if (!title || !url) {
    return res.status(400).json({ error: 'Title and URL are required' });
  }

  const newLink = { id: nextId, title, url };
  nextId++;
  links.push(newLink);
  res.status(201).json(newLink);
});

What does this code do?

  • app.get('/links', ...) handles GET requests. When someone visits /links, the server responds with the full array of bookmarks as JSON.
  • app.post('/links', ...) handles POST requests. It reads title and url from the request body, creates a new link object, and adds it to the array.
  • The if (!title || !url) check is validation. If someone sends a request without both fields, the server returns a 400 error instead of saving incomplete data.

You won't see a change yet. The next chunk finishes the server so you can start it up.

  • Add the DELETE endpoint and the server startup at the bottom of index.js:
// DELETE /links/:id - remove a link by its ID
app.delete('/links/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = links.findIndex(link => link.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Link not found' });
  }

  links.splice(index, 1);
  res.status(204).send();
});

// Start the server
app.listen(PORT, () => {
  console.log(`Link Vault API running at http://localhost:${PORT}`);
});

What does this code do?

  • app.delete('/links/:id', ...) handles DELETE requests. The :id part is a URL parameter. Express extracts it so you know which link to remove.
  • findIndex looks for a link with a matching ID. If none exists, the server responds with a 404 error.
  • splice(index, 1) removes the link from the array. The 204 status means "success, no content to return."
  • app.listen(PORT, ...) starts the server on port 3001 and prints a confirmation message.
  • Save index.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete index.js file. Make sure yours matches:

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

const app = express();
const PORT = 3001;

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

let links = [
  { id: 1, title: 'Express.js Docs', url: 'https://expressjs.com' },
  { id: 2, title: 'React Documentation', url: 'https://react.dev' },
];
let nextId = 3;

app.get('/links', (req, res) => {
  res.json(links);
});

app.post('/links', (req, res) => {
  const { title, url } = req.body;

  if (!title || !url) {
    return res.status(400).json({ error: 'Title and URL are required' });
  }

  const newLink = { id: nextId, title, url };
  nextId++;
  links.push(newLink);
  res.status(201).json(newLink);
});

app.delete('/links/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = links.findIndex(link => link.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Link not found' });
  }

  links.splice(index, 1);
  res.status(204).send();
});

app.listen(PORT, () => {
  console.log(`Link Vault API running at http://localhost:${PORT}`);
});
Test your API

Your server code is written. Time to start it up and make sure each endpoint responds correctly.

  • Start your Express server by running this command in the terminal (make sure you're still inside the server folder):
node index.js

You should see this message printed in the terminal:

Link Vault API running at http://localhost:3001

Seeing "Cannot find module" or "port already in use"?

  • If you see a "Cannot find module" error, make sure you ran npm install inside the server folder first.
  • If the port is already in use, another process is on port 3001. Press Ctrl+C to stop any previous server, then run node index.js again.

help me fix an error when starting my Express server with node index.js

  • Open your browser and visit:
http://localhost:3001/links

You should see a JSON array with two links displayed in the browser. This confirms your GET endpoint works.

  • Open a new terminal tab or window (keep the server running in the first one).
  • Test the POST endpoint by running this curl command to add a new link:
curl -X POST http://localhost:3001/links -H "Content-Type: application/json" -d '{"title":"GitHub","url":"https://github.com"}'

You should see a JSON response with your new link, including "id":3.

  • Test the DELETE endpoint by running this curl command to remove the link you just added:
curl -X DELETE http://localhost:3001/links/3
  • Refresh http://localhost:3001/links in your browser to confirm the link was deleted.

You should see only the two original links again. All three endpoints are working.

What are GET, POST, and DELETE?

These are HTTP methods. GET reads data, POST creates data, and DELETE removes data.

When you visit a URL in your browser, that's always a GET request. POST and DELETE require tools like curl or a frontend app to send.

Curl not returning what you expected?

  • Make sure the server is still running in your other terminal tab. If you closed it, run node index.js again from the server folder.
  • Check that the URL is http://localhost:3001/links (not https, and port 3001 not 3000).

help me troubleshoot curl requests to my local Express API

Your API is live and responding to requests. Next up, you'll build a React frontend that gives your users a real interface for interacting with these endpoints.

Build Your React Frontend

Your API is running and serving data at the endpoints you built. Now it's time to create the visual layer that users will actually interact with.

In this step, you'll scaffold a React app using Vite, then build out the UI components that display your links. The frontend won't connect to the backend yet. That comes in the next step.

In this step, get ready to:
  • Scaffold a React project in the client folder using Vite.
  • Build an App component with state management using useState.
  • Create a form and link list with delete buttons.
Scaffold a React app with Vite

Vite is a modern build tool that scaffolds project templates and runs a fast development server with hot reloading. You'll use it to set up the React frontend inside your client folder.

  • Open a new terminal tab or window. Your Express server is still running in the first terminal, so you need a second one for the frontend.

Why a second terminal?

Your Express API needs to keep running so the frontend can talk to it later. Each server occupies its own terminal process. You'll have two terminals open from now on: one for the backend, one for the frontend.

  • Navigate to your project's root folder by running this command:
cd ~/Desktop/link-vault
  • Scaffold a React app inside the client folder by running this command:
npm create vite@latest client -- --template react

What does this command do?

npm create vite@latest downloads and runs the Vite project scaffolder.

client tells Vite to put the files inside your existing client folder. The -- --template react part tells Vite to use the React template, skipping the interactive prompts.

  • Move into the client folder and install dependencies by running these commands:
cd client
npm install
  • Start the Vite development server by running:
npm run dev
  • Open your browser and go to http://localhost:5173. You should see the default Vite + React welcome page with a spinning React logo.

Don't see the welcome page?

  • Make sure you ran npm install before npm run dev. Without installing dependencies first, Vite cannot start.
  • Check that port 5173 is not already in use by another app. The terminal output will show which port Vite is using.
  • If you see an error about the directory not being empty, delete any hidden files inside client and try the scaffold command again.

Why isn't my Vite dev server starting?

Build the App component with state

React uses a concept called state to track data that can change over time. When state updates, React automatically re-renders the parts of your UI that depend on it.

  • In VS Code, open client/src/App.jsx.
  • Select all the existing code and replace it with the following:
import { useState, useEffect } from 'react';
import './App.css';

const API_URL = 'http://localhost:3001';

function App() {
  // State to hold the list of saved links
  const [links, setLinks] = useState([]);
  // State to hold the form input values
  const [title, setTitle] = useState('');
  const [url, setUrl] = useState('');

  return (
    <div className="app">
      <h1>Link Vault</h1>

      <form 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
        />
        <button type="submit">Add Link</button>
      </form>

      <ul className="link-list">
        {links.map(link => (
          <li key={link.id} className="link-item">
            <a href={link.url} target="_blank" rel="noopener noreferrer">
              {link.title}
            </a>
            <button className="delete-btn">
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

What does this code do?

  • useState([]) creates a links variable that starts as an empty array. The setLinks function is how you update that array later.
  • useState('') creates title and url state variables to track what the user types into the form inputs.
  • onChange={(e) => setTitle(e.target.value)} updates the state every time the user types a character. This keeps React in sync with the input field.
  • links.map() loops over the links array and renders a list item for each one. Since the array starts empty, nothing appears yet.

Why import useEffect and API_URL now?

You'll use both of these in the next step when you connect the frontend to the backend. Setting them up now means you won't need to restructure the file later.

  • Save App.jsx.
  • Check your browser. Vite automatically reloads the page. You should see a "Link Vault" heading and a form with two input fields and an "Add Link" button.

Page looks broken or shows an error?

  • Check the terminal for a syntax error. JSX is sensitive to unclosed tags. Make sure every <div> has a matching </div>.
  • Make sure the file is saved as App.jsx (not App.js). The .jsx extension tells Vite to process the JSX syntax.

Help me fix a syntax error in my React component.

Style the form and link list

The form and list structure are in place, but they need styling to look like a real application. You'll replace the default Vite CSS with styles for your Link Vault.

  • Open client/src/App.css in VS Code.
  • Select all the existing code and replace it with the following:
.app {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #1a1a1a;
}

.add-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 2rem;
}

.add-form input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 1rem;
}

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

.add-form button:hover {
  background-color: #4338ca;
}

.link-list {
  list-style: none;
  padding: 0;
}

.link-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  margin-bottom: 0.5rem;
}

.link-item a {
  color: #4f46e5;
  text-decoration: none;
}

.link-item a:hover {
  text-decoration: underline;
}

.delete-btn {
  padding: 0.25rem 0.5rem;
  background-color: #ef4444;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.875rem;
}

.delete-btn:hover {
  background-color: #dc2626;
}

What does this CSS do?

  • .app centers the entire page content with a max width of 600px.
  • .add-form uses flexbox to place the two inputs and button side by side in a row.
  • .link-item styles each link as a card-like row with the link title on the left and the delete button on the right.
  • .delete-btn is a small red button that changes shade on hover.
  • Save App.css.
  • Check your browser. You should see a clean centered layout with a "Link Vault" heading, a form row containing two text inputs and a purple "Add Link" button, and an empty area below where links will appear.

Styles not showing up?

  • Confirm that your App.jsx file has import './App.css' near the top. Without this import, React won't load the stylesheet.
  • Make sure the class names in your CSS match the className values in your JSX exactly. CSS class names are case-sensitive.

Why aren't my CSS styles appearing in the browser?

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here's the complete App.jsx:

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

const API_URL = 'http://localhost:3001';

function App() {
  const [links, setLinks] = useState([]);
  const [title, setTitle] = useState('');
  const [url, setUrl] = useState('');

  return (
    <div className="app">
      <h1>Link Vault</h1>

      <form 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
        />
        <button type="submit">Add Link</button>
      </form>

      <ul className="link-list">
        {links.map(link => (
          <li key={link.id} className="link-item">
            <a href={link.url} target="_blank" rel="noopener noreferrer">
              {link.title}
            </a>
            <button className="delete-btn">
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

And the complete App.css:

.app {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #1a1a1a;
}

.add-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 2rem;
}

.add-form input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 1rem;
}

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

.add-form button:hover {
  background-color: #4338ca;
}

.link-list {
  list-style: none;
  padding: 0;
}

.link-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  margin-bottom: 0.5rem;
}

.link-item a {
  color: #4f46e5;
  text-decoration: none;
}

.link-item a:hover {
  text-decoration: underline;
}

.delete-btn {
  padding: 0.25rem 0.5rem;
  background-color: #ef4444;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.875rem;
}

.delete-btn:hover {
  background-color: #dc2626;
}

Your frontend is running and looking good. Right now the form and buttons don't do anything because they aren't connected to your API. In the next step, you'll wire them up to send real HTTP requests to your Express server.

Connect Frontend to Backend

Your React UI looks great, but right now it's empty. The links array starts as [] and nothing ever fills it.

In this step, you'll use the fetch API to connect your React frontend to the Express backend you built earlier. Every action in the UI will trigger a real HTTP request to your API.

In this step, get ready to:
  • Fetch links from your API when the app loads.
  • Send new links to your API when the form is submitted.
  • Delete links through your API when the delete button is clicked.
Load links from your API on startup

React's useEffect hook lets you run code when a component first appears on screen. You'll use it to fetch the list of links from your Express API the moment the app loads.

  • In VS Code, open client/src/App.jsx.
  • Find the line that says const [url, setUrl] = useState(''); and add the following code below it:
  useEffect(() => {
    fetchLinks();
  }, []);

  async function fetchLinks() {
    const response = await fetch(`${API_URL}/links`);
    const data = await response.json();
    setLinks(data);
  }

What does this code do?

useEffect(() => { ... }, []) runs the function inside it exactly once, when the component first renders. The empty [] at the end is called the dependency array. It tells React there are no values to watch, so the effect only fires on mount.

fetchLinks() sends a GET request to your Express server at http://localhost:3001/links, converts the JSON response into a JavaScript array, and stores it in state with setLinks(data).

  • Save the file.
  • Switch to your browser at http://localhost:5173. You should see two links appear: Express.js Docs and React Documentation.

Don't see any links?

  • Make sure your Express server is still running in its terminal window. If it stopped, restart it with npm start from inside the server folder.
  • Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). If you see a CORS error, your server may not have the cors() middleware applied correctly.
  • Check that API_URL at the top of App.jsx is set to 'http://localhost:3001' with no trailing slash.

Help me debug why my React app can't fetch links from my Express API.

Send new links to the API

Fetching data is only half the story. Your form needs to send new links to the API using a POST request. This function will package the title and URL from your form fields into JSON, send them to the server, and update the UI with the saved link.

  • In App.jsx, find the fetchLinks function you just added. Copy the following code below it:
  async function handleSubmit(event) {
    event.preventDefault();

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

    if (response.ok) {
      const newLink = await response.json();
      setLinks([...links, newLink]);
      setTitle('');
      setUrl('');
    }
  }

What does this code do?

  • event.preventDefault() stops the browser from refreshing the page when the form submits. Without this, your app would reload and lose its state.
  • The fetch call sends a POST request with a JSON body containing the title and url from your form state.
  • setLinks([...links, newLink]) adds the newly created link to the existing array. The ... spread operator copies the old array and appends the new item.
  • setTitle('') and setUrl('') clear the form inputs after a successful save.

Now you need to tell the form to call handleSubmit when the user clicks Add Link.

  • Scroll down to the return statement and find this line:
      <form className="add-form">
  • Add onSubmit={handleSubmit} before className, so the line looks like this:
      <form onSubmit={handleSubmit} className="add-form">
  • Save the file.
  • In your browser, type My Portfolio in the Title field and https://example.com in the URL field, then click Add Link. You should see the new link appear at the bottom of the list, and the form fields should clear.

Link not appearing after you click Add Link?

  • If the page refreshes and the link disappears, you may be missing event.preventDefault() at the top of handleSubmit.
  • If nothing happens at all, make sure you added onSubmit={handleSubmit} to the <form> tag.
  • Check your browser console for errors. A 400 status means the server rejected the request because title or URL was missing from the request body.

Help me debug why my form submission isn't saving links.

Delete links through the API

The last piece of the puzzle is removing links. When a user clicks Delete, your app needs to tell the API which link to remove, then update the UI to reflect the change.

  • In App.jsx, find the handleSubmit function you just added. Copy the following code below it:
  async function handleDelete(id) {
    const response = await fetch(`${API_URL}/links/${id}`, {
      method: 'DELETE',
    });

    if (response.ok) {
      setLinks(links.filter(link => link.id !== id));
    }
  }

What does this code do?

  • The function sends a DELETE request to /links/:id, where :id is the specific link's identifier.
  • links.filter(link => link.id !== id) creates a new array that includes every link except the deleted one. React then re-renders the list without it.

Now connect the Delete button in the JSX to this function.

  • In the return statement, find this line:
            <button className="delete-btn">
  • Add onClick={() => handleDelete(link.id)} before className, so the line looks like this:
            <button onClick={() => handleDelete(link.id)} className="delete-btn">
  • Save the file.
  • In your browser, click Delete next to one of the links. The link should disappear from the list immediately.

Delete button not working?

  • Make sure you used an arrow function: onClick={() => handleDelete(link.id)}. Writing onClick={handleDelete(link.id)} without the arrow function calls the function immediately on render, not on click.
  • Check that handleDelete is defined inside the App function, not outside it. It needs access to the links state.

Help me fix my delete button in React.

What is the full-stack loop?

You've just built the pattern behind every web app. The user interacts with the UI, which sends an HTTP request to the server. The server processes the request and sends a response. The frontend reads the response and updates the UI.

This loop (UI action, HTTP request, server logic, response, state update) is the same whether you're building a social network, an e-commerce store, or a todo app. The tools change, but the pattern stays.

✔️ Awesome, I've got everything!

Great. Double check you've saved App.jsx and that both servers are still running.

ⓧ I'd like to double check the full code

Here's the complete client/src/App.jsx file:

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

const API_URL = 'http://localhost:3001';

function App() {
  const [links, setLinks] = useState([]);
  const [title, setTitle] = useState('');
  const [url, setUrl] = useState('');

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

  async function fetchLinks() {
    const response = await fetch(`${API_URL}/links`);
    const data = await response.json();
    setLinks(data);
  }

  async function handleSubmit(event) {
    event.preventDefault();

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

    if (response.ok) {
      const newLink = await response.json();
      setLinks([...links, newLink]);
      setTitle('');
      setUrl('');
    }
  }

  async function handleDelete(id) {
    const response = await fetch(`${API_URL}/links/${id}`, {
      method: 'DELETE',
    });

    if (response.ok) {
      setLinks(links.filter(link => link.id !== id));
    }
  }

  return (
    <div className="app">
      <h1>Link Vault</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
        />
        <button type="submit">Add Link</button>
      </form>

      <ul className="link-list">
        {links.map(link => (
          <li key={link.id} className="link-item">
            <a href={link.url} target="_blank" rel="noopener noreferrer">
              {link.title}
            </a>
            <button onClick={() => handleDelete(link.id)} className="delete-btn">
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Secret mission

Add Search and Filter

Your Link Vault works great, but what happens when you have dozens of saved links? Add a real-time search bar that filters links as you type, introducing a powerful pattern called derived state.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Express server process running on port 3001.
  • Vite dev server process running on port 5173.
  • The link-vault project folder on your Desktop (contains server/ and client/ directories with all project files).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your Link Vault.

  • Your Express server continues to serve the API on http://localhost:3001.
  • Your Vite dev server continues to serve the React app on http://localhost:5173.
  • Your project files remain on your Desktop for future editing.
  • Node.js remains installed on your machine (useful for future JavaScript projects).

Pause - I'll come back to this later

Shut down the running servers to free up terminal sessions and ports. Your project files stay on disk so you can start the servers again anytime.

  • Switch to the terminal window running your Express server and press Ctrl+C to stop it.
  • Switch to the terminal window running your Vite dev server and press Ctrl+C to stop it.

When you're ready to come back, navigate to each folder in your terminal and run the start commands again.

Delete - I don't want to use this again

Remove all project files and stop all running processes. This frees up disk space and ports.

  • Switch to the terminal window running your Express server and press Ctrl+C to stop it.
  • Switch to the terminal window running your Vite dev server and press Ctrl+C to stop it.
  • Delete the entire project folder by running this command:

macOS

rm -rf ~/Desktop/link-vault

Windows

rmdir /s /q %USERPROFILE%\Desktop\link-vault

This removes the server/ folder, client/ folder, all node_modules, and your source code.

Nice Work!

Nice Work!

You just built your first full-stack web application from scratch. Your Link Vault has a working React frontend talking to an Express backend, all running on your own machine.

You've learned how to:

  • Built a REST API from scratch with Express that handles GET, POST, and DELETE requests.
  • Created a React frontend with dynamic state management using useState and useEffect hooks.
  • Connected frontend to backend using HTTP fetch calls to create a complete client-server architecture.
  • Added real-time search filtering using derived state to filter an array without modifying the original data.

Ready to quiz yourself?