Build a Personal Link Vault

Build a full-stack web app with a REST API, SQLite database, and frontend.

Introduction

30 Second Summary

You save links everywhere. Browser tabs pile up, notes apps get cluttered, and messages to yourself disappear into the scroll. What if you had your own private tool to organize them all?

In this project, you will build a Personal Link Vault: a full-stack web application where you save, view, and delete bookmarks using a REST API and a database.

What You'll Build

A local web application with an HTML/CSS/JavaScript frontend, a Node.js/Express.js backend API, and a SQLite database that persists your saved links.

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

  • A REST API built with Express.js that handles GET, POST, and DELETE requests.
  • A SQLite database that stores your links and persists data between server restarts.
  • A frontend interface that communicates with the backend using JavaScript's fetch API.
  • Secret Mission: Add a real-time search feature that filters your saved links by title or tag.

Are there any prerequisites?

You should have a basic understanding of programming concepts like variables, functions, and loops. Familiarity with any programming language (such as Python or C) is enough to get started.

Install Node.js and Set Up the Project

Every web app needs a starting point. Before you can write any code for your Link Vault, you need a project folder, a configuration file that tracks your dependencies, and the packages themselves installed locally.

Node.js is already set up on your machine. In this step, you'll create the project structure and install two key packages: Express (the web framework) and better-sqlite3 (the SQLite database library).

In this step, get ready to:
  • Create and initialize the project folder with npm.
  • Install Express and better-sqlite3 as project dependencies.
Create the project directory and initialize it

You need a dedicated folder for your project and a package.json file that tracks metadata and dependencies. The npm init -y command generates this file automatically with sensible defaults.

  • In your Terminal, move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir link-vault
cd link-vault
  • Confirm the folder exists by running:
ls ~/Desktop

You should see link-vault listed in the output.

  • Initialize the project by running:
npm init -y

What does this command do?

The -y flag tells npm to accept all default values without prompting you for input. It creates a package.json file that uses the folder name (link-vault) as the package name.

Think of package.json as your project's ID card. It lists the project name, version, and every dependency your code needs to run.

  • You should see output that starts with Wrote to followed by the contents of your new package.json file.

Don't see the output?

Make sure you ran cd link-vault before running npm init -y. You need to be inside the project folder for npm to create the file there.

Help me troubleshoot npm init

Install Express and better-sqlite3

Your project needs two packages. Express handles incoming web requests and sends responses. better-sqlite3 reads and writes data to a local database file.

  • Install both dependencies by running:
npm install express@4.19.2 better-sqlite3@12.10.0

What are these packages?

  • express@4.19.2 is a web framework for Node.js. It makes it easy to define API routes that respond to browser requests.
  • better-sqlite3@12.10.0 is a database library that stores data in a single file on your machine. No database server required.

The @ symbol pins each package to an exact version so your project behaves the same way every time.

  • Wait for the install to finish. You should see a message showing how many packages were added.
  • Confirm the installation by running:
ls

You should see node_modules, package.json, and package-lock.json listed in the output.

Seeing a build error during install?

If you see an error mentioning missing build tools or a gyp error, you may need to install Xcode Command Line Tools. Run this command and then retry the install:

xcode-select --install

This is uncommon since better-sqlite3 ships prebuilt binaries for Node.js v24, but some system configurations trigger a build from source.

Help me fix this build error

✔️ Awesome, I've got everything!

Great. Make sure your Terminal is still inside the link-vault folder before moving on.

ⓧ I'd like to double check the full code

Open package.json in VS Code. It should look like this:

{
  "name": "link-vault",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "better-sqlite3": "^12.10.0",
    "express": "^4.19.2"
  }
}

The key section is dependencies at the bottom. If you see both better-sqlite3 and express listed there, you're all set.

Your project folder is ready with both dependencies installed. Next up, you'll write the actual server code that turns this folder into a working REST API.

Build the REST API Server

Your project folder is ready with Express and better-sqlite3 installed. Now it's time to build the actual server that handles requests.

Every web app follows the same pattern: a client sends a request, the server processes it, and sends back a response. In this step, you'll build the server that receives those requests, talks to a database, and returns data as JSON.

In this step, get ready to:
  • Create an Express server with middleware for JSON parsing and static file serving.
  • Set up a SQLite database with a table to store links.
  • Add three API routes that create, read, and delete links.
Create the Express server file

