REST API with Node.js and Neon Postgres

Build a Bookmark Manager API backed by a serverless Postgres database.

Introduction

30 Second Summary

You save interesting links all the time, but they end up scattered across browser tabs, notes apps, and messages to yourself. What if you could build your own system for storing them, backed by a real cloud database?

In this project, you will build a Bookmark Manager REST API with Node.js and Express, backed by a Neon serverless Postgres database that scales to zero when you are not using it.

What You'll Build

You start your server, send a request from your terminal, and see your bookmarks returned as JSON from a cloud Postgres database you can create, update, and query through simple endpoints.

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

  • A running API where you send a POST request to add a new bookmark and see it appear instantly in your cloud database.
  • Full CRUD operations that let you create, read, update, and delete bookmarks through HTTP requests.
  • A tested schema migration where you safely add a new column using Neon's instant database branching. You verify it works on an isolated copy before touching your real data.
  • Secret Mission: Add filtered search to your API so users can find bookmarks by category or keyword.

Are there any prerequisites?

This project assumes you are comfortable with relational databases and SQL. Basic familiarity with JavaScript and Node.js is also helpful.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your chance to commit to the project before any hands-on work begins.

Set Up Your Project and Neon Database

Your Bookmark Manager API needs two things before you can write a single line of application code: a Node.js project with the right libraries installed and a live Postgres database to store bookmarks in.

In this step, you'll scaffold your project, install Express and the Neon serverless driver, then create a free Neon Postgres database and fill it with sample bookmarks to query later.

In this step, get ready to:
  • Initialize a Node.js project with Express, the Neon serverless driver, and dotenv.
  • Create a free Neon account and Postgres database.
  • Create a bookmarks table and insert sample data.
Initialize the Node.js project and install dependencies

The Neon serverless driver requires Node.js version 19 or higher. Let's confirm your version before going further.

  • Check your Node.js version by running:
node --version

✔️ I see version 19 or higher

Your Node.js version is compatible with the Neon serverless driver. Move on to the next section below.

ⓧ I see a lower version number

The Neon serverless driver requires Node.js 19 or higher. Visit the Node.js downloads page to install the latest LTS version.

After installing, run node --version again to confirm you see v19 or higher.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bookmark-manager-api
cd bookmark-manager-api
  • Initialize a new Node.js project by running:
npm init -y

You should see the contents of a new package.json file printed in your terminal.

  • Install Express, the Neon serverless driver, and dotenv by running:
npm install express @neondatabase/serverless dotenv

What are these three packages?

  • express is a web framework for Node.js that handles routing and HTTP requests.
  • @neondatabase/serverless is Neon's driver that connects to Postgres over HTTP instead of a traditional TCP connection.
  • dotenv loads environment variables from a .env file into process.env so you can keep secrets out of your code.

You should see a message like "added 3 packages" in your terminal output.

Seeing permission errors during install?

On macOS or Linux, try prefixing the command with sudo. On Windows, make sure you are running your terminal as an administrator.

help me fix npm install permission errors

Before you run the next command, what do you think will happen if the driver installed correctly?

  • Verify the Neon driver is installed by running:
node -e "require('@neondatabase/serverless')"

If no output appears and you return to your prompt, the driver is installed correctly. An error message here would mean something went wrong during install.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the bookmark-manager-api folder on your Desktop, then click Open.

Why open VS Code now?

You'll use VS Code throughout this project to create and edit your API's source files. Opening the project folder now means every new file you create lives in the right place.

  • In the Explorer sidebar on the left, hover over the file list area and click the New File icon (a page with a plus sign).
  • Type .env as the file name and press Enter.
  • Paste the following line into the file:
DATABASE_URL=your-neon-connection-string-here

What is DATABASE_URL?

DATABASE_URL is an environment variable that holds the connection string your API uses to reach the Neon database. You'll replace the placeholder value with your real connection string in the next substep.

  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
Create a Neon account and project

Now that your local project is ready, you need a cloud database to connect it to. Neon offers a free tier that includes a serverless Postgres database with no credit card required.

  • Open your browser and go to console.neon.tech.
  • Sign up for a free account using your email, GitHub, Google, or another sign-in method.
  • Once signed in, click New Project.
  • Enter a Project name (for example, bookmark-manager).
  • Leave the Postgres version and Region at their defaults.
  • Click Create Project.

