Build a Bookmarks REST API

Build a full CRUD REST API with Express and SQLite from scratch.

Introduction

30 Second Summary

Every day, apps save, retrieve, and delete data for you. Bookmarks, to-do items, user profiles. The code that makes this possible is called a REST API, and it's the backbone of every web application you've ever used.

In this project, you will build a Bookmarks API from scratch using Node.js, Express, and SQLite. You'll create endpoints that save, retrieve, update, and delete bookmarks, all backed by a real database running on your machine.

What is a REST API?

A REST API is a set of URLs (called endpoints) that let apps send and receive data over the internet. When you open a weather app, it sends a request to a REST API to fetch the forecast. When you save a bookmark, it sends a request to store that data.

REST uses standard HTTP methods to describe what you want to do: GET to retrieve, POST to create, PUT to update, DELETE to remove. The API you build in this project follows exactly this pattern.

What You'll Build

A fully functional Bookmarks API that handles JSON requests, validates input, persists data to a local SQLite database file, and returns proper HTTP status codes.

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

  • An Express server with five RESTful endpoints handling all CRUD operations (Create, Read, Update, Delete).
  • A SQLite database persisting bookmarks to disk with a properly designed schema and prepared statements for security.
  • A clean project structure with separated concerns (server, database, routes) that mirrors how real-world backend codebases are organized.
  • Secret Mission: Add search and filtering so users can find bookmarks by keyword.

Do I need backend experience?

No backend experience required. If you're comfortable with JavaScript and familiar with JSON from your frontend work, you have everything you need to get started.

Set Up the Project

Every backend project starts with a solid foundation. Before you write any API code, you need a proper project structure, the right dependencies installed, and a development workflow that reloads your server automatically when you make changes.

In this step, you will create the project folder, configure it for modern JavaScript, and install the two libraries that power your API. By the end, you will have everything in place to start building.

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

Your project needs its own folder with a package.json file that tells Node.js how to run your code. You will also configure the project for modern import/export syntax (the same style you already know from React).

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then type Terminal and press Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bookmarks-api
cd bookmarks-api
  • Initialize the project with npm by running:
npm init -y

What does this command do?

npm init -y generates a package.json file with default values. This file tracks your project's name, version, scripts, and dependencies.

  • Confirm package.json was created by running:
ls

You should see package.json listed in the output.

Now you need to edit package.json to enable modern JavaScript imports and add a development script.

  • Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), then type VS Code and press Enter.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the bookmarks-api folder on your Desktop, then click Open.
  • In the Explorer sidebar on the left, click package.json to open it.
  • Select all the existing content and replace it with the following code:
{
  "name": "bookmarks-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js"
  }
}

What does this code do?

"type": "module" tells Node.js to use modern import/export syntax instead of the older require() style. This matches the import syntax you already use in React.

The dev script runs your server with --watch mode, which automatically restarts the server whenever you save a file. The start script runs the server normally without watching for changes.

  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Seeing a red underline in package.json?

Make sure every string is wrapped in double quotes, including property names. JSON requires double quotes for both keys and values.

help me fix a syntax error in my package.json

Install dependencies

Your API needs two libraries. Express is the framework that handles incoming HTTP requests and sends responses. better-sqlite3 is the database library that stores your bookmarks on disk.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, below Escape).
  • Install both dependencies by running:
npm install express@5.2.1 better-sqlite3@12.11.1

What does this command do?

npm install downloads the specified packages and saves them in a node_modules folder. It also records the exact versions in your package.json under dependencies.

Pinning versions with @5.2.1 and @12.11.1 ensures your project uses the exact same library code as this tutorial.

  • Verify the installation by running:
ls node_modules

You should see a long list of folders inside node_modules. This confirms both packages installed successfully.

  • Open package.json in VS Code and confirm a dependencies section now appears listing express and better-sqlite3.

Seeing permission errors or install failures?

Make sure your terminal is inside the bookmarks-api folder. Run pwd to check your current directory.

If you see a node-gyp error, better-sqlite3 needs to compile native code. On macOS, install Xcode Command Line Tools by running xcode-select --install and try again.

help me fix an npm install error for better-sqlite3

