Employee Management System with Neon

Build a REST API with Express and a cloud Postgres database on Neon.

Introduction

30 Second Summary

Every company needs a way to keep track of its people. Whether it's a startup with five employees or a team of hundreds, someone has to add new hires, update roles, and remove people who leave.

In this project, you will build an Employee Management System powered by Node.js and a cloud database. You'll create a REST API and a browser dashboard that lets you add, edit, and delete employees in real time.

What You'll Build

When you're done, you'll type an employee's name and department into a form, hit a button, and watch them appear in a live table powered by your own cloud database.

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

  • A REST API powered by Express.js with endpoints for creating, listing, updating, and deleting employees.
  • A cloud-hosted Postgres database on Neon storing your employee records.
  • A browser dashboard that talks to your API and lets you manage employees from the browser.
  • Secret Mission: Add a search bar that filters employees by name or department in real time.

Are there any prerequisites?

No prior experience is required. This project walks you through setting up every tool from scratch.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your growth as a software engineer.

Set Up Your Tools and Neon Database

Your employee management system needs two foundations: a runtime to execute your code and a database to store your data.

Node.js handles the runtime, letting you run JavaScript on your machine. Neon gives you a cloud-hosted Postgres database to store employee records. Without both in place, there is nothing to build on.

In this step, get ready to:
  • Install and verify Node.js on your machine.
  • Create a Neon project and grab your database connection string.
  • Build an employees table in your cloud database.
Check your Node.js installation

Node.js lets you run JavaScript outside the browser. Your backend server will run on it starting in the next step.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check your Node.js version by running this command:
node -v

✔️ I see version 24 or higher

Node.js is ready. Now confirm that npm (Node's package manager) works too.

  • Verify npm by running this command:
npm -v

You should see a version number print out. You are all set for the next substep.

ⓧ I see an older version

You have an older version of Node.js installed. This project needs v24 (the current LTS release).

  • Download the LTS installer from the official Node.js download page.
  • Run the installer. It will replace your older version automatically.
  • Close your terminal completely and reopen it so it picks up the new version.
  • Run node -v again. You should now see v24 or higher.
  • Run npm -v to confirm npm is working too.

ⓧ Command not found

Node.js is not installed yet. Let's fix that.

  • Download the LTS installer from the official Node.js download page. The page auto-detects your operating system.
  • Run the installer and accept the default settings.
  • Close your terminal completely and reopen it so it picks up the new installation.
  • Run node -v again. You should now see v24 or higher.
  • Run npm -v to confirm npm is working too.

Still not seeing the right version?

Make sure you fully closed and reopened your terminal after installing. Some terminals cache the old path and will not pick up the new Node.js until restarted.

help me troubleshoot my Node.js installation

Create your Neon project

Neon hosts your Postgres database in the cloud. Every Neon project comes with a connection string your code uses to talk to the database.

  • Sign into the Neon Console.
  • Click New Project.
  • Enter employee-manager as the project name.
  • Leave the other settings at their defaults and click Create Project.

Neon creates your project and drops you on the Project Dashboard. Now grab the connection string your code will use.

  • Click Connect on the Project Dashboard to open the connection modal.
  • Leave the default branch, database, and role selected.
  • Copy the connection string. It starts with postgresql://.
  • Paste it into a temporary note or text file on your machine. You will use it in the next step.

What is a connection string?

A connection string is a single URL that contains everything your code needs to find and authenticate with your database: the host address, port, database name, username, and password. It looks something like postgresql://user:pass@host/neondb.

Treat it like a password. Never share it publicly or commit it to version control.

Can't find the Connect button?

Make sure you are on the Project Dashboard for your employee-manager project. The Connect button appears near the top of the dashboard.

help me find my Neon connection string

Create the employees table

Your database exists but it is empty. You need a table to hold employee records before your API can read or write any data.

  • In the Neon Console sidebar, expand Postgres database and click SQL Editor.
  • Clear any placeholder SQL in the editor.
  • Create the employees table by pasting the following SQL and clicking Run:
-- Create the employees table with three columns
CREATE TABLE employees (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT NOT NULL
);

What does this SQL do?

This creates a table called employees with three columns:

  • id is an auto-incrementing number that gives each employee a unique identifier.
  • name stores the employee's name as text.
  • department stores which department they belong to.

NOT NULL means every employee must have a name and department. The database will reject any row that leaves these blank.

You should see a success message confirming the table was created. Now verify the table exists.

Before you run the next query, what do you think the result will look like for a brand-new table with no data?

  • Clear the SQL Editor and run this query to verify your table:
SELECT * FROM employees;

You should see an empty result set with three column headers: id, name, and department. No rows appear because you have not added any employees yet.

Seeing an error when running the SQL?

Check that the default database neondb is selected at the top of the SQL Editor. If a different database is selected, your table may have been created in the wrong place.

Make sure you copied the entire SQL statement including the closing ); at the end.