The server file is the entry point for your backend. It loads your dependencies, configures how requests are processed, and starts listening for connections.

  • In VS Code, create a new file called server.js in the root of your link-vault folder.
  • Add the following code to set up the Express app with middleware:
const express = require('express');
const Database = require('better-sqlite3');
const path = require('path');

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

// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

What does this code do?

This sets up the foundation of your server:

  • require('express') loads the Express framework. require('better-sqlite3') loads the database library. require('path') is a built-in Node.js module for working with file paths.
  • express.json() is middleware that automatically parses incoming JSON data from requests. Without it, your server can't read data sent by the frontend.
  • express.static() serves files from a public folder. This is how your HTML, CSS, and JavaScript reach the browser.

What is middleware?

Middleware is code that runs between receiving a request and sending a response. Think of it like a security checkpoint at an airport. Every request passes through middleware before reaching your route handlers.

  • Save server.js.
  • You won't see a change yet. The next chunk adds the database, and after that you'll start the server.
Set up the SQLite database

Your server needs somewhere to store links permanently. SQLite stores everything in a single file on your machine. No separate database server to install or configure.

  • In server.js, add the following code directly below the middleware lines:
// Database setup
const db = new Database('linkvault.db');
db.pragma('journal_mode = WAL');

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

What does this code do?

new Database('linkvault.db') creates (or opens) a database file called linkvault.db in your project folder.

db.pragma('journal_mode = WAL') enables Write-Ahead Logging, which makes the database faster and safer when reading and writing at the same time.

The CREATE TABLE IF NOT EXISTS statement defines the shape of your data. Each link has an auto-incrementing id, a required url and title, an optional tag, and a timestamp that fills in automatically.

  • Save server.js.
  • You won't see a change yet. The next chunk adds the API routes and starts the server.
Add the API routes and start the server

A REST API exposes specific URLs (called endpoints) that the frontend can call to create, read, or delete data. Each endpoint responds to a specific HTTP method: GET to read, POST to create, DELETE to remove.

  • In server.js, add the following code below the database setup (after the closing }); of the db.exec call):
// API Routes

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

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

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

  const stmt = db.prepare('INSERT INTO links (url, title, tag) VALUES (?, ?, ?)');
  const result = stmt.run(url, title, tag || null);

  res.status(201).json({ id: result.lastInsertRowid, url, title, tag });
});

// DELETE a link
app.delete('/api/links/:id', (req, res) => {
  const { id } = req.params;
  const stmt = db.prepare('DELETE FROM links WHERE id = ?');
  const result = stmt.run(id);

  if (result.changes === 0) {
    return res.status(404).json({ error: 'Link not found' });
  }

  res.json({ message: 'Link deleted' });
});

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

What do these routes do?

  • GET /api/links queries every row from the links table (newest first) and sends them back as a JSON array.
  • POST /api/links reads the URL, title, and tag from the request body, validates them, inserts a new row, and returns the created link with its new ID.
  • DELETE /api/links/:id removes the link with the given ID. The :id part is a route parameter. Express extracts it from the URL automatically.
  • The ? placeholders in the SQL statements are parameterized queries. They prevent SQL injection by separating data from the query itself.

Why parameterized queries instead of string concatenation?

Parameterized queries (using ? placeholders) are the standard defense against SQL injection attacks. If you built queries by pasting user input directly into the SQL string, a malicious user could manipulate your database. The better-sqlite3 library handles escaping automatically when you use ? placeholders.

  • Save server.js.
  • Start the server by running this command in your terminal:
node server.js
  • You should see this message in your terminal:
Link Vault running at http://localhost:3000
  • Open your browser and visit http://localhost:3000/api/links.
  • You should see an empty JSON array: []. This confirms your server is running, connected to the database, and responding to requests.

