Build a Full-Stack Bookmark Manager

Build a React and Express app that saves bookmarks to a SQLite database.

Introduction

30 Second Summary

Every app you use daily follows the same pattern: a screen you interact with, a server that processes your requests, and a database that remembers your data. Understanding how these three layers connect is the single most valuable skill in web development.

In this project, you will build a bookmark manager application with a React front-end, an Express back-end API, and a SQLite database that saves your data to a local file.

What You'll Build

A full-stack bookmark manager where you can add, view, and delete web bookmarks through a React UI that communicates with your own REST API and database.

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

  • A React front-end with a form to add bookmarks and a list to display them.
  • An Express REST API with GET, POST, and DELETE endpoints.
  • A SQLite database that persists your bookmarks to a local file.
  • Secret Mission: Add edit functionality so you can update bookmark titles and URLs after saving them.

Are there any prerequisites?

You need a basic understanding of HTML, CSS, and JavaScript. No prior React or Node.js experience is required.

Set Up the Project Structure

Every full-stack app needs a clear separation between its front-end and back-end code. This project uses two folders: one for your Express API server, and one for your React client app.

In this step, you will create both folders and install all the dependencies each side needs to run.

In this step, get ready to:
  • Create the project folder with server and client directories.
  • Install server dependencies (Express, CORS, better-sqlite3).
  • Scaffold the React client using Vite.
Create the project folder and initialize the server

You will create a bookmark-manager folder on your Desktop, then set up the server inside it with Node.js and three packages.

  • Press Cmd+` (macOS) or Ctrl+` (Windows) to open the integrated terminal in VS Code.
  • Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir bookmark-manager
cd bookmark-manager
  • Create the server directory and initialize it as a Node.js project by running:
mkdir server
cd server
npm init -y

What does npm init -y do?

npm init creates a package.json file that tracks your project's name, version, and dependencies.

The -y flag accepts all default values so you skip the interactive prompts.

You should see output ending with Wrote to .../server/package.json confirming the file was created.

  • Install the three packages your server needs by running:
npm install express cors better-sqlite3

What are these packages?

  • express is a web framework that handles HTTP requests and routing.
  • cors allows your React app (running on a different port) to communicate with your API.
  • better-sqlite3 is a fast database library that stores data in a local file on your machine.

You should see output like added X packages in your terminal, confirming all three packages installed successfully.

Seeing an install error?

  • Make sure you are inside the server folder when you run npm install. Run pwd to check your current location.
  • If you see errors about node-gyp or native modules, your Node.js version might be too old. better-sqlite3 requires Node.js v20 or later. Run node --version to check.

Help me fix npm install errors for better-sqlite3

  • Open server/package.json in VS Code and replace its entire contents with this:
{
  "name": "bookmark-server",
  "version": "1.0.0",
  "description": "Bookmark Manager API",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^5.2.1",
    "cors": "^2.8.6",
    "better-sqlite3": "^12.9.0"
  }
}

What does this file do?

  • name and description identify your project for anyone reading the file.
  • "start": "node index.js" lets you run your server later with npm start.
  • dependencies lists the three packages you just installed with their version ranges.
  • Save server/package.json.

✔️ Awesome, I've got everything!

Great. Double check you have saved server/package.json and you are ready to move on.

ⓧ I'd like to double check the full code

{
  "name": "bookmark-server",
  "version": "1.0.0",
  "description": "Bookmark Manager API",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^5.2.1",
    "cors": "^2.8.6",
    "better-sqlite3": "^12.9.0"
  }
}
Scaffold the React client with Vite

Vite is a modern build tool that scaffolds React projects with a fast development server. You will use it to create the entire client folder in one command.

  • Navigate back to the project root by running:
cd ..
  • Scaffold a new React app in a folder called client by running:
npm create vite@latest client -- --template react

What does this command do?

  • npm create vite@latest runs the Vite project scaffolding tool.
  • client tells Vite to create the project inside a folder called client.
  • -- --template react selects the React JavaScript template so you skip all interactive prompts.