help me fix my SQL query in the Neon Console

Your tools are ready and your database is waiting for data. Next up, you will build your first API endpoint to connect to Neon and read from this table.

Build Your First API Endpoint

Your tools are installed and your Neon database has an empty employees table waiting for data. Now you need a way for your future dashboard to talk to that database.

An API endpoint is the door between your frontend and your database. In this step, you will create a Node.js project, connect it to your Neon database with Express.js, and build a route that reads from the employees table.

In this step, get ready to:
  • Initialize a Node.js project and install dependencies.
  • Create an Express server connected to your Neon database.
  • Test your first API endpoint in the browser.
Set up the project folder and dependencies

Every Node.js project starts with a folder and a package.json file that tracks your dependencies. You will also create a .env file to securely store your Neon connection string.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir employee-manager
cd employee-manager
  • Initialize a package.json file by running:
npm init -y

You should see a JSON summary printed to your terminal. This confirms your package.json was created with default values.

  • Install the three packages your project needs by running:
npm install express pg dotenv

You should see output confirming packages were added. This creates a node_modules folder in your project.

What are these packages?

  • express is a web framework for building APIs and handling HTTP requests.
  • pg is a PostgreSQL client that lets your code talk to your Neon database.
  • dotenv loads environment variables from a .env file so you can keep secrets out of your source code.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the employee-manager folder, then click Open.

Trust the authors

VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.

  • In the VS Code Explorer sidebar on the left, hover over the EMPLOYEE-MANAGER heading and click the new file icon.
  • Name the file .env and press Enter.
  • Paste the following into your new .env file, replacing your-neon-connection-string-here with the connection string you saved from the Neon Console:
DATABASE_URL=your-neon-connection-string-here

Why use a .env file?

Your Neon connection string contains your database password. Storing it in a .env file keeps it out of your source code. The dotenv package loads this file automatically so your code can access the value through process.env.DATABASE_URL.

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

Not sure where to find your connection string?

Return to the Neon Console, open your employee-manager project, and click Connect on the Project Dashboard. Copy the connection string from the modal. It starts with postgresql://.

Help me find my Neon connection string.

Create the Express server

Your project folder is ready with all its dependencies. Now you will create the server file that connects to your Neon database and listens for incoming requests.

  • In the VS Code Explorer sidebar, hover over the EMPLOYEE-MANAGER heading and click the new file icon.
  • Name the file server.js and press Enter.
  • Set up the server skeleton by pasting the following code into server.js:
const express = require('express');
const { Pool } = require('pg');
require('dotenv').config();

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

app.use(express.json());

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false,
  },
});

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