✔️ Awesome, I've got everything!

Great. Double check you have saved package.json.

ⓧ I'd like to double check the full code

Your package.json should look like this:

{
  "name": "bookmarks-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js"
  },
  "dependencies": {
    "express": "5.2.1",
    "better-sqlite3": "12.11.1"
  }
}
Create the project file structure

A well-organized backend separates concerns into different files. You will create a file for the server, a file for the database, and a folder for your route handlers.

  • In your VS Code terminal, create the project files by running these commands:
touch server.js database.js
mkdir routes
touch routes/bookmarks.js

What is this file structure for?

  • server.js is the entry point that starts your Express server.
  • database.js handles the database connection and table creation.
  • routes/bookmarks.js contains all the endpoint handlers for your bookmarks REST API.
  • Confirm all files were created by running:
ls
ls routes

You should see server.js, database.js, and the routes folder in the first listing. The second listing should show bookmarks.js inside the routes folder.

  • Test that Node.js and the watch flag work by running:
npm run dev

Since server.js is empty, the process will complete immediately and then watch for file changes. This confirms Node.js can find your file and the dev script is configured correctly.

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

Seeing 'server.js not found'?

Make sure your terminal is inside the bookmarks-api folder where server.js lives. Run pwd to check, and ls to verify server.js is listed.

help me fix a 'file not found' error when running npm run dev

Your project foundation is solid. Next up, you will write the Express server that listens for incoming requests and sends back responses.

Create Your Express Server

Your project structure is in place with all the files and dependencies ready to go. Now it's time to bring your server to life.

An Express server is the engine that listens for incoming requests and sends back responses. In this step, you'll write the code that creates that engine, teach it to understand JSON data, and confirm it's working by hitting it with a request from your terminal.

What is Express?

Node.js can handle HTTP requests on its own, but the built-in tools are low-level. You'd have to manually parse URLs, read request bodies, and format responses yourself.

Express is a lightweight framework that sits on top of Node.js and handles all of that plumbing for you. You describe your routes ("when someone visits this URL, do this") and Express takes care of the rest. It's the most popular Node.js web framework, used by companies from startups to Netflix.

In this step, get ready to:
  • Write an Express server that parses JSON and responds to requests.
  • Start the server with auto-reload and verify it's running.
Build the basic server in server.js

Express works by creating an application instance, attaching middleware to process incoming data, and defining routes that map URLs to response logic.

  • Open server.js in your editor and add the following setup code:
import express from 'express';

// Create the Express application
const app = express();
const PORT = 3000;

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

What does this code do?

  • import express from 'express' brings in the Express framework using modern ES module syntax (the same import style you already know from React).
  • const app = express() creates your application instance. Every route and middleware you add later attaches to this object.
  • app.use(express.json()) registers middleware that automatically parses incoming JSON request bodies into JavaScript objects you can access via req.body.
  • Below the middleware line, add the health check route and the listen call:
// Health check endpoint
app.get('/', (req, res) => {
  res.json({ message: 'Bookmarks API is running' });
});

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

What does this code do?

  • app.get('/', ...) defines a route that responds to GET requests at the root URL. It sends back a JSON object confirming the API is alive.
  • app.listen(PORT, ...) starts the server on port 3000 and logs a message to your terminal when it's ready to receive requests.
  • Save server.js.

You won't see a change yet. The next substep starts the server so you can confirm it's working.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

import express from 'express';

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

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Bookmarks API is running' });
});

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

With the code saved, you can start the server and send it a request to confirm everything connects.

  • Start the server with auto-reload by running this command in your terminal:
npm run dev

What does npm run dev do?

This runs the dev script you defined in package.json, which expands to node --watch server.js. The --watch flag automatically restarts the server every time you save a change to your code.

  • You should see this output in your terminal:
Server running at http://localhost:3000
  • Open a new terminal tab by pressing Cmd+T (macOS) or Ctrl+Shift+T (Windows).
  • Send a GET request to your server by running this command:
curl http://localhost:3000
  • You should see this JSON response:
{"message":"Bookmarks API is running"}