Neon provisions a Postgres database automatically. You land on the Project Dashboard.

What did Neon just create?

Every Neon project starts with a default branch (called production), a database (called neondb), and a database owner role. You'll use branching later in this project to safely test schema changes.

  • On the Project Dashboard, click the Connect button to open the Connect to your database modal.
  • In the modal, find the connection string. It looks like postgresql://user:password@ep-xxxx.region.aws.neon.tech/neondb.
  • Click the copy icon next to the connection string to copy it to your clipboard.

Keep this connection string private

Your connection string contains your database password. Do not share it publicly or commit it to version control. The .env file keeps it out of your source code.

  • Switch back to VS Code where you have .env open.
  • Select the placeholder text your-neon-connection-string-here and replace it with the connection string you just copied.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

Your .env file now contains a single line: DATABASE_URL set to your real Neon connection string.

✔️ Awesome, I've got everything!

Great. Double check you've saved your .env file.

ⓧ I'd like to double check the full code

Your .env file should have exactly one line:

DATABASE_URL=your-neon-connection-string-here

The value after the = sign should be the real connection string you copied from the Neon Console (starting with postgresql://).

Connection string not showing in Neon?

Make sure you are on the Project Dashboard and have clicked the Connect button. The connection string appears in the modal that opens.

help me find my Neon connection string

Create the bookmarks table using the Neon SQL Editor

Your database exists but it is empty. You need a table to hold bookmarks and some sample data to test with.

  • In the Neon Console sidebar, expand Postgres database and click SQL Editor.
  • Make sure your default branch and the neondb database are selected at the top of the editor.
  • Clear any placeholder SQL in the editor and paste the following query:
CREATE TABLE bookmarks (
  id SERIAL PRIMARY KEY,
  url TEXT NOT NULL,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);
  • Click Run to execute the query.

What does this table look like?

  • id auto-increments with every new bookmark. You never set it manually.
  • url and title are required fields because a bookmark without a URL or name is useless.
  • description is optional for adding notes about a bookmark.
  • created_at records when the bookmark was added, defaulting to the current time.

You should see a success message confirming the table was created. Now insert two sample bookmarks.

  • Clear the SQL Editor and paste this query:
INSERT INTO bookmarks (url, title, description)
VALUES
  ('https://developer.mozilla.org', 'MDN Web Docs', 'Comprehensive web development documentation'),
  ('https://github.com', 'GitHub', 'Code hosting and version control platform');
  • Click Run to insert the rows.

Why insert sample data now?

Having real rows in the table means you can immediately test your API in the next step. You won't have to wonder whether an empty response is a bug or just an empty table.

Before you run the next query, how many rows do you expect to see?

  • Clear the SQL Editor and run:
SELECT * FROM bookmarks;

You should see two rows: one for MDN Web Docs and one for GitHub, each with an auto-generated id and created_at timestamp.

Seeing 'relation bookmarks does not exist'?

Make sure you ran the CREATE TABLE query first and that it completed successfully. Also confirm the correct branch and database (neondb) are selected in the SQL Editor dropdowns.

help me fix my bookmarks table not being found in Neon SQL Editor

Your project folder is scaffolded, your Neon database is live, and your bookmarks table has sample data waiting. Next up, you'll write the code that connects your API to this database and serves your first endpoint.

Build Your First API Endpoint and See Data

Your project folder is ready, your dependencies are installed, and your Neon database has sample bookmarks waiting to be queried. Now it's time to write the code that pulls those bookmarks out of the cloud and into your browser.

The fastest way to confirm everything is wired correctly is to read live data from the database. In this step, you'll create two files and then see your sample bookmarks returned as JSON right in the browser.

In this step, get ready to:
  • Create a database connection module that talks to your Neon Postgres database.
  • Build an Express server with a GET endpoint to retrieve bookmarks.
  • Test the endpoint and see live data from your cloud database in the browser.
Create the database connection module

Before the server can query bookmarks, it needs a way to talk to your Neon database. You'll create a small module called db.js that sets up this connection using the Neon serverless driver.

  • Create a new file called db.js in your bookmark-manager-api folder and add this code:
const { neon } = require('@neondatabase/serverless');
require('dotenv').config();

// Create a query function using the Neon connection string
const sql = neon(process.env.DATABASE_URL);

module.exports = { sql };

What does this code do?

  • neon is imported from the Neon serverless driver. This is the function that creates your database connection.
  • require('dotenv').config() loads the variables from your .env file into process.env. This is how DATABASE_URL becomes available to your code.
  • neon(process.env.DATABASE_URL) creates a query function that communicates with your Neon database over HTTP instead of a traditional TCP connection. This is what makes the Neon driver different from standard Postgres drivers.
  • module.exports = { sql } exports the query function so other files (like your server) can use it.
  • Save db.js.
  • Verify the module loads correctly by running this command:
node -e "require('./db')"

If no error prints and you return to your terminal prompt, the module loaded successfully.

Seeing an error?

  • If you see Cannot find module '@neondatabase/serverless', your dependencies may not have installed properly. Run npm install again from inside your bookmark-manager-api folder.
  • If you see Cannot find module './db', make sure db.js is saved in the bookmark-manager-api folder and that your terminal is in the same folder.

Still stuck? Ask for help here: Why is my db.js module not loading?

✔️ Awesome, I've got everything!

Great. Double check you've saved db.js and let's move on.

ⓧ I'd like to double check the full code

const { neon } = require('@neondatabase/serverless');
require('dotenv').config();

// Create a query function using the Neon connection string
const sql = neon(process.env.DATABASE_URL);

module.exports = { sql };
Create the Express server with a GET route

With the database connection ready, you need a server that listens for requests and responds with data. Express is a lightweight web framework that makes defining REST API routes straightforward.

  • Create a new file called server.js in your bookmark-manager-api folder and add this code:
const express = require('express');
const { sql } = require('./db');

const app = express();
app.use(express.json());

// GET /bookmarks - List all bookmarks
app.get('/bookmarks', async (req, res) => {
  try {
    const bookmarks = await sql`SELECT * FROM bookmarks ORDER BY created_at DESC`;
    res.json(bookmarks);
  } catch (error) {
    console.error('Error fetching bookmarks:', error);
    res.status(500).json({ error: 'Failed to fetch bookmarks' });
  }
});

What does this code do?

  • express() creates a new Express application.
  • app.use(express.json()) tells Express to automatically parse incoming JSON request bodies. You'll need this when creating bookmarks later.
  • app.get('/bookmarks', ...) defines a route that responds to GET requests at the /bookmarks path.
  • The sql tagged template sql`SELECT * FROM bookmarks ORDER BY created_at DESC` queries all bookmarks from your Neon database, sorted newest first. Tagged templates are how the Neon driver keeps queries safe from SQL injection.
  • The try/catch block handles database errors gracefully, returning a 500 status code if something goes wrong.
  • Save server.js. The server can't start yet because it doesn't have a listen block. You'll add that next.
  • Below the GET route in server.js, add this code to start the server:
// Start the server on port 3000
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

What does this code do?

  • process.env.PORT || 3000 uses port 3000 by default. If a PORT variable is set in the environment, it uses that instead.
  • app.listen() starts the server and logs the URL so you know exactly where to find it.
  • Save server.js.

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and let's test it.

ⓧ I'd like to double check the full code

const express = require('express');
const { sql } = require('./db');

const app = express();
app.use(express.json());

// GET /bookmarks - List all bookmarks
app.get('/bookmarks', async (req, res) => {
  try {
    const bookmarks = await sql`SELECT * FROM bookmarks ORDER BY created_at DESC`;
    res.json(bookmarks);
  } catch (error) {
    console.error('Error fetching bookmarks:', error);
    res.status(500).json({ error: 'Failed to fetch bookmarks' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
Test the endpoint
  • Start the server by running this command:
node server.js

You should see this message in your terminal:

Server running on http://localhost:3000

Server not starting?

  • If you see Cannot find module './db', make sure db.js is saved in the same folder as server.js.
  • If you see EADDRINUSE, another process is already using port 3000. Close any other running servers, or stop the process using that port.

Still stuck? Ask for help here: Why won't my Express server start?

Before you open the browser, take a guess. What do you think you'll see when you visit /bookmarks?

  • Open your browser and visit http://localhost:3000/bookmarks.

You should see a JSON array containing the two sample bookmarks you inserted in Step 1. Each bookmark has an id, url, title, description, and created_at timestamp.

What just happened?

Your browser sent an HTTP GET request to your local Express server. Express matched the request to your /bookmarks route.

The route used the sql function from db.js to send a query to your Neon database over HTTP. Neon executed the query and sent the results back. Express then returned those results to your browser as JSON.

Not seeing your bookmarks?

  • If you see {"error":"Failed to fetch bookmarks"}, your DATABASE_URL in .env may be incorrect. Open the Neon Console, click Connect on your Project Dashboard, and copy the connection string again.
  • If the browser shows "Cannot GET /bookmarks", make sure the route path in server.js is exactly '/bookmarks' and that you saved the file before starting the server.
  • If you see an empty array [], your bookmarks table may not have sample data. Return to the Neon SQL Editor and re-run the INSERT statements from Step 1.

Still stuck? Ask for help here: Why am I not seeing my bookmarks in the browser?

Your API is live and reading from a cloud database. Next up, you'll add the ability to create, update, and delete bookmarks so your API can do more than just list them.

Add Full CRUD Operations

Your API can read bookmarks from the database, which proves the connection between Express and Neon is working. But a read-only API is like a library where you can browse the catalogue but never borrow a book.

To make this a real bookmark manager, you need to create, update, and delete bookmarks too. In this step, you will add the remaining three CRUD operations so your API can handle the full lifecycle of a bookmark.

In this step, get ready to:
  • Add a POST route to create new bookmarks.
  • Add PUT and DELETE routes to update and remove bookmarks.
  • Test all endpoints with curl.
Add a POST route to create bookmarks

The GET route you built reads data out of the database. A POST route does the opposite: it accepts JSON data from the request body and inserts a new row into the bookmarks table.

  • Switch back to your terminal and press Ctrl+C to stop the running server.
  • In server.js, find the closing }); of the GET route, then copy the following code below it:
// POST /bookmarks - Create a new bookmark
app.post('/bookmarks', async (req, res) => {
  try {
    const { url, title, description, category } = req.body;

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

    const [bookmark] = await sql`
      INSERT INTO bookmarks (url, title, description)
      VALUES (${url}, ${title}, ${description || null})
      RETURNING *
    `;
    res.status(201).json(bookmark);
  } catch (error) {
    console.error('Error creating bookmark:', error);
    res.status(500).json({ error: 'Failed to create bookmark' });
  }
});

What does this code do?

  • req.body contains the JSON data the client sends with the request. The route pulls out url, title, and description.
  • The if (!url || !title) check rejects the request with a 400 status if either required field is missing.
  • The sql tagged template inserts the values safely. Neon's driver prevents SQL injection by default because it only accepts tagged templates, not raw string concatenation.
  • RETURNING * tells Postgres to send the full inserted row back, including the auto-generated id and created_at.
  • Save server.js.
  • Restart the server by running:
node server.js
  • Test the POST route by running this command in a new terminal window:
curl -X POST http://localhost:3000/bookmarks -H "Content-Type: application/json" -d "{\"url\":\"https://nodejs.org\",\"title\":\"Node.js\",\"description\":\"JavaScript runtime\"}"

You should see a JSON object with your new bookmark, complete with an id and created_at timestamp that Postgres generated automatically.

Seeing "Cannot POST /bookmarks"?

  • Make sure you saved server.js and restarted the server with Ctrl+C then node server.js. Node.js does not pick up file changes automatically.
  • Confirm the POST route is placed after the closing }); of the GET route, not inside it.

Help me debug my POST route

Add PUT and DELETE routes

Creating bookmarks is great, but you also need to fix typos and remove bookmarks you no longer want. The PUT route updates an existing bookmark by its id, and the DELETE route removes one.

  • Switch back to your terminal and press Ctrl+C to stop the server.
  • In server.js, find the closing }); of the POST route you just added, then copy the following code below it:
// PUT /bookmarks/:id - Update a bookmark
app.put('/bookmarks/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const { url, title, description } = req.body;

    const [bookmark] = await sql`
      UPDATE bookmarks
      SET url = ${url}, title = ${title}, description = ${description || null}
      WHERE id = ${id}
      RETURNING *
    `;

    if (!bookmark) {
      return res.status(404).json({ error: 'Bookmark not found' });
    }

    res.json(bookmark);
  } catch (error) {
    console.error('Error updating bookmark:', error);
    res.status(500).json({ error: 'Failed to update bookmark' });
  }
});

What does this code do?

  • req.params.id captures the bookmark ID from the URL. When someone sends a request to /bookmarks/3, id equals 3.
  • The UPDATE query replaces all three fields and uses RETURNING * to send the updated row back.
  • If bookmark is undefined, no row matched that ID. The route returns a 404 so the client knows the bookmark does not exist.
  • Now add the DELETE route directly below the PUT route you just pasted:
// DELETE /bookmarks/:id - Delete a bookmark
app.delete('/bookmarks/:id', async (req, res) => {
  try {
    const { id } = req.params;

    const [bookmark] = await sql`
      DELETE FROM bookmarks WHERE id = ${id} RETURNING *
    `;

    if (!bookmark) {
      return res.status(404).json({ error: 'Bookmark not found' });
    }

    res.json(bookmark);
  } catch (error) {
    console.error('Error deleting bookmark:', error);
    res.status(500).json({ error: 'Failed to delete bookmark' });
  }
});