You should see output confirming the project was scaffolded, including a message about running npm install.

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

You should see output like added X packages confirming the React dependencies installed.

Seeing permission errors or command not found?

  • Make sure you ran cd .. first so you are in the bookmark-manager root folder before running the Vite command.
  • If npm create fails, ensure your Node.js version is 20.19 or later by running node --version.

Help me troubleshoot the Vite scaffold command

Verify the project structure
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to Desktop, select the bookmark-manager folder, then click Open.
  • In the Explorer sidebar on the left, expand both the server and client folders.

You should see package.json and node_modules inside both folders.

The client folder also contains files like index.html, vite.config.js, and a src folder created by Vite.

Why two separate folders?

Keeping the server and client in separate directories with their own package.json files means each side manages its own dependencies independently.

This mirrors how professional teams structure full-stack apps. The front-end and back-end can be developed, tested, and deployed on their own schedules.

Your project structure is ready. Next up, you will create the database layer that stores your bookmarks.

Create the Database Layer

Your project structure is ready with both a server and client folder installed. Now it's time to give your application a place to store data.

Every bookmark a user saves needs to persist between sessions. Without a database, bookmarks would vanish every time the server restarts. In this step, you'll create a SQLite database that stores bookmarks in a file on your machine.

In this step, get ready to:
  • Create the database configuration file.
  • Verify the database initializes correctly.
Create the database configuration file

Your database layer is a single file that initializes the connection and defines the table structure. Every other file in your server will import this one file to interact with the database.

  • In the VS Code Explorer sidebar, right-click the server folder and select New File.
  • Name the file database.js and press Enter.
  • Add the following code to database.js:
const Database = require('better-sqlite3');
const path = require('path');

// Open (or create) a database file called bookmarks.db in the server folder
const db = new Database(path.join(__dirname, 'bookmarks.db'));

// Enable Write-Ahead Logging for better performance
db.pragma('journal_mode = WAL');

