Build a Bookmark Saver with React
Build a full-stack bookmark app with an Express API and React frontend.
Introduction
30 Second Summary
Think about every website you use. There's the part you see on screen, and a server behind the scenes handling your data. Understanding how these two halves talk to each other is the foundation of modern web development.
In this project, you will build a Bookmark Saver app from scratch using React for the frontend and Express for the backend. You'll wire them together so they communicate seamlessly, giving you hands-on experience with full-stack JavaScript.
What You'll Build
A full-stack Bookmark Saver application where an Express backend stores bookmarks in memory and a React frontend lets you add, view, and delete them.
By the end of this project, you'll have:
- An Express server with GET, POST, and DELETE API routes that manage bookmarks.
- A React app with a form to add bookmarks and a list to display and delete them.
- A Vite proxy configuration connecting your frontend to backend without CORS issues.
- Secret Mission: Add an "edit" feature so you can update bookmark titles inline.
Are there any prerequisites?
No prior experience with React or Express is needed. This guide walks you through setting up every tool from scratch, including Node.js and Vite.
Before We Start
A full-stack app has two separate pieces that need to talk to each other. Your backend handles storing and managing data. Your frontend handles what users see and interact with.
Before we can build anything, both environments need to be ready. In this step, you will verify your Node.js installation, set up an Express backend, and scaffold a React frontend with Vite.
In this step, get ready to:
- Verify that Node.js version 20.19.0 or higher is installed.
- Create the project folder and initialize the Express backend.
- Scaffold a React frontend using Vite.
Verify your Node.js version
This project uses Vite to scaffold the frontend, which requires Node.js 20.19.0 or higher. Let's confirm your version first.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Node.js version by running this command:
node --version
You should see a version number printed in the terminal, like v20.19.0 or higher.
✔️ I see version 20.19.0 or higher
Your Node.js version is compatible. You are ready to move on to creating the project folder.
ⓧ I see an older version
Your installed Node.js version is too old for Vite. You need to upgrade to 20.19.0 or higher.
- Go to https://nodejs.org/en/download in your browser.
- Download the LTS installer for your operating system.
- Run the installer and follow the prompts. Accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm the new version.
ⓧ Command not found
Node.js is not installed on your machine yet.
- Go to https://nodejs.org/en/download in your browser.
- Download the LTS installer for your operating system.
- Run the installer and follow the prompts. Accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm the version is 20.19.0 or higher.
Create the project folder and initialize the backend
Your project needs two sub-folders: one for the Express backend and one for the React frontend. Let's create the structure and get the backend ready.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and the backend sub-folder, then move into the backend by running these commands:
mkdir bookmark-saver
cd bookmark-saver
mkdir backend
cd backend
- Initialize the backend as a Node.js project by running:
npm init -y
What does this command do?
The npm init -y command creates a package.json file in the current folder. This file tracks your project's name, version, and dependencies. The -y flag accepts all the defaults so you don't have to answer prompts.
The generated package.json needs a few edits before we install Express. We need to enable ES module syntax and set up a start script.
- Open the backend folder in Cursor. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it.
- In Cursor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, open the bookmark-saver folder, select the backend folder, then click Open.
- In the Explorer sidebar, click package.json to open it.
- Select all the content in the file and replace it with the following:
{
"name": "bookmark-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
}
}
Why add type module?
Setting "type": "module" tells Node.js to use the modern import/export syntax instead of the older require style. This keeps your backend code consistent with the frontend, which also uses import statements.
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
- Return to your terminal. Make sure you are still inside the backend folder.
- Install Express by running:
npm install express
You should see output confirming that Express was installed, with a line like added 1 package or similar.
Seeing permission errors?
On macOS, try prefixing the command with sudo (you will be asked for your password). On Windows, try running the terminal as Administrator by right-clicking and selecting Run as administrator.
Help me fix this permission error
- Confirm the install worked by opening package.json in Cursor again.
You should now see a dependencies section containing "express": "^5.2.1".
✔️ Awesome, I've got everything!
Great. Double check you have saved package.json.
ⓧ I'd like to double check the full code
Your backend/package.json should look exactly like this:
{
"name": "bookmark-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.2.1"
}
}
Scaffold the React frontend with Vite
Now let's create the frontend. Vite provides a scaffolding tool that generates a ready-to-use React project in seconds.
- In your terminal, move back to the project root by running:
cd ..
- Scaffold the React frontend into a client folder by running:
npm create vite@latest client -- --template react
What does this command do?
This command uses the create-vite tool to generate a new React project. The client argument names the folder it creates. The --template react flag tells Vite to use the React starter template with all the files you need.
- Move into the new client folder by running:
cd client
- Install the frontend dependencies by running:
npm install
You should see output showing packages being downloaded and installed. This pulls in React, ReactDOM, and the Vite development tools.
- Confirm the project was scaffolded correctly by running:
ls src
You should see files including App.jsx, App.css, and main.jsx listed in the output.
Don't see those files?
Make sure you ran npm create vite@latest client -- --template react from the bookmark-saver folder (not from inside backend). If the client folder is missing, navigate back to bookmark-saver and try the scaffold command again.
Help me troubleshoot my Vite scaffold
Your project structure is ready. Next up, you will build the Express API that stores and serves bookmarks.
Build the Express API
Your project folders are ready and Express is installed. Now it's time to build the server that stores and manages bookmarks.
Express is a web framework for Node.js that makes it easy to create API endpoints. You'll define routes that respond to different types of HTTP requests so the frontend can read, create, and delete bookmarks.
In this step, get ready to:
- Set up a server file with Express and seed data.
- Add GET, POST, and DELETE API routes for bookmarks.
- Test the API by running the server and checking the response.
Set up the Express server with starter data
Before adding routes, the server needs a foundation. You'll create the Express app, tell it how to read JSON request bodies, and seed it with a couple of example bookmarks.
- In the backend folder, create a new file called server.js.
- Paste the following code into server.js:
import express from 'express';
const app = express();
const port = 3001;
// Parse incoming JSON request bodies
app.use(express.json());
// Seed data: bookmarks stored in memory
let bookmarks = [
{ id: 1, title: 'React Documentation', url: 'https://react.dev' },
{ id: 2, title: 'Express.js Guide', url: 'https://expressjs.com' },
];
// Counter for generating unique IDs
let nextId = 3;
// Start the server
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
What does this code do?
- import express from 'express' loads Express using ES module syntax (enabled by "type": "module" in your package.json).
- app.use(express.json()) tells Express to automatically parse JSON from incoming request bodies into req.body.
- The bookmarks array acts as a simple in-memory database. Two bookmarks are pre-loaded so you have data to work with immediately.
- nextId tracks the next available ID for new bookmarks.
- app.listen(port, ...) starts the server on port 3001 and logs a confirmation message.
- Save server.js.
- Confirm the file exists by running:
ls
You should see server.js listed alongside package.json and node_modules in the output.
Don't see server.js?
- Make sure you created the file inside the backend folder, not in the project root or client folder.
- Check that your terminal is in the backend directory. Run pwd (macOS/Linux) or cd (Windows) to confirm.
Still stuck? Help me find where my server.js file ended up
Add the API routes
A REST API uses different HTTP methods for different actions. GET retrieves data, POST creates new data, and DELETE removes data.
- In server.js, find the line let nextId = 3;. Add the following code below it, above the app.listen block:
app.get('/api/bookmarks', (req, res) => {
res.json(bookmarks);
});
app.post('/api/bookmarks', (req, res) => {
const { title, url } = req.body;
// Reject the request if title or url is missing
if (!title || !url) {
return res.status(400).json({ error: 'Title and URL are required' });
}
// Create a new bookmark with an auto-incrementing ID
const newBookmark = { id: nextId++, title, url };
bookmarks.push(newBookmark);
res.status(201).json(newBookmark);
});
What do these routes do?
- GET /api/bookmarks responds with the entire bookmarks array as JSON. This is what the frontend calls to display the list.
- POST /api/bookmarks reads title and url from the request body, validates that both exist, then creates a new bookmark object and adds it to the array.
- res.status(201) sets the HTTP status code to "Created" so the frontend knows the operation succeeded.
- Save server.js. You won't see a visible change yet. The next chunk completes the API with the DELETE route.
Seeing a red underline or syntax error?
- Make sure the new code is placed after let nextId = 3; and before app.listen(port, () => {.
- Check that you have a closing }); for both app.get and app.post. A missing bracket is the most common cause.
Need help? Help me fix a syntax error in my Express routes
- Below the POST route you just added (still above app.listen), add the DELETE route:
app.delete('/api/bookmarks/:id', (req, res) => {
// Convert the URL parameter from a string to a number
const id = parseInt(req.params.id);
const index = bookmarks.findIndex((b) => b.id === id);
// If no bookmark has this ID, return a 404 error
if (index === -1) {
return res.status(404).json({ error: 'Bookmark not found' });
}
// Remove the bookmark from the array
bookmarks.splice(index, 1);
res.status(204).end();
});
What does the DELETE route do?
- :id in the path is a route parameter. Express captures whatever value appears in the URL (like /api/bookmarks/2) and makes it available as req.params.id.
- parseInt converts the string parameter to a number so it can match the numeric id property on each bookmark.
- findIndex locates the bookmark's position in the array. If it returns -1, the bookmark doesn't exist.
- splice(index, 1) removes exactly one item at that position. The 204 status means "No Content" — the deletion succeeded but there's nothing to send back.
- Save server.js. You'll test the full API in the next substep.
Editor showing an error on the colon in :id?
- The colon is part of Express's route syntax, not a typo. Make sure :id is inside the string '/api/bookmarks/:id'.
- If your editor highlights it as an error, it may be a linting rule. The code will still run correctly.
Need help? Help me fix an issue with my DELETE route
✔️ Awesome, I've got everything!
Great. Make sure you've saved server.js with all three routes in place.
ⓧ I'd like to double check the full code
Here's the complete server.js file. Compare yours against it:
import express from 'express';
const app = express();
const port = 3001;
app.use(express.json());
let bookmarks = [
{ id: 1, title: 'React Documentation', url: 'https://react.dev' },
{ id: 2, title: 'Express.js Guide', url: 'https://expressjs.com' },
];
let nextId = 3;
app.get('/api/bookmarks', (req, res) => {
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 newBookmark = { id: nextId++, title, url };
bookmarks.push(newBookmark);
res.status(201).json(newBookmark);
});
app.delete('/api/bookmarks/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Bookmark not found' });
}
bookmarks.splice(index, 1);
res.status(204).end();
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Test your API
Your server code is complete. Time to start it up and confirm it responds with your seed data.
- Make sure your terminal is in the backend folder.
- Start the server by running:
node server.js
You should see this message in your terminal:
Server running at http://localhost:3001
- Open your browser and paste this URL into the address bar:
http://localhost:3001/api/bookmarks
You should see a JSON array with two bookmarks displayed in the browser. It will look something like this:
[
{ "id": 1, "title": "React Documentation", "url": "https://react.dev" },
{ "id": 2, "title": "Express.js Guide", "url": "https://expressjs.com" }
]
Why visit the URL in a browser?
When you type a URL into your browser's address bar and hit Enter, the browser sends a GET request to that address. Your Express server receives the request, matches it to the GET /api/bookmarks route, and sends back the JSON response.
This is the same request your React frontend will make later. The browser is just a quick way to confirm the route works.
Don't see JSON in the browser?
- Check your terminal for error messages. A common issue is a typo in the code that prevents the server from starting.
- Make sure the URL is exactly http://localhost:3001/api/bookmarks with no extra slashes or typos.
- If you see "Cannot GET /api/bookmarks", your routes may not be in the right place. Double check them against the full code tab above.
- If port 3001 is already in use, stop any other servers and try again.
Still stuck? Help me debug why my Express server isn't returning bookmarks
Keep the server running
Leave this terminal open with the server running. You'll need it for the next steps. If you ever need to stop the server, press Ctrl+C in the terminal.
Your API is live and responding to requests. Next up, you'll build the React frontend that talks to this server and lets users interact with bookmarks through a visual interface.
Build the React Frontend
Your Express API is live and returning bookmark data. Now you need a way for users to actually see and interact with those bookmarks.
In this step, you will build a React frontend that fetches bookmarks from your API, displays them in a list, and lets users add or delete bookmarks through a form.
In this step, get ready to:
- Set up React state and a data-fetching function that pulls bookmarks from your API.
- Build a form and delete buttons that send POST and DELETE requests to your backend.
- Style the app with custom CSS so the bookmarks look clean and readable.
Set up component state and data fetching
React components manage their own data through state. Your bookmark app needs three pieces of state: the list of bookmarks, the title field value, and the URL field value.
- In Cursor, open client/src/App.jsx from the Explorer sidebar.
- Select all the existing content and delete it.
- Paste the following code to set up imports, state, and data fetching:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [bookmarks, setBookmarks] = useState([])
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
async function fetchBookmarks() {
const response = await fetch('/api/bookmarks')
const data = await response.json()
setBookmarks(data)
}
useEffect(() => {
fetchBookmarks()
}, [])
What does this code do?
useState creates three reactive variables. bookmarks holds the array of saved bookmarks. title and url track what the user types into the form fields.
fetchBookmarks() calls your Express API at /api/bookmarks and stores the response in state. The useEffect with an empty [] dependency array runs this fetch once when the component first loads.
You won't see a change yet. The next chunk adds the event handlers that make the form and delete buttons work.
Add the form submission and delete handlers
Your component needs two functions: one to send a new bookmark to the API, and one to delete an existing bookmark.
- In client/src/App.jsx, paste the following code directly below the useEffect block you just added:
async function handleSubmit(e) {
e.preventDefault()
if (!title || !url) return
await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, url }),
})
setTitle('')
setUrl('')
fetchBookmarks()
}
async function handleDelete(id) {
await fetch(`/api/bookmarks/${id}`, {
method: 'DELETE',
})
fetchBookmarks()
}
What do these handlers do?
handleSubmit prevents the default form reload, checks that both fields have values, then sends a POST request with the title and URL as JSON. After the request completes, it clears the inputs and re-fetches the updated list.
handleDelete sends a DELETE request to /api/bookmarks/:id with the bookmark's ID, then refreshes the list so the deleted item disappears.
You won't see a change yet. The next chunk adds the JSX that renders the form and bookmark list.
- Still in client/src/App.jsx, paste the following code directly below handleDelete to add the component's return statement and close the file:
return (
<div className="app">
<h1>Bookmark Saver</h1>
<form onSubmit={handleSubmit} className="bookmark-form">
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="url"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<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={() => handleDelete(bookmark.id)}
className="delete-btn"
>
Delete
</button>
</li>
))}
</ul>
</div>
)
}
export default App
What does the JSX render?
The <form> has two controlled inputs bound to title and url state. When the form submits, handleSubmit fires.
The <ul> maps over the bookmarks array and renders each one as a clickable link with a Delete button. Each <li> uses bookmark.id as its key so React can efficiently track changes.
- Save client/src/App.jsx.
Seeing a syntax error in your editor?
Make sure all three chunks are pasted in order with no gaps or extra characters between them. The file should start with import { useState, useEffect } and end with export default App.
Check that you deleted all of the original Vite template content before pasting.
help me fix a syntax error in my App.jsx file
✔️ Awesome, I've got everything!
Great. Double check you have saved client/src/App.jsx before moving on.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [bookmarks, setBookmarks] = useState([])
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
async function fetchBookmarks() {
const response = await fetch('/api/bookmarks')
const data = await response.json()
setBookmarks(data)
}
useEffect(() => {
fetchBookmarks()
}, [])
async function handleSubmit(e) {
e.preventDefault()
if (!title || !url) return
await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, url }),
})
setTitle('')
setUrl('')
fetchBookmarks()
}
async function handleDelete(id) {
await fetch(`/api/bookmarks/${id}`, {
method: 'DELETE',
})
fetchBookmarks()
}
return (
<div className="app">
<h1>Bookmark Saver</h1>
<form onSubmit={handleSubmit} className="bookmark-form">
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="url"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<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={() => handleDelete(bookmark.id)}
className="delete-btn"
>
Delete
</button>
</li>
))}
</ul>
</div>
)
}
export default App
Style the bookmark app
The component is functional, but the default Vite CSS won't match your bookmark UI. You will replace it with styles tailored to your form and list layout.
- Open client/src/App.css in the Explorer sidebar.
- Select all the existing content and delete it.
- Paste the following styles:
.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;
gap: 0.5rem;
margin-bottom: 2rem;
}
.bookmark-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.bookmark-form button {
padding: 0.5rem 1rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.bookmark-form button:hover {
background: #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 {
padding: 0.25rem 0.5rem;
background: #ef4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.delete-btn:hover {
background: #dc2626;
}
What do these styles do?
.app centers the content and caps it at 600px wide. .bookmark-form uses flexbox so the inputs and button sit in a single row.
.bookmark-item spaces each bookmark as a flex row with the link on the left and the delete button on the right. .delete-btn is styled red to make the destructive action visually distinct.
- Save client/src/App.css.
- Open your terminal inside the client folder and start the dev server by running:
npm run dev
- Open http://localhost:5173 in your browser.
- You should see the heading Bookmark Saver, a form with two input fields and a purple Add Bookmark button, and an empty list below.
Why is the bookmark list empty?
The fetch to /api/bookmarks fails right now because the Vite dev server does not know to forward that request to your Express backend on port 3001. You will fix this in the next step by adding a proxy.
Page not loading at all?
Make sure you ran npm run dev from inside the client folder, not the project root.
If you see a blank white page, open your browser's developer console. A syntax error in App.jsx will show there with a line number you can trace back.
help me debug why my Vite React app is not loading in the browser
✔️ Awesome, I've got everything!
Great. Double check you have saved client/src/App.css before moving on.
ⓧ I'd like to double check the full code
.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;
gap: 0.5rem;
margin-bottom: 2rem;
}
.bookmark-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.bookmark-form button {
padding: 0.5rem 1rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.bookmark-form button:hover {
background: #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 {
padding: 0.25rem 0.5rem;
background: #ef4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.delete-btn:hover {
background: #dc2626;
}
Your React frontend is built and styled. Next up, you will wire the frontend to the backend so the fetch requests actually reach your Express API.
Connect Frontend to Backend with the Vite Proxy
Your React frontend and Express backend are both built. There is one problem though: they run on different ports.
The React dev server runs on port 5173 and the Express server runs on port 3001. Browsers block requests between different origins by default, so your frontend cannot talk to the backend yet. In this step, you will configure a Vite proxy that forwards API requests from port 5173 to port 3001 behind the scenes.
In this step, get ready to:
- Configure the Vite proxy to forward API requests to the Express server.
- Run both servers simultaneously in separate terminals.
- Test adding and deleting bookmarks through the full-stack app.
Add the proxy configuration to Vite
Your React component already calls /api/bookmarks as a relative URL. Without a proxy, the browser sends that request to the Vite dev server on port 5173, which has no API routes. The proxy intercepts any request starting with /api and passes it to your Express server instead.
- Open client/vite.config.js in Cursor.
- Replace the entire file contents with the following code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// Configure Vite with the React plugin and a dev proxy
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// Forward any request starting with /api to Express
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
})
What does this code do?
The server.proxy object tells Vite's dev server to intercept requests. Any request whose path starts with /api gets redirected to http://localhost:3001 where your Express server lives.
The changeOrigin: true setting updates the request's origin header to match the target. This makes the Express server see the request as coming from its own domain.
Why a proxy instead of CORS?
Production full-stack apps typically serve the frontend and API from the same domain. The proxy pattern mirrors that single-origin setup during development.
The alternative is configuring CORS headers on the Express server to allow cross-origin requests. That adds complexity and teaches a pattern you would undo before deploying to production.
- Save client/vite.config.js.
Seeing a syntax error?
Make sure you have matching curly braces for each nested object (server, proxy, '/api'). The config is three levels deep, so a missing brace is easy to miss.
Also check that the comma after plugins: [react()] is present. Without it, JavaScript cannot parse the next property.
Help me debug my vite.config.js proxy setup.
✔️ 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
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// Configure Vite with the React plugin and a dev proxy
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// Forward any request starting with /api to Express
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
})
Start both servers
Your full-stack app needs two processes running at the same time: Express serving the API, and Vite serving the React frontend. You will use two separate terminal panels for this.
- In Cursor, open a terminal by pressing Cmd+J (macOS) or Ctrl+J (Windows).
- Navigate to the backend folder and start the Express server by running these commands:
cd backend
node server.js
- You should see Server running at http://localhost:3001 printed in the terminal.
- Open a second terminal panel by clicking the + icon at the top-right of the terminal panel.
- In the new terminal, navigate to the client folder and start the Vite dev server by running these commands:
cd client
npm run dev
- You should see Vite print a local URL: http://localhost:5173.
- Open http://localhost:5173 in your browser.
You should see the Bookmark Saver heading and two seeded bookmarks: React Documentation and Express.js Guide.
Not seeing the bookmarks?
Make sure the Express server is still running in your first terminal. If you accidentally closed it, run node server.js from the backend folder again.
If you see a blank page, open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for network errors. A 404 on /api/bookmarks means the proxy is not forwarding correctly. Double check that vite.config.js is saved and restart the Vite server with npm run dev.
Help me troubleshoot why my bookmarks are not loading.
Test the full flow
Both servers are running and the seeded bookmarks are visible. Now you will test the create and delete features to confirm the frontend is talking to the backend through the proxy.
- In the Title input field, type NextWork.
- In the URL input field, type https://nextwork.org.
- Click Add Bookmark.
The new bookmark should appear at the bottom of the list immediately.
- Click the Delete button next to the bookmark you just added.
The bookmark should disappear from the list. Check your first terminal (the Express server) to see logged requests coming in from the frontend.
What just happened?
When you clicked Add Bookmark, your React app sent a POST request to /api/bookmarks. Vite's proxy intercepted that request and forwarded it to Express on port 3001. Express added the bookmark to its in-memory array and sent back the new bookmark as JSON.
React then called fetchBookmarks() to re-fetch the full list from the server, updating the UI. The same cycle happens for delete, but with a DELETE request.
Bookmark not appearing after submit?
Make sure you filled in both fields. The Express server rejects requests where title or URL is missing and returns a 400 error.
If the form clears but the bookmark does not show up in the list, your GET request may be failing silently. Open the browser console and look for network errors on /api/bookmarks.
Help me figure out why my bookmark form submission is not working.
Secret mission
Add Edit Functionality
Your Bookmark Saver can add and delete, but what about fixing a typo in a title? In this secret mission, you'll add inline editing so users can update bookmark titles without deleting and re-creating them.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about.
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 backend and client code.
- Node.js packages installed in node_modules directories (inside both backend/ and client/).
- Running Express and Vite dev server processes.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with your Bookmark Saver app.
- Your Express server and Vite dev server will keep running in their terminal windows until you close them.
- To restart later, open two terminals, run node server.js from the backend/ folder, and npm run dev from the client/ folder.
Pause - I'll come back to this later
Shut down running processes to free up memory, but keep your project files for later.
- Switch to the terminal running your Express server and press Ctrl+C to stop it.
- Switch to the terminal running your Vite dev server and press Ctrl+C to stop it.
- Your project folder stays on your Desktop. When you are ready to resume, just start both servers again.
Delete - I don't want to use this again
Remove all project files and start fresh. Nothing was deployed, so deleting the folder is all you need.
- Stop both running servers by pressing Ctrl+C in each terminal window.
- Delete the project folder by running the command for your operating system below.
On macOS or Linux:
cd ~/Desktop
rm -rf bookmark-saver
On Windows:
cd ~\Desktop
rmdir /s /q bookmark-saver
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see bookmark-saver listed.
Nice Work!
Nice Work!
You just built a full-stack JavaScript application from scratch. Your React frontend talks to your Express backend, proving you understand how modern web apps are wired together from client to server.
You've learned how to:
- Built an Express REST API with GET, POST, and DELETE routes that serve, create, and remove bookmarks from an in-memory data store.
- Built a React frontend that manages state with useState and fetches data from your API with useEffect.
- Connected frontend to backend using a Vite development proxy, letting your React app call /api/bookmarks without worrying about cross-origin issues.
- Secret Mission: Added PUT-based edit functionality with inline editing UI, handling keyboard events and controlled inputs.
Ready to quiz yourself?