Not seeing the expected output?

  • If you see curl: (7) Failed to connect, make sure your server is running in the other terminal tab. Check that you ran npm run dev and saw the "Server running" message.
  • If you see a syntax error, double check that server.js matches the code in the "double check" tab above. Missing commas or mismatched brackets are common culprits.
  • Make sure you're running the curl command in the NEW terminal tab, not the one running your server.

Ask for help: Why isn't my Express server responding to curl requests?

Your Express server is alive and responding to requests. Next up, you'll connect a database so your API can actually store and retrieve bookmarks.

Set Up the Database

Your server can handle requests, but it has no memory. Every time you restart it, any data would vanish into thin air.

In this step, you'll create a SQLite database that persists your bookmarks to a file on disk. You'll design the table schema and wire the database into your Express server.

In this step, get ready to:
  • Create a database connection module.
  • Design and create the bookmarks table schema.
  • Connect the database to your server.
Create the database connection in database.js

SQLite stores data in a single file on disk, so there's no database server to install or configure. The better-sqlite3 library gives you a fast, synchronous way to interact with that file from Node.js.

  • Open database.js in your editor and add the following code to create the database connection:
import Database from 'better-sqlite3';

// Create a database instance pointing to a file called bookmarks.db
const db = new Database('bookmarks.db');

What does this code do?

These two lines set up your database connection:

  • import Database from 'better-sqlite3' brings in the database library you installed earlier.
  • new Database('bookmarks.db') opens (or creates) a file called bookmarks.db in your project folder. All your bookmark data will live in this single file.

You won't see a change yet. The next substep completes this file with the table definition.

Design and create the bookmarks table

Databases need a schema that defines what data you can store. You'll define a bookmarks table with columns for each piece of information a bookmark needs.

  • Add the initializeDatabase function and the default export below your existing code in database.js:
export function initializeDatabase() {
  // Enable WAL mode 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,
      description TEXT,
      created_at TEXT DEFAULT (datetime('now'))
    )
  `);
}

export default db;

What does this code do?

The initializeDatabase function does two things when called:

  • db.pragma('journal_mode = WAL') enables Write-Ahead Logging, which makes database reads faster.
  • db.exec(...) runs the SQL statement that creates your table. The IF NOT EXISTS clause means it won't fail if the table is already there from a previous run.

Each column serves a purpose: id auto-generates a unique number for each bookmark, title and url are required fields, description is optional, and created_at auto-fills with the current timestamp.

Finally, export default db makes the database instance available to other files (your routes will need it soon).

  • Save database.js.

You won't see output from this file yet. The next substep connects it to your server.

✔️ Awesome, I've got everything!

Great. Double check you've saved database.js before moving on.

ⓧ I'd like to double check the full code

import Database from 'better-sqlite3';

const db = new Database('bookmarks.db');

export function initializeDatabase() {
  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,
      description TEXT,
      created_at TEXT DEFAULT (datetime('now'))
    )
  `);
}

export default db;
Connect the database to the server

The database module is ready, but your server doesn't know about it yet. You need to import and call initializeDatabase() so the table gets created when the server starts.

  • In server.js, find the import line at the top:
import express from 'express';
  • Add the database import on the line directly below it:
import { initializeDatabase } from './database.js';
  • Now find the app.use(express.json()) line and add the following on a new line below it:
initializeDatabase();

Why call it before the routes?

Calling initializeDatabase() before your routes ensures the database table exists before any request tries to read from or write to it. If a request arrived before the table was created, it would crash.

  • Save server.js. Your server will auto-restart thanks to node --watch.
  • Confirm the database file was created by running this in your second terminal tab:
ls

You should see bookmarks.db listed alongside your other project files like server.js, database.js, and package.json.

Don't see bookmarks.db?

  • Check that your server is still running in the first terminal tab. If it crashed, look at the error message for a typo in database.js or the import path in server.js.
  • Make sure the import path is './database.js' with the ./ prefix. Without it, Node.js looks in node_modules instead of your project folder.
  • Verify you're running ls from inside the bookmarks-api folder, not a parent directory.

Help me figure out why bookmarks.db isn't appearing after I start the server.

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and confirmed bookmarks.db exists.