// Create the bookmarks table if it doesn't already exist
db.exec(`
  CREATE TABLE IF NOT EXISTS bookmarks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    url TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

// Export the database connection for use in other files
module.exports = db;

What does this code do?

  • require('better-sqlite3') loads the SQLite library you installed in Step 1.
  • new Database(path.join(__dirname, 'bookmarks.db')) creates (or opens) a database file called bookmarks.db in the same folder as this script.
  • db.pragma('journal_mode = WAL') enables Write-Ahead Logging, which improves database read/write performance.
  • The CREATE TABLE IF NOT EXISTS statement defines four columns for your bookmarks: an auto-incrementing ID, a title, a URL, and a timestamp. It only creates the table if it doesn't already exist, so running this file multiple times is safe.
  • module.exports = db makes the database connection available to other files in your project.
  • Save database.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Verify the database initializes

Running the database file triggers the initialization code. If everything works, a bookmarks.db file will appear in your server folder.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (both macOS and Windows).
  • Navigate into the server folder by running this command:
cd server
  • Run the database file to trigger initialization:
node database.js

The command will finish without printing anything. That's expected.

  • Confirm the database file was created by running:
ls

You should see bookmarks.db listed alongside database.js, package.json, and node_modules.

Don't see bookmarks.db?

  • Make sure you saved database.js before running the command. Unsaved changes won't execute.
  • Check that your terminal is inside the server folder. Run pwd to confirm your current directory ends with /server.
  • If you see an error about Cannot find module 'better-sqlite3', run npm install inside the server folder to reinstall dependencies.

Help me troubleshoot why my database file isn't being created.

Why SQLite instead of something like MongoDB?

SQLite requires zero configuration. No accounts, no separate servers, no connection strings. You install one npm package and get a fully working database stored as a single file.

It also teaches SQL, which is a transferable skill across most databases in the industry.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that bookmarks.db appears in your server folder.

ⓧ I'd like to double check the full code

Here is the complete server/database.js file:

const Database = require('better-sqlite3');
const path = require('path');

const db = new Database(path.join(__dirname, 'bookmarks.db'));

db.pragma('journal_mode = WAL');

db.exec(`
  CREATE TABLE IF NOT EXISTS bookmarks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    url TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

module.exports = db;

Your database is ready and waiting for bookmarks. Next up, you'll build the REST API that reads from and writes to this database.

Build the REST API

Your database is ready and waiting for data. Now you need a way for the outside world to interact with it.

A REST API is a set of URLs (called endpoints) that accept requests and return responses. In this step, you will build three endpoints that let any client create, read, and delete bookmarks through standard HTTP methods.

In this step, get ready to:
  • Create an Express server with three API routes.
  • Start the server and test each endpoint with curl commands.
  • Confirm the full create-read-delete cycle works.
Create the Express server file

Express is a web framework for Node.js that makes it simple to define URL routes and handle requests. You will create a single file that sets up the server and all three endpoints.

  • In VS Code, create a new file called index.js inside your server folder.
  • Add the imports and server setup by pasting this code:
const express = require('express');
const cors = require('cors');
const db = require('./database');

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

// Enable cross-origin requests and JSON body parsing
app.use(cors());
app.use(express.json());

What does this code do?

This block loads three modules and configures the server:

  • express is the web framework that handles routing and responses.
  • cors allows your React front-end (running on a different port) to make requests to this server.
  • db is the database connection you exported from database.js in the previous step.
  • express.json() tells Express to automatically parse incoming JSON request bodies.
  • Below the middleware lines, add the GET route that returns all bookmarks:
app.get('/api/bookmarks', (req, res) => {
  const bookmarks = db.prepare('SELECT * FROM bookmarks ORDER BY created_at DESC').all();
  res.json(bookmarks);
});

What does this route do?

When a client sends a GET request to /api/bookmarks, this handler runs a SQL query that selects every row from the bookmarks table. The .all() method returns the results as an array of objects. The server sends that array back as JSON.

  • Below the GET route, add the POST route that creates a new bookmark:
app.post('/api/bookmarks', (req, res) => {
  const { title, url } = req.body;
  if (!title || !url) {
    return res.status(400).json({ error: 'Title and URL are required' });
  }
  const stmt = db.prepare('INSERT INTO bookmarks (title, url) VALUES (?, ?)');
  const info = stmt.run(title, url);
  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(info.lastInsertRowid);
  res.status(201).json(bookmark);
});

What does this route do?

  • It extracts title and url from the request body.
  • If either field is missing, it returns a 400 error with a helpful message.
  • The ? placeholders in the SQL prevent injection attacks by separating data from commands.
  • After inserting, it fetches the newly created row using lastInsertRowid and returns it with a 201 (Created) status.
  • Below the POST route, add the DELETE route and the server startup:
app.delete('/api/bookmarks/:id', (req, res) => {
  const { id } = req.params;
  const info = db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id);
  if (info.changes === 0) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }
  res.status(204).send();
});

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

What does this code do?

  • The DELETE route reads the bookmark id from the URL (the :id part). If no row was deleted, it returns a 404 error. Otherwise it returns 204 (No Content) to confirm deletion.
  • app.listen() starts the server on port 3001 and prints a message so you know it is running.
  • Save server/index.js.