What does this code do?

  • require('express') imports the Express framework. require('pg') imports the PostgreSQL client. require('dotenv').config() loads your .env file into process.env.
  • express.json() is middleware that tells Express to parse incoming JSON request bodies automatically.
  • new Pool(...) creates a connection pool to your Neon database. The pool reuses connections instead of opening a new one for every request.
  • app.listen(port, ...) starts the server and prints a message confirming it is running on port 3000.
  • Save server.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the VS Code integrated terminal by pressing Ctrl+` (backtick key, below Escape) on both macOS and Windows.
  • Start the server by running:
node server.js

You should see Server running at http://localhost:3000 printed in the terminal. Your server is alive, but it has no routes yet.

Seeing an error instead?

  • If you see a connection error, double check that the value in your .env file is the full connection string from the Neon Console, starting with postgresql://.
  • If you see Cannot find module 'express', make sure you ran npm install express pg dotenv inside the employee-manager folder.
  • If you see SyntaxError, compare your code carefully against the snippet above. A missing comma or bracket is the most common cause.

Help me debug my server.js startup error.

Now you will add the route that reads employees from your database. This is the endpoint your dashboard will call later to load the employee list.

  • Stop the server by pressing Ctrl+C in the terminal.
  • In server.js, find the app.listen block at the bottom of the file. Add the following code directly above it:
// Get all employees
app.get('/employees', async (req, res) => {
  try {
    const result = await pool.query('SELECT * FROM employees ORDER BY id ASC');
    res.json(result.rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to fetch employees' });
  }
});

What does this route do?

app.get('/employees', ...) registers a GET route at the path /employees. When someone visits that URL, Express runs the function you pass in.

Inside the function, pool.query(...) sends a SQL query to your Neon database. res.json(result.rows) sends the rows back to the browser as JSON. The try/catch block catches any database errors and returns a 500 status instead of crashing.

  • Save server.js.

✔️ Awesome, I've got everything!

Double check you saved server.js with the new route above the app.listen block.

ⓧ I'd like to double check the full code

Your server.js should look exactly like this:

const express = require('express');
const { Pool } = require('pg');
require('dotenv').config();

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

app.use(express.json());

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false,
  },
});

// Get all employees
app.get('/employees', async (req, res) => {
  try {
    const result = await pool.query('SELECT * FROM employees ORDER BY id ASC');
    res.json(result.rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to fetch employees' });
  }
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Test the endpoint in your browser

Your server now has a route that queries the employees table. Time to prove it works.

  • Restart the server by running:
node server.js

You should see Server running at http://localhost:3000 again.

Before you open the endpoint, predict what you will see. Your employees table is empty. What should the GET /employees route return for an empty table?

  • Open your browser and navigate to:
http://localhost:3000/employees

You should see [] in the browser. That is an empty JSON array. Your API is connected to Neon, queried the employees table, and returned zero rows.

Not seeing [] in the browser?

  • Make sure you stopped the old server with Ctrl+C before running node server.js again. If the old server is still running, port 3000 is already in use.
  • If you see Cannot GET /employees, make sure you added the app.get('/employees', ...) block above app.listen, not below it.
  • If you see a connection error in the terminal, check that your .env file has the correct DATABASE_URL value from the Neon Console.

Help me troubleshoot my GET /employees endpoint.

Your API is alive and connected to Neon. Next up, you will add endpoints to create and delete employees so your management system can actually manage data.

Add Create and Delete Endpoints

Your API is alive. In the last step, you hit GET /employees and saw an empty JSON array come back from Neon, proving the connection works end to end.

A read-only API can only show data. To have a working employee management system, you need routes that create records and remove them too. These two routes complete the core CRUD loop (Create, Read, Update, Delete).

In this step, get ready to:
  • Add a POST /employees route that creates new employees in the database.
  • Add a DELETE /employees/:id route that removes employees by ID.
  • Test both routes using curl commands from the terminal.
Add a POST route for creating employees

The POST route will read a name and department from the request body, insert them into the database with a parameterized SQL query, and return the new employee as JSON.

  • In server.js, find the closing }); of your GET /employees route.
  • Copy the following code below it, before the app.listen block:
// Create a new employee
app.post('/employees', async (req, res) => {
  try {
    const { name, department } = req.body;
    const result = await pool.query(
      'INSERT INTO employees (name, department) VALUES ($1, $2) RETURNING *',
      [name, department]
    );
    res.status(201).json(result.rows[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to create employee' });
  }
});

What does this code do?

  • req.body contains the JSON data sent by the client. The express.json() middleware you added in the previous step parses it automatically.
  • $1 and $2 are parameterized placeholders that prevent SQL injection. The actual values come from the array [name, department].
  • RETURNING * tells Postgres to send back the newly created row so you can return it to the client.
  • res.status(201) sends a 201 Created status code, which signals that a new resource was successfully created.
  • Save server.js.
  • Switch to the terminal where your server is running.
  • Press Ctrl+C to stop the server.
  • Restart the server by running:
node server.js
  • Open a second terminal window to run your test commands.
  • Test the POST route by creating your first employee. Run this command:

macOS

curl -X POST http://localhost:3000/employees -H "Content-Type: application/json" -d '{"name":"Alice Smith","department":"Engineering"}'

Windows

In PowerShell, use curl.exe (not curl) to call the real curl utility. PowerShell aliases curl to a different command.

curl.exe -X POST http://localhost:3000/employees -H "Content-Type: application/json" -d '{"name":"Alice Smith","department":"Engineering"}'

You should see a JSON response containing your new employee with an auto-generated id, the name you sent, and the department.

Not seeing the employee JSON?

  • Make sure you stopped the old server with Ctrl+C and restarted it after saving your changes.
  • Check that the new route is placed after the closing }); of the GET route and before the app.listen block.
  • On Windows, use curl.exe instead of curl in PowerShell.

Still stuck? Ask for help.

Add a DELETE route for removing employees

Creating employees is only half the story. You also need a way to remove them from the database by their ID.

  • In server.js, find the closing }); of the POST route you just added.
  • Copy the following code below it, before the app.listen block:
// Delete an employee
app.delete('/employees/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const result = await pool.query(
      'DELETE FROM employees WHERE id = $1 RETURNING *',
      [id]
    );
    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'Employee not found' });
    }
    res.json({ message: `Employee ${result.rows[0].name} deleted` });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to delete employee' });
  }
});

What does this code do?

  • :id in the URL path is a route parameter. Express.js stores it in req.params.id so you can use it in your query.
  • The query deletes the matching row and uses RETURNING * to get the deleted employee's data back.
  • If no row matches (the result array is empty), the route returns a 404 status with an error message.
  • The success response includes the deleted employee's name in a confirmation message.
  • Save server.js.
  • Stop the server with Ctrl+C and restart it by running:
node server.js
  • In your second terminal, delete the employee you created earlier by running:

macOS

curl -X DELETE http://localhost:3000/employees/1

Windows

curl.exe -X DELETE http://localhost:3000/employees/1

You should see a confirmation message like {"message":"Employee Alice Smith deleted"}.

Seeing "Employee not found"?

  • The ID in your DELETE URL must match the id that was returned when you created the employee. Check the JSON output from your earlier POST command.
  • If you already deleted that employee, create a new one first with the POST command, then delete the new ID.

Need more help? Ask here.

Test the full create-and-delete flow

Time for a proper end-to-end test. You will create two employees, then confirm they show up in your database through the GET route.

  • Create two employees by running these commands one at a time:

macOS

curl -X POST http://localhost:3000/employees -H "Content-Type: application/json" -d '{"name":"Bob Lee","department":"Marketing"}'
curl -X POST http://localhost:3000/employees -H "Content-Type: application/json" -d '{"name":"Carol Diaz","department":"Design"}'

Windows

curl.exe -X POST http://localhost:3000/employees -H "Content-Type: application/json" -d '{"name":"Bob Lee","department":"Marketing"}'
curl.exe -X POST http://localhost:3000/employees -H "Content-Type: application/json" -d '{"name":"Carol Diaz","department":"Design"}'

Before you check your browser, how many employees do you think GET /employees will return? Remember you created one earlier and then deleted it.

  • Open http://localhost:3000/employees in your browser.

You should see a JSON array with two employees: Bob Lee and Carol Diaz. The first employee (Alice Smith) is gone because you deleted her in the previous test.

Why is the data still there after restarting?

Your data lives in the Neon cloud database, not in your local server's memory. Stopping and restarting server.js has no effect on the stored records. That is the whole point of using a database.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Scroll up and confirm that your server.js has these sections in order:

  • The require imports at the top (express, pg, dotenv).
  • The app and pool setup, including app.use(express.json()).
  • The GET /employees route (from the previous step).
  • The POST /employees route (added in this step).
  • The DELETE /employees/:id route (added in this step).
  • The app.listen block at the bottom.

If any section is missing or out of order, scroll back to the matching substep above and copy the code block into the correct position.

Your API now supports creating and deleting employees. Next up, you will build a browser dashboard that talks to these endpoints, so you can manage employees from a real UI instead of the terminal.

Connect a Browser Dashboard

Your API endpoints handle creating, reading, and deleting employees from the terminal. Real users expect to manage employees from a browser, not by typing curl commands.

In this step, you will build an HTML dashboard that talks to your API. You will also discover a real-world problem that every web developer encounters.

In this step, get ready to:
  • Create an HTML dashboard with a form and employee table.
  • Wire up JavaScript to fetch and display employees from your API.
  • Discover why the browser blocks your dashboard from connecting.
Create the dashboard HTML and styles

The dashboard is a single HTML file with a form for adding employees and a table for displaying them. All the styling lives inside the file, so there is nothing else to install.

  • In the VS Code Explorer sidebar, hover over your project folder name and click the New Folder icon (the folder with a + symbol).
  • Name the folder public and press Enter.
  • Right-click the new public folder and select New File.
  • Name the file index.html and press Enter.

The file opens automatically in the editor. You should see public/index.html in the Explorer sidebar.

  • Paste the following code into index.html to set up the page structure and styles:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Employee Manager</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; background: #f5f5f5; }
    h1 { text-align: center; margin-bottom: 30px; color: #333; }
    .form-container { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    .form-container input { padding: 10px; margin-right: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
    .form-container button { padding: 10px 20px; background: #4CAF50; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
    .form-container button:hover { background: #45a049; }
    table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }
    th { background: #4CAF50; color: #fff; }
    tr:hover { background: #f1f1f1; }
    .btn-edit { padding: 5px 10px; background: #2196F3; color: #fff; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
    .btn-delete { padding: 5px 10px; background: #f44336; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
    .btn-edit:hover { background: #1976D2; }
    .btn-delete:hover { background: #d32f2f; }
  </style>
</head>

What do these styles do?

The CSS resets browser defaults, centers the page at 800px wide, and gives it a light gray background. It also pre-styles a form container, a table with green headers, and blue Edit / red Delete buttons that you will add in the next chunk.

  • Save index.html.
  • Below </head>, paste the body of your page with a form for adding employees and a table for displaying them:
<body>
  <h1>Employee Manager</h1>

  <div class="form-container">
    <input type="text" id="name" placeholder="Employee name">
    <input type="text" id="department" placeholder="Department">
    <button id="addBtn">Add Employee</button>
  </div>

  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Department</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody id="employeeTable">
    </tbody>
  </table>

</body>
</html>

What does this HTML do?

The form has two text inputs (for name and department) and an "Add Employee" button. Below it, a table with column headers (ID, Name, Department, Actions) is ready to display employee rows.

The <tbody id="employeeTable"> is empty on purpose. JavaScript will fill it with employee data from your API.

  • Save index.html.
  • In the VS Code Explorer sidebar, right-click index.html under the public folder.
  • Select Reveal in Finder (macOS) or Reveal in File Explorer (Windows).
  • Double-click index.html in the file browser to open it in your default web browser.

You should see the "Employee Manager" heading, two text inputs, a green "Add Employee" button, and a table with green column headers (ID, Name, Department, Actions). The table body is empty because there is no JavaScript yet.

Don't see the dashboard?

  • Make sure you saved index.html before opening it. Unsaved changes stay in VS Code and do not show in the browser.
  • Check that index.html is inside the public folder, not in the project root.
  • Confirm the browser's address bar starts with file://. If it shows localhost, you opened it from a server instead of the file system.

Still stuck? Ask for help.

Add the JavaScript and observe the CORS failure

The page looks right, but it does nothing yet. Your dashboard needs JavaScript to fetch employees from your API and display them in the table.

  • In public/index.html, find the closing </table> tag. Add the following <script> block below it, before the closing </body> tag:
  <script>
    // Fetch employees from the API and render them as table rows
    const API_URL = 'http://localhost:3000';
    let employees = [];

    async function fetchEmployees() {
      const res = await fetch(`${API_URL}/employees`);
      employees = await res.json();
      renderEmployees(employees);
    }

    function renderEmployees(list) {
      const tbody = document.getElementById('employeeTable');
      tbody.innerHTML = '';
      list.forEach(emp => {
        const tr = document.createElement('tr');
        tr.innerHTML = `
          <td>${emp.id}</td>
          <td>${emp.name}</td>
          <td>${emp.department}</td>
          <td>
            <button class="btn-edit" onclick="editEmployee(${emp.id}, '${emp.name}', '${emp.department}')">Edit</button>
            <button class="btn-delete" onclick="deleteEmployee(${emp.id})">Delete</button>
          </td>
        `;
        tbody.appendChild(tr);
      });
    }
    fetchEmployees();
  </script>

What does this code do?

  • API_URL points to your Express server running on http://localhost:3000.
  • fetchEmployees() sends a GET request to /employees and stores the response in the employees array.
  • renderEmployees() loops through each employee and builds a table row with Edit and Delete buttons.
  • The standalone fetchEmployees() call at the bottom runs automatically when the page loads.

Before you save and refresh, think about what will happen. Your HTML file is loaded from your file system (file:// protocol). Your API runs on http://localhost:3000. Do you think the browser will let them talk to each other?

  • Save index.html and refresh the page in your browser.
  • Open DevTools by pressing F12 (Windows) or Cmd+Option+I (macOS).
  • Click the Console tab.

You will see a CORS error in the console. The employee table stays empty. This is exactly what is supposed to happen.

What is CORS?

CORS stands for Cross-Origin Resource Sharing. Browsers block web pages from making requests to a different origin (domain, protocol, or port) than the one that served the page.

Your HTML file is loaded from file:// but your API runs on http://localhost:3000. Those are different origins, so the browser blocks the request. This is a security feature, not a bug.

The CORS error blocks all requests, but your dashboard code is not finished yet. Let's complete the remaining JavaScript handlers so the dashboard is ready for the fix in the next step.

  • Inside the <script> tag, find the standalone fetchEmployees(); call at the bottom. Add the following code above it to handle the Add Employee button:
    // Handle the Add Employee button click
    document.getElementById('addBtn').addEventListener('click', async () => {
      const name = document.getElementById('name').value;
      const department = document.getElementById('department').value;
      if (!name || !department) return alert('Please fill in both fields');

      await fetch(`${API_URL}/employees`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, department }),
      });
      document.getElementById('name').value = '';
      document.getElementById('department').value = '';
      fetchEmployees();
    });

What does this code do?

  • When the user clicks the Add Employee button, this code reads the name and department from the input fields.
  • It sends a POST request to /employees with the data as JSON.
  • After the request completes, it clears the input fields and re-fetches the full employee list.
  • Below the }); that closes the Add Employee button handler, paste the following code to handle editing and deleting employees:
    // Prompt the user to update an employee's details
    async function editEmployee(id, currentName, currentDepartment) {
      const name = prompt('Enter new name:', currentName);
      if (name === null) return;
      const department = prompt('Enter new department:', currentDepartment);
      if (department === null) return;

      await fetch(`${API_URL}/employees/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, department }),
      });
      fetchEmployees();
    }

    // Delete an employee after confirmation
    async function deleteEmployee(id) {
      if (!confirm('Are you sure you want to delete this employee?')) return;
      await fetch(`${API_URL}/employees/${id}`, { method: 'DELETE' });
      fetchEmployees();
    }