What does this code do?

  • The DELETE query removes the row matching the given id and uses RETURNING * to send the deleted row back one last time.
  • The same 404 check protects against deleting a bookmark that does not exist.
  • Save server.js.
  • Restart the server by running:
node server.js
  • Test the PUT route by updating one of your sample bookmarks:
curl -X PUT http://localhost:3000/bookmarks/1 -H "Content-Type: application/json" -d "{\"url\":\"https://nodejs.org\",\"title\":\"Node.js Official\",\"description\":\"The official Node.js website\"}"

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

  • Test the DELETE route by removing the bookmark you created earlier in this step:
curl -X DELETE http://localhost:3000/bookmarks/3

You should see the deleted bookmark returned as JSON one final time, confirming it was removed.

Getting a 404 or empty response?

  • A 404 means no bookmark matched the ID you used. Open http://localhost:3000/bookmarks in your browser to see which IDs exist.
  • Make sure the PUT and DELETE routes are placed after the POST route and before the app.listen call at the bottom of the file.
  • Confirm you restarted the server after saving your changes.

Help me debug my PUT or DELETE route

Test all endpoints with curl

All four CRUD routes are in place. Before you move on, confirm that creating a brand new bookmark works end-to-end.

Before you run the next command, what do you think the response will contain? Will it include fields you did not send in the request?

  • Create a new bookmark by running:
curl -X POST http://localhost:3000/bookmarks -H "Content-Type: application/json" -d "{\"url\":\"https://neon.tech\",\"title\":\"Neon\",\"description\":\"Serverless Postgres\"}"

You should see the new bookmark returned as JSON with an id and created_at timestamp that Postgres generated for you. Those two fields appear even though you did not send them, because the database fills them in automatically.

  • Confirm all your bookmarks are persisted by opening http://localhost:3000/bookmarks in your browser.

You should see a JSON array with your sample bookmarks plus every bookmark you created and updated during testing.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js and that your server is still running.

ⓧ I'd like to double check the full code

Scroll up through your server.js and confirm it contains these sections in this order:

  • The require imports for express and sql from ./db.
  • app.use(express.json()) middleware.
  • GET /bookmarks route (from Step 2).
  • POST /bookmarks route with validation and INSERT.
  • PUT /bookmarks/:id route with UPDATE and 404 check.
  • DELETE /bookmarks/:id route with DELETE and 404 check.
  • app.listen on port 3000 at the bottom.

Your API now handles the full lifecycle of a bookmark. Next up, you will learn how to safely evolve your database schema using Neon's instant branching.

Safely Evolve Your Schema with Neon Branching

Your Bookmark Manager API has full CRUD. You can create, read, update, and delete bookmarks from a cloud Postgres database.

But what happens when you need to change the database schema? You want to add a category column to organize bookmarks. Running ALTER TABLE directly on your only database is risky. If the migration has an unintended side effect, your data could be corrupted.