ⓧ I'd like to double check the full code

import express from 'express';
import { initializeDatabase } from './database.js';

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

app.use(express.json());

initializeDatabase();

app.get('/', (req, res) => {
  res.json({ message: 'Bookmarks API is running' });
});

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

Your database is live and your table is ready to store bookmarks. Next up, you'll build the routes that let you create, read, update, and delete bookmarks through HTTP requests.

Build the CRUD Routes

Your database is ready and waiting for data. Now you need to build the endpoints that let clients create, read, update, and delete bookmarks in that database.

Each endpoint corresponds to an HTTP method (GET, POST, PUT, DELETE) and handles a specific operation on the bookmarks resource. Together, these four operations form what developers call CRUD. By the end of this step, you'll have a fully working API you can talk to from the terminal.

What is CRUD?

CRUD stands for Create, Read, Update, Delete. These are the four basic operations almost every app performs on its data. Think about a notes app: you create a note, read it back, update its content, and delete it when you're done.

In a REST API, each CRUD operation maps to an HTTP method. POST creates, GET reads, PUT updates, and DELETE removes. Once you understand this pattern, you can build the backend for any data-driven app.

In this step, get ready to:
  • Create Read endpoints that retrieve bookmarks from the database.
  • Create endpoints for writing data (create, update, delete).
  • Mount the router on the server and test all five endpoints with curl.
Create the router and implement Read endpoints

An Express Router is a mini-app that groups related routes together. You'll define all bookmark routes in their own file, then attach the whole group to the main server later.

  • Open routes/bookmarks.js in your editor and add the following code to set up the router and both Read endpoints:
import { Router } from 'express';
import db from '../database.js';

const router = Router();

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

// GET single bookmark by ID
router.get('/:id', (req, res) => {
  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(req.params.id);
  if (!bookmark) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }
  res.json(bookmark);
});

What does this code do?

The imports bring in Express's Router class and the database connection you created in the last step.

  • GET / uses a prepared statement to fetch every bookmark, ordered newest first. The .all() method returns an array of row objects.
  • GET /:id uses .get() to fetch a single row by its ID. If no row exists, .get() returns undefined, so the if (!bookmark) check triggers a 404 response.
  • The ? in the SQL query is a parameter placeholder. better-sqlite3 substitutes the actual value safely, which protects against SQL injection attacks.
  • Save routes/bookmarks.js.
  • You won't see a change yet. These routes activate once you mount the router on the server in a later substep.

Seeing a red squiggly on the import?

If your editor shows an error on import db from '../database.js', confirm that your package.json includes "type": "module". Without this, Node.js defaults to CommonJS and doesn't understand import syntax.

help me fix an import error in my Express routes file

Implement Create, Update, and Delete endpoints

The Read endpoints retrieve data without changing anything. The remaining three endpoints modify the database. Each one validates the incoming request before touching any data.

  • In routes/bookmarks.js, add the POST endpoint below your GET routes:
// POST create a new bookmark
router.post('/', (req, res) => {
  const { title, url, description } = req.body;

  if (!title || typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
  }

  if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
    return res.status(400).json({ error: 'A valid URL starting with http:// or https:// is required' });
  }

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

  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(result.lastInsertRowid);
  res.status(201).json(bookmark);
});

What does this code do?

The POST handler creates a new bookmark. It follows a validate-then-write pattern:

  • First, it extracts title, url, and description from the JSON request body.
  • If title is missing or empty, or url doesn't start with http:// or https://, the handler returns a 400 status immediately.
  • After inserting, it uses result.lastInsertRowid to fetch the newly created row and return it with a 201 (Created) status.
  • Save routes/bookmarks.js.
  • You won't see a change yet. Two more endpoints to go before testing.

Unsure where to paste?

Place this code directly below the closing }); of your GET /:id route. Keep one blank line between route handlers for readability.

help me figure out where to add my POST route in routes/bookmarks.js

  • Below the POST handler, add the PUT and DELETE endpoints along with the router export:
// PUT update an existing bookmark
router.put('/:id', (req, res) => {
  const { id } = req.params;
  const { title, url, description } = req.body;

  const existing = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id);
  if (!existing) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }

  if (!title || typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
  }

  if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
    return res.status(400).json({ error: 'A valid URL starting with http:// or https:// is required' });
  }

  const stmt = db.prepare('UPDATE bookmarks SET title = ?, url = ?, description = ? WHERE id = ?');
  stmt.run(title.trim(), url, description || null, id);

  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id);
  res.json(bookmark);
});

// DELETE a bookmark
router.delete('/:id', (req, res) => {
  const { id } = req.params;

  const existing = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id);
  if (!existing) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }

  db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id);
  res.status(204).send();
});

export default router;

What does this code do?

Both PUT and DELETE follow the same safety pattern: look up the existing bookmark first, and return 404 if it doesn't exist.

  • PUT replaces the bookmark's fields with new values after validating them (same checks as POST). It returns the updated bookmark.
  • DELETE removes the row and returns status 204 (No Content), which means "success, but nothing to send back."
  • export default router makes this router available for other files to import.
  • Save routes/bookmarks.js.
  • You'll test all five endpoints in the next substep after mounting the router on the server.

Seeing a syntax error?

Make sure every }); closes its matching router.put( or router.delete(. A missing bracket or parenthesis is the most common cause of syntax errors in route files.

help me fix a syntax error in my Express routes file

✔️ Awesome, I've got everything!

Great. Double check you've saved routes/bookmarks.js before moving on.

ⓧ I'd like to double check the full code

import { Router } from 'express';
import db from '../database.js';

const router = Router();

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

// GET single bookmark by ID
router.get('/:id', (req, res) => {
  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(req.params.id);
  if (!bookmark) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }
  res.json(bookmark);
});

// POST create a new bookmark
router.post('/', (req, res) => {
  const { title, url, description } = req.body;

  if (!title || typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
  }

  if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
    return res.status(400).json({ error: 'A valid URL starting with http:// or https:// is required' });
  }

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

  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(result.lastInsertRowid);
  res.status(201).json(bookmark);
});

// PUT update an existing bookmark
router.put('/:id', (req, res) => {
  const { id } = req.params;
  const { title, url, description } = req.body;

  const existing = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id);
  if (!existing) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }

  if (!title || typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
  }

  if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
    return res.status(400).json({ error: 'A valid URL starting with http:// or https:// is required' });
  }

  const stmt = db.prepare('UPDATE bookmarks SET title = ?, url = ?, description = ? WHERE id = ?');
  stmt.run(title.trim(), url, description || null, id);

  const bookmark = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id);
  res.json(bookmark);
});

// DELETE a bookmark
router.delete('/:id', (req, res) => {
  const { id } = req.params;

  const existing = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id);
  if (!existing) {
    return res.status(404).json({ error: 'Bookmark not found' });
  }

  db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id);
  res.status(204).send();
});

export default router;
Mount the router and test all endpoints

The routes file is self-contained, but the server doesn't know about it yet. Mounting connects your router to the main Express app at a specific URL prefix.

  • In server.js, find this line near the top:
import { initializeDatabase } from './database.js';
  • Add this import directly below it:
import bookmarksRouter from './routes/bookmarks.js';
  • Now find your health check route (the app.get('/', ...) block). Add this line below it to mount the bookmarks router:
app.use('/api/bookmarks', bookmarksRouter);

What does mounting do?

When you mount a router at /api/bookmarks, Express prefixes that path onto every route defined inside the router. Your router's GET / becomes GET /api/bookmarks, and GET /:id becomes GET /api/bookmarks/:id.

This keeps your main server file clean. All bookmark logic lives in one dedicated file.

  • Save server.js. Your server restarts automatically thanks to node --watch.
  • In your second terminal tab, test the API by creating a bookmark:
curl -X POST http://localhost:3000/api/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"title": "Express Docs", "url": "https://expressjs.com", "description": "Official Express documentation"}'

You should see a JSON response with your new bookmark, including an id and created_at timestamp. The status code is 201 (Created).

  • Retrieve all bookmarks by running:
curl http://localhost:3000/api/bookmarks

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

  • Retrieve a single bookmark by its ID (use 1 since it's your first entry):
curl http://localhost:3000/api/bookmarks/1
  • Update the bookmark by running:
curl -X PUT http://localhost:3000/api/bookmarks/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Express 5 Docs", "url": "https://expressjs.com", "description": "Updated Express documentation"}'

You should see the updated bookmark with the new title and description.

  • Delete the bookmark by running:
curl -X DELETE http://localhost:3000/api/bookmarks/1

You won't see any response body. A 204 status means the deletion succeeded with nothing to return.

  • Confirm the deletion by fetching all bookmarks again:
curl http://localhost:3000/api/bookmarks

You should see an empty array [], confirming the bookmark was removed.

Getting "Cannot GET /api/bookmarks"?

This means the router isn't mounted. Check that server.js has both the import bookmarksRouter line and the app.use('/api/bookmarks', bookmarksRouter) line. Make sure the app.use line comes after initializeDatabase().

Also confirm your server restarted. Look in the terminal running npm run dev for the Server running at http://localhost:3000 message after saving.

help me debug why my Express router isn't responding to requests

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js before moving on.

ⓧ I'd like to double check the full code

import express from 'express';
import { initializeDatabase } from './database.js';
import bookmarksRouter from './routes/bookmarks.js';

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

app.use(express.json());

initializeDatabase();

app.get('/', (req, res) => {
  res.json({ message: 'Bookmarks API is running' });
});

app.use('/api/bookmarks', bookmarksRouter);

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

All five endpoints are working. Next up, you'll add proper error handling and input validation so the API responds gracefully when something goes wrong.

Add Error Handling and Input Validation

Your CRUD endpoints work, but what happens when a client sends garbage data or something unexpected crashes? A production API needs to reject bad input gracefully and recover from surprise errors without exposing internal details.

In this step, you will review the input validation already in your routes, add a global error-handling middleware as a safety net, and test that your API responds with the correct HTTP status codes for every failure case.

In this step, get ready to:
  • Confirm input validation on POST and PUT routes.
  • Add a global error handler middleware to catch unexpected crashes.
  • Test error scenarios with curl to verify correct status codes.
Confirm input validation on your routes

Your POST and PUT routes already check incoming data before writing to the database. Let's walk through the validation patterns you have in place.

  • In your code editor, open routes/bookmarks.js and find the POST route (router.post('/', ...)).
  • Locate the two if statements near the top of the handler:
if (!title || typeof title !== 'string' || title.trim() === '') {
  return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
}

if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
  return res.status(400).json({ error: 'A valid URL starting with http:// or https:// is required' });
}

What does this validation do?

  • The title check rejects missing values, non-string types, and strings that are only whitespace. This covers three common forms of bad input in one guard.
  • The URL check ensures the value starts with http:// or https://. Without this, a client could store anything (an empty string, a JavaScript snippet, a file path) as a bookmark URL.
  • Both return a 400 status code, which tells the client the request was malformed. The descriptive error message helps developers debug their requests.

The PUT route uses the same validation logic. Both routes follow the same pattern: validate first, then write to the database.

Why validate on the server?

Even if a frontend form validates data before sending it, the API must validate independently. Anyone can send a request directly with curl or a script, bypassing any client-side checks.

Server-side validation is the last line of defence before data hits your database.

Add a global error handler

Your route-level validation catches known bad input. But what about unexpected crashes, like a database failure or a bug in your code?

Express uses a special middleware pattern for catching errors. If a middleware function has exactly four parameters, Express treats it as an error handler.

  • In server.js, find this line:
app.use('/api/bookmarks', bookmarksRouter);
  • Add the global error handler directly below it, before the app.listen call:
// Global error handler - catches unexpected errors
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

What does this code do?

  • The four-parameter signature (err, req, res, next) is how Express recognizes this as an error handler. All four parameters must be present, even if you do not use next.
  • console.error(err.stack) logs the full stack trace to your terminal so you can debug the issue.
  • The response sends a generic Internal server error message to the client. You never want to expose stack traces or internal details to external users.
  • Placement matters. This must come after all routes so it can catch errors from any of them.
  • Save server.js.
  • Your server auto-restarts thanks to node --watch. Check the terminal where your server is running to confirm it restarted without errors.