What does this code do?

  • editEmployee() uses browser prompt() dialogs to let the user type a new name and department, then sends a PUT request to update the record.
  • deleteEmployee() asks for confirmation with confirm(), then sends a DELETE request to remove the employee.
  • Both functions call fetchEmployees() afterward to refresh the table with the latest data.
  • Save index.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html and that the CORS error is still visible in your browser's Console tab.

ⓧ I'd like to double check the full code

Your public/index.html is built from the five code blocks above. Scroll up and confirm each one is present in this order:

  • The <head> section with all the CSS styles, ending with </head>.
  • The <body> section with the form, the table, and closing </body></html> tags.
  • The <script> block with fetchEmployees, renderEmployees, and the standalone fetchEmployees() call at the bottom.
  • The addBtn click handler, added above the standalone fetchEmployees() call.
  • The editEmployee and deleteEmployee functions, added below the Add Employee handler.

Not seeing the CORS error?

  • Make sure your server is still running in the other terminal. If it stopped, run node server.js again.
  • Confirm you are looking at the Console tab in DevTools, not the Elements or Network tab.
  • Check that API_URL in your script is set to 'http://localhost:3000' with the correct port.

Still stuck? Ask for help.

Your dashboard is built and wired up, but CORS is keeping it locked out. Next up, you will fix this and watch your employees appear in the browser.