Seeing a red underline or syntax error?

  • Make sure every opening brace { has a matching closing brace }. Each route handler ends with });.
  • Check that the template literal in app.listen uses backticks (`, not single quotes '.
  • help me find a syntax error in my server/index.js file

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete server/index.js file for comparison:

const express = require('express');
const cors = require('cors');
const db = require('./database');

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

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

app.get('/api/bookmarks', (req, res) => {
  const bookmarks = db.prepare('SELECT * FROM bookmarks ORDER BY created_at DESC').all();
  res.json(bookmarks);
});

app.post('/api/bookmarks', (req, res) => {
  const { title, url } = req.body;
  if (!title || !url) {
    return res.status(400).json({ error: 'Title and URL are required' });
  }
  const stmt = db.prepare('INSERT INTO bookmarks (title, url) VALUES (?, ?)');
  const info = stmt.run(title, url);
  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(info.lastInsertRowid);
  res.status(201).json(bookmark);
});

app.delete('/api/bookmarks/:id', (req, res) => {
  const { id } = req.params;
  const info = db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id);
  if (info.changes === 0) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }
  res.status(204).send();
});

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

Your API code is written, but the server is not running yet. You need to start it and then send requests to confirm each endpoint works.

  • Open a terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
  • Navigate into the server folder and start the server by running these commands:
cd server
node index.js

You should see this message in your terminal:

Server running on http://localhost:3001

Seeing an error instead?

  • If you see Cannot find module './database', make sure database.js is in the same server folder as index.js.
  • If you see EADDRINUSE, port 3001 is already in use. Close any other terminal that ran this server previously.
  • help me fix my Express server startup error

Now test the API. You need to open a second terminal because the server is occupying the first one.

  • Open a new terminal tab by clicking the + icon in the VS Code terminal panel.
  • Test the GET endpoint by running this command:
curl http://localhost:3001/api/bookmarks

You should see an empty array: []. This confirms the GET route works and the database has no bookmarks yet.

  • Test the POST endpoint by creating a bookmark with this command:
curl -X POST http://localhost:3001/api/bookmarks -H "Content-Type: application/json" -d "{\"title\": \"NextWork\", \"url\": \"https://nextwork.org\"}"

You should see a JSON object returned with your bookmark's id, title, url, and created_at fields.

  • Run the GET command again to confirm the bookmark is stored:
curl http://localhost:3001/api/bookmarks

You should now see an array containing the bookmark you just created.

Getting connection refused?

  • Check that your first terminal still shows the server running. If you accidentally closed it, run node index.js again from the server folder.
  • Make sure the curl command uses port 3001, not 3000.
  • help me debug why curl cannot connect to my Express server
Test the DELETE endpoint

The last endpoint to verify is DELETE. You will remove the test bookmark you just created and confirm it is gone.

  • Delete the bookmark by running this command (the 1 at the end is the bookmark's id):
curl -X DELETE http://localhost:3001/api/bookmarks/1

This command returns no visible output because a 204 response has no body. That is the expected behavior.

  • Confirm the bookmark is gone by running GET one more time:
curl http://localhost:3001/api/bookmarks

You should see an empty array again: []. Your full create-read-delete cycle works.

Still seeing the bookmark after DELETE?

  • Check the id number in your DELETE URL. Look at the GET response to find the correct id value for the bookmark you want to remove.
  • Make sure the method flag is uppercase: -X DELETE not -X delete.
  • help me troubleshoot my DELETE endpoint

Why CORS?

Browsers enforce a security rule called the same-origin policy. Your React app will run on port 5173 and your API runs on port 3001. Without CORS middleware, the browser would block your front-end from talking to your own API.

The cors() middleware you added tells the browser that requests from any origin are allowed. In a production app you would restrict this, but for local development it keeps things simple.

Your API is live and tested. Next up, you will build the React front-end that replaces these curl commands with a real user interface.

Build the React Front-End and Connect to the API

Your API is live and responding to requests. Now it is time to build the front-end that your users will actually see and interact with.

React will handle the UI, managing what appears on screen as data changes. You will use React's useState to track form inputs and bookmark data, useEffect to load bookmarks when the page opens, and fetch() to send requests to your Express API.

In this step, get ready to:
  • Replace the default Vite content with a bookmark manager component.
  • Style the bookmark form and list.
  • Start the React dev server and test the full integration.
Replace the default content with the bookmark component

The Vite scaffold gave you a placeholder app with a spinning logo and counter. You will replace all of that with a component that talks to your API.

  • In VS Code, open client/src/App.jsx from the Explorer sidebar.
  • Select all the existing code with Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
  • Paste the following code into the empty file:
import { useState, useEffect } from 'react';
import './App.css';

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

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

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

  async function fetchBookmarks() {
    const response = await fetch(API_URL);
    const data = await response.json();
    setBookmarks(data);
  }

  async function addBookmark(e) {
    e.preventDefault();
    const response = await fetch(API_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, url }),
    });
    if (response.ok) {
      setTitle('');
      setUrl('');
      fetchBookmarks();
    }
  }

  async function deleteBookmark(id) {
    const response = await fetch(`${API_URL}/${id}`, {
      method: 'DELETE',
    });
    if (response.ok) {
      fetchBookmarks();
    }
  }

  return (
    <div className="app">
      <h1>Bookmark Manager</h1>
      <form onSubmit={addBookmark} className="bookmark-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 Bookmark</button>
      </form>
      <ul className="bookmark-list">
        {bookmarks.map((bookmark) => (
          <li key={bookmark.id} className="bookmark-item">
            <a href={bookmark.url} target="_blank" rel="noopener noreferrer">
              {bookmark.title}
            </a>
            <button onClick={() => deleteBookmark(bookmark.id)} className="delete-btn">
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

What does this code do?

This file defines a single React component that manages the entire bookmark UI. Here is what each section handles:

  • useState([]) creates a bookmarks array in memory. Two more useState calls track the form input values for title and url.
  • useEffect with an empty [] dependency array calls fetchBookmarks() once when the component first appears on screen.
  • fetchBookmarks() sends a GET request to your Express API and stores the response in state.
  • addBookmark() sends a POST request with the form data, then clears the inputs and refreshes the list.
  • deleteBookmark() sends a DELETE request for a specific bookmark ID, then refreshes the list.
  • The JSX return renders a form with two inputs and a button, plus a list that maps over the bookmarks array to display each one with a delete button.
  • Save App.jsx.
  • You will not see a change yet. The next substep adds styles, and then you will start the dev server to see everything.

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

Here is the complete client/src/App.jsx file for comparison:

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

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

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

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

  async function fetchBookmarks() {
    const response = await fetch(API_URL);
    const data = await response.json();
    setBookmarks(data);
  }

  async function addBookmark(e) {
    e.preventDefault();
    const response = await fetch(API_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, url }),
    });
    if (response.ok) {
      setTitle('');
      setUrl('');
      fetchBookmarks();
    }
  }

  async function deleteBookmark(id) {
    const response = await fetch(`${API_URL}/${id}`, {
      method: 'DELETE',
    });
    if (response.ok) {
      fetchBookmarks();
    }
  }

  return (
    <div className="app">
      <h1>Bookmark Manager</h1>
      <form onSubmit={addBookmark} className="bookmark-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 Bookmark</button>
      </form>
      <ul className="bookmark-list">
        {bookmarks.map((bookmark) => (
          <li key={bookmark.id} className="bookmark-item">
            <a href={bookmark.url} target="_blank" rel="noopener noreferrer">
              {bookmark.title}
            </a>
            <button onClick={() => deleteBookmark(bookmark.id)} className="delete-btn">
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;
Add styles for the bookmark form and list

The component renders plain HTML elements with class names like bookmark-form and bookmark-item. You need matching CSS to make them look like a real app.

  • Open client/src/App.css in VS Code.
  • Select all with Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
  • Paste the following styles into the empty file:
.app {
  max-width: 600px;
  margin: 2rem auto;
  padding: 0 1rem;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #333;
}

.bookmark-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 2rem;
}

.bookmark-form input {
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

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

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

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

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

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

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

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

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

What do these styles do?

  • .app centers the content with a max width of 600px so it does not stretch across wide screens.
  • .bookmark-form stacks the inputs and button vertically using flexbox.
  • .bookmark-item places the bookmark link on the left and the delete button on the right using justify-content: space-between.
  • .delete-btn is styled as a red button that stands out from the purple form button.
  • Save App.css.
  • You will not see changes yet. Next, you will start the dev server to see everything come together.

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

Here is the complete client/src/App.css file for comparison:

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

h1 {
  text-align: center;
  color: #333;
}

.bookmark-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 2rem;
}

.bookmark-form input {
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

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

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

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

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

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

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

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

.delete-btn:hover {
  background-color: #dc2626;
}
Start the React dev server and test the full app

Your Express server is already running in one terminal. You need a second terminal for the React dev server since both need to run at the same time.

  • Open a new terminal window or tab. In VS Code, click Terminal in the top menu bar, then click New Terminal.
  • Navigate into the client folder by running this command:
cd client
  • Start the Vite dev server by running:
npm run dev

What should I see?

Vite prints a local URL in your terminal, typically http://localhost:5173. This is where your React app is served from.

  • Hold Cmd (macOS) or Ctrl and click the URL in your terminal to open it in your browser.

You should see a centered heading that says "Bookmark Manager" with a form containing two input fields and a purple "Add Bookmark" button.

  • Type NextWork in the Title field.
  • Type https://www.nextwork.org in the URL field.
  • Click Add Bookmark.

The bookmark appears in a list below the form. The form inputs clear automatically, ready for the next entry.

  • Refresh the page in your browser.

Your bookmark is still there. This confirms the data persists in the SQLite database and loads fresh on every page visit.

  • Click the red Delete button next to your bookmark.

The bookmark disappears immediately. Your full-stack app is working end to end.

What is the data flow here?

When you click Add Bookmark, React calls addBookmark() which sends a POST request to your Express server on port 3001. Express receives the request, inserts a row into the SQLite database, and returns the new bookmark as JSON.

React then calls fetchBookmarks() to GET the latest list from the API. The component re-renders with the updated data. This is the fundamental pattern behind every modern web application.

Not seeing any bookmarks appear?

  • Make sure your Express server is still running in the other terminal. The React app cannot reach the API if the server is stopped.
  • Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Look for a CORS error or "Failed to fetch" message.
  • Confirm that API_URL in your App.jsx is exactly http://localhost:3001/api/bookmarks. A typo in the port or path will cause silent failures.
  • Help me debug why my React app cannot connect to my Express API.

Secret mission

Add Edit Functionality

Your bookmark manager can create, read, and delete bookmarks. But what happens when you mistype a URL or want to rename a bookmark? In this secret mission, you'll add a PUT endpoint and inline editing to complete all four CRUD operations.

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:

  • Project folder on your Desktop containing server and client directories.
  • Node.js packages (node_modules folders in both server and client).
  • SQLite database file (bookmarks.db inside the server folder).
  • Express server process (if still running in your terminal).
  • Vite dev server process (if still running in your terminal).

Keep everything running

No action needed. Choose this if you are still actively building or want to extend the project with the Secret Mission.

  • Your Express server and Vite dev server will keep running until you close the terminal windows.
  • Your bookmarks stay saved in bookmarks.db even after restarting your computer.

Pause - I'll come back to this later

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

  • In the terminal running your Express server, press Ctrl+C to stop the server.
  • In the terminal running your Vite dev server, press Ctrl+C to stop it.
  • Your project folder, all source files, and the bookmarks.db database will remain on your Desktop.
  • To resume later, open two terminals, run node index.js in the server folder and npm run dev in the client folder.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your code, database, and all saved bookmarks permanently.

  • Stop both servers by pressing Ctrl+C in each terminal window.
  • Delete the entire project folder from your Desktop.
  • On macOS, drag the folder to the Trash. On Windows, right-click the folder and select Delete.

Nice Work!

Nice Work!

You just built a complete full-stack web application from scratch. Your bookmark manager has a React front-end, an Express API, and a SQLite database all working together.

You've learned how to:

  • Built a React front-end with Vite that manages state, handles form input, and renders dynamic data from an API.
  • Created an Express REST API with GET, POST, and DELETE endpoints that validate input and return proper HTTP status codes.
  • Connected all three layers with fetch() calls and CORS configuration, completing the full data flow from user click to database and back.
  • Added PUT/edit functionality in the Secret Mission, completing all four CRUD operations.

Ready to quiz yourself?