Neon's database branching creates an instant copy-on-write clone of your database. You can test the migration on the clone first, verify it works, and then apply it to your main database with confidence.

In this step, get ready to:
  • Create a test branch of your Neon database.
  • Run and verify a schema migration on the branch.
  • Apply the migration to your main branch and update the API.
Create a branch in the Neon Console

Instead of running a migration directly on your production database, you will create an isolated branch first. A branch in Neon is a copy-on-write clone. It shares the underlying data with the parent and only stores changes as deltas.

  • Return to the Neon Console in your browser.
  • Click Branches in the left sidebar.
  • Click Create branch.
  • Enter test-migration as the branch name.
  • Select your main branch as the parent.
  • Click Create.

The branch is created instantly. All your existing data is available on the new branch without any copying because Neon uses copy-on-write storage under the hood.

Why branching instead of ALTER TABLE directly?

On a traditional Postgres setup, you would run ALTER TABLE directly on your only database. If the migration has a bug or an unintended side effect, your production data is affected.

Neon branches give you a zero-cost sandbox. You test the migration on the branch, confirm it works, and only then apply it to main. This is the workflow that makes Neon worth using over a regular Postgres setup.

Run the migration on the branch

Now you have an isolated clone of your database. Time to add the category column here first, where a mistake cannot hurt your real data.

  • Click SQL Editor in the left sidebar of the Neon Console.
  • Select the test-migration branch from the branch dropdown at the top of the editor.
  • Add a category column to the bookmarks table by running this SQL statement:
ALTER TABLE bookmarks ADD COLUMN category VARCHAR(100) DEFAULT 'general';

What does this statement do?

This adds a new category column to every row in the bookmarks table. The DEFAULT 'general' clause means all existing rows automatically get the value general for this column. Any new rows inserted without specifying a category also default to general.

  • Now tag some bookmarks with a more specific category by running this update:
UPDATE bookmarks SET category = 'development' WHERE url LIKE '%neon%' OR url LIKE '%github%';

Before you check the results, what do you expect the category column to show for each row?

  • Verify the migration by running:
SELECT * FROM bookmarks;

You should see the category column in your results. Rows with URLs containing neon or github show development. All other rows show general.

Don't see the category column?

Double check that the branch dropdown at the top of the SQL Editor says test-migration. If you accidentally ran the query on the main branch, the ALTER TABLE went to the wrong place.

help me debug my SQL migration on the Neon branch

Apply the migration to main and update the API

The migration works on the branch. Time to apply it to your main database and update your API code to use the new column.

  • In the SQL Editor, switch the branch dropdown back to your main branch.
  • Run the same migration statements by running:
ALTER TABLE bookmarks ADD COLUMN category VARCHAR(100) DEFAULT 'general';
  • Then run the update:
UPDATE bookmarks SET category = 'development' WHERE url LIKE '%neon%' OR url LIKE '%github%';
  • Confirm the migration applied to main by running:
SELECT * FROM bookmarks;

You should see the same category column with development and general values, now on your main branch.

Now update your API to accept and store the new category field. Switch back to VS Code.

  • In server.js, find the POST /bookmarks route. Locate the INSERT query inside it:
    const [bookmark] = await sql`
      INSERT INTO bookmarks (url, title, description)
      VALUES (${url}, ${title}, ${description || null})
      RETURNING *
    `;
  • Replace those lines so the query includes category:
    const [bookmark] = await sql`
      INSERT INTO bookmarks (url, title, description, category)
      VALUES (${url}, ${title}, ${description || null}, ${category || 'general'})
      RETURNING *
    `;

What changed here?

You added category to the column list and ${category || 'general'} to the values. The || operator provides a fallback so bookmarks created without a category default to general.

  • Now find the PUT /bookmarks/:id route. Locate the destructuring line:
    const { url, title, description } = req.body;
  • Replace it with:
    const { url, title, description, category } = req.body;
  • In the same PUT route, find the UPDATE query:
    const [bookmark] = await sql`
      UPDATE bookmarks
      SET url = ${url}, title = ${title}, description = ${description || null}
      WHERE id = ${id}
      RETURNING *
    `;
  • Replace it so the query includes category:
    const [bookmark] = await sql`
      UPDATE bookmarks
      SET url = ${url}, title = ${title}, description = ${description || null}, category = ${category || 'general'}
      WHERE id = ${id}
      RETURNING *
    `;
  • Save server.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js before continuing.