Fix CORS and Complete the Dashboard

In the previous step, you saw a CORS error block your dashboard from reaching the API. That error is the browser's security policy in action, preventing web pages from making requests to a different origin.

In this step, you will install a CORS middleware that tells the browser your API accepts requests from any origin. You will also add the final CRUD route for updating employees, completing your management system.

In this step, get ready to:
  • Install CORS middleware and fix the cross-origin error.
  • Add a PUT route to update existing employees.
  • Test the full create, edit, and delete workflow in the browser.
Install and add CORS middleware

The cors package is an Express.js middleware that adds the right headers to your API responses. These headers tell the browser that cross-origin requests are allowed.

  • Stop the running server by pressing Ctrl+C in your terminal.
  • Install the cors package by running this command:
npm install cors

You should see output confirming the package was added to your project.

Now you need to tell Express to use the middleware. This requires two small edits in server.js.

  • In server.js, find this line near the top of your file:
const express = require('express');
  • Add this line directly below it:
const cors = require('cors');
  • Now find this line further down:
app.use(express.json());
  • Add this line directly above it, so cors() runs before any route:
app.use(cors());

The top of your server.js should now look like this:

const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
require('dotenv').config();

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

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

What does cors() do?

When a browser sends a request from one origin (your index.html file) to a different origin (localhost:3000), it first checks whether the server allows it. app.use(cors()) adds a response header that says "any origin is allowed."