Server not starting?

  • If you see a syntax error, check that every opening brace { and parenthesis ( has a matching closing one. Also check that the template literal in app.listen uses backticks (`) not regular quotes.
  • If you see an error about better-sqlite3 failing to load, your macOS may be missing Xcode Command Line Tools. Run xcode-select --install in your terminal, follow the installer prompts, then run npm install better-sqlite3@12.10.0 again.
  • If you see EADDRINUSE, port 3000 is already in use. Stop whatever is using it or change the PORT value in server.js.

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

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and keep the server running in your terminal.

ⓧ I'd like to double check the full code

Here is the complete server.js file. Make sure yours matches exactly:

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

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

// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

// Database setup
const db = new Database('linkvault.db');
db.pragma('journal_mode = WAL');

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

// API Routes

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

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

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

  const stmt = db.prepare('INSERT INTO links (url, title, tag) VALUES (?, ?, ?)');
  const result = stmt.run(url, title, tag || null);

  res.status(201).json({ id: result.lastInsertRowid, url, title, tag });
});

// DELETE a link
app.delete('/api/links/:id', (req, res) => {
  const { id } = req.params;
  const stmt = db.prepare('DELETE FROM links WHERE id = ?');
  const result = stmt.run(id);

  if (result.changes === 0) {
    return res.status(404).json({ error: 'Link not found' });
  }

  res.json({ message: 'Link deleted' });
});

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

Your REST API is live and responding to requests. Next up, you'll build the frontend that talks to these endpoints and gives your Link Vault a visual interface.

Build the Frontend Interface

Your API is built and tested. It can receive requests and talk to the database. But right now, the only way to use it is by typing URLs directly into the browser.

In this step, you will build the frontend that real users interact with. The frontend sends HTTP requests to your API endpoints and displays the results as a clean web page.

In this step, get ready to:
  • Create the HTML page with a form and link display area.
  • Style the page with CSS.
  • Write JavaScript that communicates with the API to load, save, and delete links.
Create the public folder and HTML page

Express is already configured to serve static files from a folder called public. Any file you put in that folder becomes accessible at http://localhost:3000.

  • Create the public folder and the three frontend files by running these commands:
mkdir public
touch public/index.html public/style.css public/app.js
  • Confirm the files were created by running:
ls public

You should see app.js, index.html, and style.css listed in the output.

  • In VS Code, open public/index.html from the Explorer sidebar.
  • Add the following HTML to build the page structure:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Link Vault</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>Link Vault</h1>

    <form id="link-form">
      <input type="url" id="url-input" placeholder="https://example.com" required>
      <input type="text" id="title-input" placeholder="Link title" required>
      <input type="text" id="tag-input" placeholder="Tag (optional)">
      <button type="submit">Save Link</button>
    </form>

    <div id="links-container"></div>
  </div>

  <script src="app.js"></script>
</body>
</html>

What does this HTML do?

The page has two main sections. The <form> collects the URL, title, and an optional tag from the user. Each input has a unique id so JavaScript can grab the values later.

The empty <div id="links-container"> is where saved links will appear. JavaScript will fill it dynamically by fetching data from your API.

  • Save public/index.html.

File not saving?

Make sure you are editing the file inside the public folder, not in the root link-vault folder. The file path in the VS Code tab should show public/index.html.

Help me find where my file is saved.

Style the page with CSS

The HTML gives the page structure, but without styles it will look like plain unstyled text. The stylesheet makes the form and link cards look polished.

  • Open public/style.css in VS Code.
  • Add the following styles to set up the page layout, form, and link card appearance:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

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

.container {
  max-width: 700px;
  margin: 40px auto;
  padding: 0 20px;
}

h1 {
  text-align: center;
  margin-bottom: 30px;
  color: #1a1a1a;
}

#link-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 30px;
}

#link-form input {
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

#link-form button {
  padding: 12px;
  background-color: #2563eb;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
}

#link-form button:hover {
  background-color: #1d4ed8;
}

.link-card {
  background: white;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.link-info h3 {
  font-size: 16px;
  margin-bottom: 4px;
}

.link-info a {
  color: #2563eb;
  text-decoration: none;
  font-size: 13px;
  word-break: break-all;
}

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

.link-tag {
  display: inline-block;
  background: #e5e7eb;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  color: #6b7280;
  margin-top: 4px;
}

.delete-btn {
  background: #ef4444;
  color: white;
  border: none;
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

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

What do these styles do?

The .container centers the page content at a max width of 700px. The #link-form styles stack the inputs vertically with spacing, a white background, and a subtle shadow.

The .link-card class creates a row for each saved link with the title and URL on the left and a red delete button on the right. The .link-tag gets a small pill-shaped badge.

  • Save public/style.css.

Styles not applying later?

Double check that your index.html has <link rel="stylesheet" href="style.css"> in the <head>. The file name must match exactly.

Help me figure out why my CSS isn't loading.

Connect the frontend to the API with JavaScript

The HTML and CSS give you a static page. JavaScript is what makes it interactive by talking to your API using the fetch() function.

  • Open public/app.js in VS Code.
  • Add the following code to grab DOM references and define the data-fetching logic:
const linkForm = document.getElementById('link-form');
const linksContainer = document.getElementById('links-container');

// Fetch and display all links
async function loadLinks() {
  const response = await fetch('/api/links');
  const links = await response.json();
  displayLinks(links);
}

// Display links in the container
function displayLinks(links) {
  if (links.length === 0) {
    linksContainer.innerHTML = '<p style="text-align:center;color:#999;">No links saved yet. Add your first one above!</p>';
    return;
  }

  linksContainer.innerHTML = links.map(link => `
    <div class="link-card">
      <div class="link-info">
        <h3>${link.title}</h3>
        <a href="${link.url}" target="_blank">${link.url}</a>
        ${link.tag ? `<span class="link-tag">${link.tag}</span>` : ''}
      </div>
      <button class="delete-btn" data-id="${link.id}">Delete</button>
    </div>
  `).join('');
}

What does this code do?

The two const lines at the top store references to the form and the links container so you can interact with them throughout the file.

The loadLinks() function calls your GET /api/links endpoint and passes the result to displayLinks(). The displayLinks() function builds an HTML card for each link using a template literal and injects it into the page.

  • Below the displayLinks function, add the event handlers that let users save and delete links:
// Handle form submission
linkForm.addEventListener('submit', async (event) => {
  event.preventDefault();

  const url = document.getElementById('url-input').value;
  const title = document.getElementById('title-input').value;
  const tag = document.getElementById('tag-input').value;

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

  if (response.ok) {
    linkForm.reset();
    loadLinks();
  }
});

// Handle delete button clicks
linksContainer.addEventListener('click', async (event) => {
  if (event.target.classList.contains('delete-btn')) {
    const id = event.target.dataset.id;

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

    if (response.ok) {
      loadLinks();
    }
  }
});

// Load links on page load
loadLinks();

What does this code do?

The form handler listens for the submit event, grabs the input values, and sends a POST request to your API with the link data as JSON. After a successful save, it resets the form and reloads the list.

The delete handler uses event delegation. It listens for clicks anywhere in the links container, checks if the click was on a delete button, and sends a DELETE request to remove that link. The final loadLinks() call at the bottom runs when the page first loads to display any existing links.

  • Save public/app.js.

Why event delegation for delete buttons?

Delete buttons are created dynamically each time the links list re-renders. If you attached a listener to each button individually, those listeners would be lost every time the list updates. Event delegation solves this by listening on the parent container, which always exists.

  • If your server is still running from the previous step, stop it by pressing Ctrl+C in the terminal.
  • Restart the server by running:
node server.js
  • Open your browser and visit http://localhost:3000.

You should see the Link Vault page with a white form containing three input fields and a blue Save Link button. Below the form, you should see the message "No links saved yet. Add your first one above!".

  • Test the app by entering https://nextwork.org in the URL field.
  • Enter NextWork in the title field.
  • Enter learning in the tag field.
  • Click Save Link.

A link card should appear below the form showing the title, URL, a "learning" tag badge, and a red Delete button. Click Delete to confirm deletion works too.

Page showing Cannot GET / or a blank screen?

Make sure your public folder is in the root of your link-vault directory (the same level as server.js). The Express static middleware looks for the public folder relative to where server.js lives.

Also confirm you restarted the server after creating the files. Express loads the static folder path at startup.

Help me debug why my page isn't loading.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files in the public folder.

ⓧ I'd like to double check the full code

Here is the complete code for all three frontend files.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Link Vault</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>Link Vault</h1>

    <form id="link-form">
      <input type="url" id="url-input" placeholder="https://example.com" required>
      <input type="text" id="title-input" placeholder="Link title" required>
      <input type="text" id="tag-input" placeholder="Tag (optional)">
      <button type="submit">Save Link</button>
    </form>

    <div id="links-container"></div>
  </div>

  <script src="app.js"></script>
</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

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

.container {
  max-width: 700px;
  margin: 40px auto;
  padding: 0 20px;
}

h1 {
  text-align: center;
  margin-bottom: 30px;
  color: #1a1a1a;
}

#link-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 30px;
}

#link-form input {
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

#link-form button {
  padding: 12px;
  background-color: #2563eb;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
}

#link-form button:hover {
  background-color: #1d4ed8;
}

.link-card {
  background: white;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.link-info h3 {
  font-size: 16px;
  margin-bottom: 4px;
}

.link-info a {
  color: #2563eb;
  text-decoration: none;
  font-size: 13px;
  word-break: break-all;
}

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

.link-tag {
  display: inline-block;
  background: #e5e7eb;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  color: #6b7280;
  margin-top: 4px;
}

.delete-btn {
  background: #ef4444;
  color: white;
  border: none;
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.delete-btn:hover {
  background: #dc2626;
}
const linkForm = document.getElementById('link-form');
const linksContainer = document.getElementById('links-container');

// Fetch and display all links
async function loadLinks() {
  const response = await fetch('/api/links');
  const links = await response.json();
  displayLinks(links);
}

// Display links in the container
function displayLinks(links) {
  if (links.length === 0) {
    linksContainer.innerHTML = '<p style="text-align:center;color:#999;">No links saved yet. Add your first one above!</p>';
    return;
  }

  linksContainer.innerHTML = links.map(link => `
    <div class="link-card">
      <div class="link-info">
        <h3>${link.title}</h3>
        <a href="${link.url}" target="_blank">${link.url}</a>
        ${link.tag ? `<span class="link-tag">${link.tag}</span>` : ''}
      </div>
      <button class="delete-btn" data-id="${link.id}">Delete</button>
    </div>
  `).join('');
}

// Handle form submission
linkForm.addEventListener('submit', async (event) => {
  event.preventDefault();

  const url = document.getElementById('url-input').value;
  const title = document.getElementById('title-input').value;
  const tag = document.getElementById('tag-input').value;

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

  if (response.ok) {
    linkForm.reset();
    loadLinks();
  }
});

// Handle delete button clicks
linksContainer.addEventListener('click', async (event) => {
  if (event.target.classList.contains('delete-btn')) {
    const id = event.target.dataset.id;

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

    if (response.ok) {
      loadLinks();
    }
  }
});

// Load links on page load
loadLinks();

Secret mission

Add Search Filtering

Your Link Vault works great, but once you save dozens of links, finding the right one becomes a chore. In this secret mission, you'll add a real-time search feature that filters your saved links as you type. This is the same pattern used by every search bar on the web.

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:

  • The running Node.js server process on port 3000.
  • The link-vault project folder on your Desktop (includes source code, node_modules, and the linkvault.db database file).
  • Node.js installed on your machine.

Keep everything running

No action needed. Choose this if you are still actively building or plan to add more features to your Link Vault.

  • Your server continues running at http://localhost:3000 until you close the Terminal window or press Ctrl+C.
  • Your saved links persist in the linkvault.db file inside the project folder.
  • Node.js stays installed and available for future projects.

Pause - I'll come back to this later

Stop the running server to free up the port, but keep all your files so you can resume later.

  • Switch back to the Terminal where the server is running.
  • Press Ctrl+C to stop the server.
  • Your project files and database remain on your Desktop, ready for next time.
  • To resume, open Terminal, navigate to the link-vault folder, and run node server.js again.

Delete - I don't want to use this again

Remove all project files and start fresh. This permanently deletes your saved links and all source code.

  • Switch back to the Terminal where the server is running.
  • Press Ctrl+C to stop the server.
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf link-vault
  • Confirm the folder is gone by running:
ls | grep link-vault

You should see no output, confirming the folder has been deleted.

  • If you want to uninstall Node.js as well, you can remove it from your system. If you installed Node.js via the .pkg installer, you will need to manually remove it from /usr/local/. If you installed via Homebrew, run:
brew uninstall node

Most learners keep Node.js installed for future projects.

That's a Wrap!

That's a Wrap!

Nice work! You just built a full-stack web application from scratch.

You now understand the universal pattern behind every web app, from Twitter to Shopify. A frontend talks to a backend API, which talks to a database. That's it.

Here's what you accomplished:

  • Built a REST API with Express.js that handles GET, POST, and DELETE requests.
  • Created a SQLite database that persists your data between server restarts.
  • Connected a frontend to a backend using JavaScript's fetch() API to complete the full request/response cycle: browser sends an HTTP request, Express routes it, the database runs a query, and the JSON response updates the DOM.
  • Added real-time search filtering with SQL LIKE queries in the Secret Mission.

Ready to quiz yourself?