ⓧ I'd like to double check the full code

Your server.js is long, so scroll through and confirm each section matches. Here is what to check, in order:

  • The GET /bookmarks route is unchanged from Step 2.
  • The POST /bookmarks route destructures category from the request body. The INSERT query includes category in the column list and ${category || 'general'} in the values.
  • The PUT /bookmarks/:id route destructures category from the request body. The SET clause includes category = ${category || 'general'}.
  • The DELETE /bookmarks/:id route is unchanged from Step 3.
  • The server listens on PORT at the bottom of the file.

Now restart your server so the code changes take effect.

  • In your terminal, press Ctrl+C to stop the running server.
  • Start it again by running:
node server.js

Before you run the next command, what new field do you expect to see in the JSON response that was not there before?

  • Test creating a bookmark with a category by running this in a new terminal:
curl -X POST http://localhost:3000/bookmarks -H "Content-Type: application/json" -d "{\"url\":\"https://neon.tech/docs\",\"title\":\"Neon Docs\",\"description\":\"Neon documentation\",\"category\":\"database\"}"

You should see the new bookmark returned as JSON with a category field set to database.

Seeing an error or missing category field?

  • Make sure you stopped the old server with Ctrl+C and restarted with node server.js. Code changes are not picked up until you restart.
  • Confirm you ran the ALTER TABLE on the main branch, not just on test-migration. The server connects to main.
  • Check that your INSERT query in the POST route now lists four columns: url, title, description, category.

help me fix my bookmark category issue

One last cleanup step. The test-migration branch served its purpose. Time to delete it.

  • Return to the Neon Console and click Branches in the left sidebar.
  • Click the test-migration branch from the table to open its overview page.
  • Click the Actions menu and select Delete.
  • Click Delete in the confirmation dialog.

The branch is gone. Only your main branch remains with the fully migrated schema.

You just used Neon's branching to safely evolve your database schema. Your bookmarks now have categories, and you proved the migration worked before touching production data. Next up, an optional Secret Mission where you put those categories to work with filtered search.

Secret mission

Add Filtered Search

Your API has categories now, but there is no way to filter by them. In this mission, add query parameter support so users can find bookmarks by category, keyword, or both.

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 uses Neon's free tier, which costs $0 and scales to zero when idle.

Resources you used:

  • Neon project with a Postgres database (free tier, scales to zero after 5 minutes of inactivity).
  • Local bookmark-manager-api project folder on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project soon.

  • Your Neon database stays on the free tier and costs $0. It automatically scales to zero after 5 minutes of inactivity.
  • Your local bookmark-manager-api folder remains on your Desktop.

Pause - I'll come back to this later

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

  • Stop the local server by pressing Ctrl+C in your terminal if it is still running.
  • Neon automatically suspends your database compute after 5 minutes of inactivity. There is nothing extra to do.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your Neon database and local files.

  • Stop the local server by pressing Ctrl+C in your terminal if it is still running.
  • Open the Neon Console at console.neon.tech.
  • Select your project from the project list.
  • Click Settings in the left sidebar.
  • Click Delete project and confirm the deletion. This permanently removes your database and all its data.
  • Delete the local project folder from your Desktop.

macOS

  • Run this command to delete the project folder:
cd ~/Desktop
rm -rf bookmark-manager-api

Windows

  • Run this command to delete the project folder:
cd ~\Desktop
rmdir /s /q bookmark-manager-api

Nice Work!

Nice Work!

Nice work! You just built a fully functional Bookmark Manager API backed by a cloud Postgres database that scales to zero when you are not using it.

You've learned how to:

  • Built a Node.js REST API with Express that performs full CRUD operations against a cloud Postgres database using Neon's serverless driver, which communicates over HTTP instead of traditional TCP connections.
  • Created your database schema using the Neon Console's SQL Editor and managed your data directly from the browser.
  • Used Neon's instant database branching to safely test a schema migration on an isolated copy-on-write clone before applying it to your main branch.
  • Secret Mission: Added filtered search with dynamic query building, supporting category and keyword filters.

Ready to quiz yourself?