Without this header, the browser blocks the response before your JavaScript ever sees it. That is the error you saw in the previous step.

  • Save server.js.
  • Restart the server by running this command:
node server.js

Before you refresh the dashboard, think about what you expect to see. Will the CORS error still appear, or will the employee table load?

  • Switch back to your browser where index.html is open and refresh the page.

The CORS error is gone. If you created employees earlier with curl, they appear in the table. If not, you see an empty table ready for data.

Still seeing the CORS error?

  • Make sure you saved server.js after adding both lines.
  • Confirm you restarted the server with Ctrl+C and then ran node server.js again. Code changes only take effect after a restart.
  • Check that app.use(cors()) appears before your route definitions. Middleware order matters in Express.
  • Why is my CORS error still showing after adding the cors middleware?
Add the update route

Your dashboard already has **Edit** buttons wired up from the previous step. Clicking one opens a prompt dialog asking for a new name and department. The frontend sends a PUT request, but the backend does not have a matching route yet.

  • Stop the server with Ctrl+C.
  • In server.js, find the comment that says // Delete an employee.
  • Add the following PUT route directly above that comment:
// Update an employee
app.put('/employees/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const { name, department } = req.body;
    const result = await pool.query(
      'UPDATE employees SET name = $1, department = $2 WHERE id = $3 RETURNING *',
      [name, department, id]
    );
    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'Employee not found' });
    }
    res.json(result.rows[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to update employee' });
  }
});