Server crashed on restart?

  • Make sure you placed the error handler after app.use('/api/bookmarks', bookmarksRouter) but before app.listen(...).
  • Check that the function has exactly four parameters: err, req, res, next. Missing one causes Express to treat it as regular middleware.

Help me debug my Express error handler placement.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js and it restarted cleanly.

ⓧ I'd like to double check the full code

import express from 'express';
import { initializeDatabase } from './database.js';
import bookmarksRouter from './routes/bookmarks.js';

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

app.use(express.json());

initializeDatabase();

app.get('/', (req, res) => {
  res.json({ message: 'Bookmarks API is running' });
});

app.use('/api/bookmarks', bookmarksRouter);

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});
Test error scenarios with curl

Now let's confirm that your API rejects bad data with the correct status codes and helpful error messages.

  • In your second terminal tab, test a POST request with a missing title by running:
curl -s -X POST http://localhost:3000/api/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
  • You should see a 400 response:
{"error":"Title is required and must be a non-empty string"}
  • Test a POST with an invalid URL by running:
curl -s -X POST http://localhost:3000/api/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"title": "Bad URL", "url": "not-a-url"}'
  • You should see:
{"error":"A valid URL starting with http:// or https:// is required"}
  • Test requesting a bookmark ID that does not exist by running:
curl -s http://localhost:3000/api/bookmarks/9999
  • You should see a 404 response:
{"error":"Bookmark not found"}

Why do status codes matter?

HTTP status codes are how APIs communicate success and failure to clients. A 400 means the client sent bad data. A 404 means the resource does not exist. A 500 means something went wrong on the server.

Frontend code uses these codes to decide what to show the user. A well-designed API makes frontend error handling straightforward.

🙋‍♀️ Not seeing the expected error message?

  • Make sure your server is still running. Check the terminal tab where npm run dev is active.
  • Verify the Content-Type: application/json header is included in your POST commands. Without it, Express cannot parse the JSON body.
  • Check that your JSON in the -d flag uses double quotes around keys and values, wrapped in single quotes on the outside.

Help me troubleshoot my curl error testing.

Your API now gracefully handles bad input and unexpected errors. Next up, you will take on the Secret Mission and add search and filtering to make your bookmarks truly useful.

Secret mission

Add Search and Filtering

Your API can create, read, update, and delete bookmarks. But what happens when a user has hundreds of bookmarks and needs to find one quickly? Add search and filtering to make your API genuinely useful.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • The bookmarks-api project folder on your Desktop (includes node_modules/, source files, and bookmarks.db).
  • The running Node.js development server on port 3000.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with your API.

  • Your server continues running in the terminal where you started it.
  • Your bookmarks.db file retains all bookmarks you created during testing.
  • You can restart the server any time by running npm run dev from inside the bookmarks-api folder.

Pause - I'll come back to this later

Stop the running server to free up port 3000 and terminal resources. Your project files stay on disk.

  • Switch to the terminal where your server is running.
  • Press Ctrl+C to stop the server.
  • When you are ready to resume, open a terminal in the bookmarks-api folder and run npm run dev again.

Delete - I don't want to use this again

Remove all project files and stop the server. This cannot be undone.

  • Switch to the terminal where your server is running and press Ctrl+C to stop it.
  • Delete the entire project folder by running the following command.

macOS

cd ~/Desktop
rm -rf bookmarks-api

Windows

cd ~\Desktop
rmdir /s /q bookmarks-api
  • Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see bookmarks-api listed.

Nice Work!

Nice Work!

Nice work! You just built a fully functional REST API from scratch that handles Create, Read, Update, and Delete operations on a bookmarks resource, all backed by a real database.

You've learned how to:

  • Build a REST API from scratch with Express handling five endpoints across all HTTP methods (GET, POST, PUT, DELETE).
  • Create and query a SQLite database using prepared statements for built-in SQL injection protection.
  • Structure a backend project with separated concerns (server, database, routes) and implement input validation that returns meaningful HTTP status codes.
  • Secret Mission: Added search and sort filtering using SQL queries and URL query parameters.

Ready to quiz yourself?