What does this route do?

  • app.put('/employees/:id') listens for PUT requests where :id is the employee's database ID.
  • The UPDATE ... SET ... WHERE SQL query changes the name and department for the matching row.
  • RETURNING * sends the updated row back so the frontend can confirm the change.
  • If no row matches the ID, the route returns a 404 error instead of silently succeeding.
  • Save server.js.
  • Restart the server by running this command:
node server.js
  • Switch back to your browser and refresh the page.
  • Click the blue Edit button on any employee row.
  • A prompt dialog asks for a new name. Enter a different name and click OK.
  • A second prompt asks for a new department. Enter a different department and click OK.

The table refreshes and shows the updated name and department for that employee.

Edit not working?

  • If nothing happens when you click Edit, open DevTools (F12) and check the Console tab for errors. A 404 or 405 means the PUT route is not registered. Make sure you restarted the server after saving.
  • Verify the route path is /employees/:id and uses app.put (not app.post).
  • Why is my PUT edit route returning a 404 or 405 error in the browser console?
Test the complete system

Your API now supports all four CRUD operations. Time to run through the full workflow and confirm every action persists in your Neon database.

  • In the dashboard, type a name (e.g. Jane Smith) and a department (e.g. Engineering) into the form fields.
  • Click Add Employee.

The new employee appears as a row in the table immediately.

  • Click the Edit button on the new row and change the department to Marketing.
  • Click the Delete button on a different row and confirm the deletion.

Before you check the database, predict what you will see. How many rows should the employees table have now? What department should the edited employee show?

  • Return to the Neon Console in your browser.
  • In the sidebar, click SQL Editor.
  • Run this query to see the live data:
SELECT * FROM employees ORDER BY id ASC;

The query results match what your dashboard shows. The edited employee has the new department, and the deleted employee is gone. Every action you performed in the browser persisted in your cloud database.

Why check the database directly?

The dashboard shows you what the API returns, but the SQL Editor shows you what is actually stored. Confirming both match proves that your API routes are writing to and reading from the real database, not just caching data in memory.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js. The index.html file did not change in this step.

ⓧ I'd like to double check the full code

Your server.js is longer than a quick glance, so scroll through it and confirm these sections appear in this exact order:

  • Require statements: express, cors, pg, and dotenv.
  • Middleware: app.use(cors()) followed by app.use(express.json()).
  • Pool: new Pool() with your connection string and SSL config.
  • Routes in order: GET /employees, POST /employees, PUT /employees/:id, DELETE /employees/:id.
  • app.listen: on port 3000.

Your employee management system is fully operational. You have a REST API that handles all four CRUD operations, a cloud database that persists every change, and a browser dashboard that ties it all together. Next up, there is a Secret Mission that adds a real-time search bar to your dashboard.

Secret mission

Add Real-Time Search

Your employee list works great for a handful of people. But as the list grows, finding one person means scrolling through every row. Can you add a search bar that filters employees by name or department as you type?

Clean Up Your Resources

Clean Up Your Resources

This project uses Neon's free tier, which costs $0 per month. The compute scales to zero after 5 minutes of inactivity, so there are no ongoing costs even if you leave everything running.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Neon Postgres project ("employee-manager") with an employees table.
  • Local project folder on your Desktop containing server.js, .env, public/index.html, and node_modules.

Keep everything running

No action needed. Choose this if you are still actively building or want to revisit the project later.

  • Your Neon project stays on the free tier at $0 per month. The database compute scales to zero after 5 minutes of inactivity.
  • Your local project files remain on your Desktop in the project folder you created.
  • Node.js stays installed on your machine. It is useful for future JavaScript and backend projects.

Pause - I'll come back to this later

Neon automatically suspends compute after 5 minutes of inactivity, so there is nothing to pause on the cloud side. You can free up local resources by stopping the server.

  • If your Express server is still running in the terminal, press Ctrl+C to stop it.
  • Your Neon database, local files, and Node.js installation all remain in place for when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to clean up completely.

  • If your Express server is still running in the terminal, press Ctrl+C to stop it.
  • Delete your local project folder by running this command (macOS/Linux):
  • On Windows, run this command instead:
cd ~/Desktop
rm -rf employee-manager
cd ~\Desktop
Remove-Item -Recurse -Force employee-manager
  • Delete your Neon project to remove the cloud database and all its data:
  • Open the Neon Console.
  • Select your employee-manager project.
  • Click Settings in the sidebar.
  • Click Delete. This permanently removes the project, including all branches, databases, and roles.

Nice Work!

Nice Work!

Nice work! You just built a working employee management system powered by an Express API, a Neon cloud database, and a browser dashboard.

You've learned how to:

  • Built a REST API with Express.js that handles full CRUD operations for creating, reading, updating, and deleting employee records.
  • Connected your API to a cloud-hosted Postgres database on Neon using raw SQL queries and the pg library.
  • Diagnosed and fixed a real-world CORS error to connect your browser dashboard to your backend API.
  • Secret Mission: Completed an optional challenge to push your skills further.

Ready to quiz yourself?