Build a Full Stack Web App
Build a support ticket tracker with a Node.js REST API and browser frontend.
Introduction
30 Second Summary
Every team has a way of tracking problems, from sticky notes on a wall to shared spreadsheets to message threads that spiral out of control. The pattern is always the same: someone reports an issue, someone works on it, and it gets resolved.
In this project, you will build a Support Ticket Tracker from scratch. It's a full stack web app where you log tickets, track their status, and manage them through a dashboard powered by a server running on your own machine.
What You'll Build
You'll open your browser, type in a support issue, pick a priority, hit submit, and watch the ticket appear on a live dashboard where you can update its status and delete it when it's resolved.
By the end of this project, you'll have:
- A ticket dashboard built with HTML, CSS, and JavaScript where submitting an issue instantly adds it to a color-coded list.
- A REST API powered by Node.js and Express that creates, reads, updates, and deletes tickets.
- Persistent data stored in a JSON file that survives page refreshes and server restarts.
- Secret Mission: Add status filtering to view only Open, In Progress, or Resolved tickets, plus a keyword search bar to find specific tickets by description.
Are there any prerequisites?
This project assumes you have VS Code and Node.js already installed. The project walks you through everything else, including installing Express.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project
Every full stack app starts with a project structure. You need a place to keep your code, a way to manage packages, and a web framework to handle incoming requests.
In this step, you'll create your project folder, install Express (the framework that powers your server), and write a minimal server that serves your frontend files to the browser.
In this step, get ready to:
- Create a project folder and install Express.
- Set up your server and frontend file structure.
- Start the server and see your app in the browser.
Create the project folder and initialize it
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the built-in terminal by pressing Ctrl+` (backtick key, usually above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir support-ticket-tracker
cd support-ticket-tracker
- Initialize a new Node.js project by running:
npm init -y
You should see a summary of your new package.json file printed in the terminal.
What does this command do?
npm init -y creates a package.json file that tracks your project's name, version, and dependencies. The -y flag accepts all the defaults so you skip the setup prompts.
- Install Express by running:
npm install express@5.2.1
You should see a confirmation that express was added to your project.
What is Express?
Express is a web framework for Node.js that handles incoming HTTP requests and sends responses. It makes it simple to define routes (like /api/tickets) and serve files to the browser.
Seeing an error from npm install?
- Express 5.2.1 requires Node.js 18 or higher. Check your version by running node --version in the terminal. If it is below 18, update Node.js from the official site.
- Make sure you are inside the support-ticket-tracker folder. Run pwd (macOS) or cd (Windows) to check your current directory.
- Help me fix this npm install error.
- Now open your project folder in VS Code so the Explorer sidebar can see your files.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to Desktop, select the support-ticket-tracker folder, and click Open.
Trust the authors prompt
If VS Code asks whether you trust the authors of this folder, click Yes, I trust the authors. This is your own project folder, so it is safe.
The Explorer sidebar now shows your package.json and node_modules folder.
Create the server file and folder structure
Your project needs a server file, a data file to store tickets, and a folder for the frontend files that the browser loads.
- Open the terminal again by pressing Ctrl+` if it closed when the folder opened.
- Create the folder for your frontend files by running:
mkdir public
- In the Explorer sidebar, hover over support-ticket-tracker at the top to reveal the action icons.
- Click the New File icon (a page with a + symbol).
- Type server.js and press Enter.
- Click the New File icon again and create a file called tickets.json.
- Click the public folder in the Explorer to select it.
- Click the New File icon and create index.html inside it.
- Create two more files inside public the same way: style.css and app.js.
- Confirm all your files exist by running:
ls
You should see node_modules, package-lock.json, package.json, public, server.js, and tickets.json.
- Click tickets.json in the Explorer to open it.
- Type [] on the first line. This is the empty array where your tickets will be stored.
- Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Now it is time to write the server code. Your server will use Express to serve the frontend files in the public folder to anyone who visits the site.
- Click server.js in the Explorer to open it, then add the following code:
// Import Express and Node.js built-in modules
const express = require('express');
const fs = require('node:fs');
const path = require('path');
// Create the Express app and configure constants
const app = express();
const PORT = 3000;
const DATA_FILE = path.join(__dirname, 'tickets.json');
// Serve static files (HTML, CSS, JS) from the public folder
app.use(express.static(path.join(__dirname, 'public')));
// Start the server and print the URL
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
- require('express') imports the Express framework. require('node:fs') and require('path') import Node.js built-in modules for file operations. You'll use these in a later step.
- PORT sets which port the server listens on. DATA_FILE stores the full path to your tickets.json data file.
- express.static() is middleware that tells Express to serve files from the public folder. When someone visits your site, Express sends them the HTML, CSS, and JS files.
- app.listen() starts the server on port 3000 and prints a confirmation message to the terminal.
- Save server.js.
- Click index.html in the Explorer (inside the public folder) to open it, then add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support Ticket Tracker</title>
<!-- Link the stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Placeholder content to test the server -->
<p>Hello World</p>
<!-- Load the JavaScript file -->
<script src="app.js"></script>
</body>
</html>
What does this HTML do?
- The <link> tag connects your stylesheet (style.css) so you can add styling later.
- The <script> tag loads app.js, which is where your frontend logic will live.
- The Hello World paragraph is a placeholder to confirm the server is serving your files. You'll replace it with the real dashboard in the next step.
- Save index.html.
✔️ Awesome, I've got everything!
Great. Double check you have saved both server.js and index.html.
ⓧ I'd like to double check the full code
Here is the full server.js file:
const express = require('express');
const fs = require('node:fs');
const path = require('path');
const app = express();
const PORT = 3000;
const DATA_FILE = path.join(__dirname, 'tickets.json');
app.use(express.static(path.join(__dirname, 'public')));
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Here is the full public/index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support Ticket Tracker</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p>Hello World</p>
<script src="app.js"></script>
</body>
</html>
And tickets.json should contain just [].
Start the server and verify
Everything is in place. Before you run the server, think about what you expect to see in the terminal. Will there be an error, or a success message?
- Start your server by running:
node server.js
You should see Server running on http://localhost:3000 printed in the terminal.
Why is the terminal stuck?
The terminal is not stuck. Your server is running and waiting for requests. The terminal stays occupied while the server is active. To stop the server later, press Ctrl+C.
- Open your web browser (Chrome or Edge).
- Type http://localhost:3000 in the address bar and press Enter.
You should see Hello World displayed on the page. That text is coming from your index.html file, served to the browser by your Express server.
Not seeing Hello World?
- Check that the terminal shows Server running on http://localhost:3000. If you see an error instead, read the error message carefully and fix the issue in server.js.
- Make sure index.html is saved inside the public folder, not in the project root. Express only serves files from public.
- Confirm the URL is exactly http://localhost:3000 with no trailing slash or extra path.
- Help me troubleshoot my Express server.
Your project is set up and the server is running. Next up, you'll build the ticket dashboard UI that your users will see.
Build the Ticket Dashboard UI
Your server is up and running, serving files from the public folder to your browser. Right now the page is blank because index.html only has placeholder text.
Before you can wire up any data or backend logic, you need something to look at. In this step, you'll build a dashboard UI with a ticket submission form and a styled card layout.
In this step, get ready to:
- Build an HTML form with a description input, priority dropdown, and submit button.
- Style the dashboard with CSS for a clean, professional layout.
- Add placeholder ticket cards to preview the card design.
Build the HTML structure
Every web app starts with HTML that defines the elements a user can see and interact with. Your dashboard needs a form for submitting tickets and a container where ticket cards will appear.
- In VS Code, open index.html from the public folder in the Explorer sidebar.
- Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows) and replace it with this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support Ticket Tracker</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Support Ticket Tracker</h1>
<!-- Ticket submission form -->
<form id="ticket-form">
<input type="text" id="description" placeholder="Describe the issue..." required>
<select id="priority">
<option value="Low">Low</option>
<option value="Medium" selected>Medium</option>
<option value="High">High</option>
</select>
<button type="submit">Add Ticket</button>
</form>
<!-- Container where ticket cards will appear -->
<div id="ticket-list"></div>
</div>
<script src="app.js"></script>
</body>
</html>
What does this HTML do?
- The <form> wraps a text input for the ticket description, a <select> dropdown for priority (Low, Medium, High), and a submit button.
- The <div id="ticket-list"> is an empty container. Later, ticket cards will be created inside it dynamically from API data.
- The required attribute on the text input prevents the form from submitting if the field is empty.
- Save index.html.
- Refresh your browser at http://localhost:3000.
You should see a heading that says "Support Ticket Tracker", a text input, a dropdown, and a button. They look plain and unstyled for now.
Don't see the form?
- Make sure your server is still running. If it stopped, run node server.js again in your terminal.
- Check that you saved index.html inside the public folder, not in the project root.
- Help me debug my HTML form.
Style the dashboard
The form works, but it looks like a raw HTML page from the 1990s. CSS transforms it into something that looks like a real application.
- Open style.css from the public folder in VS Code.
- Add the page layout and reset styles by pasting this code:
/* Page layout and reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f2f5;
color: #333;
padding: 20px;
}
.container {
max-width: 700px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #1a1a2e;
}
#ticket-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
What does this CSS do?
- The * selector resets margins and padding on every element so browsers render the page consistently.
- .container centers the content and caps it at 700px wide, preventing the layout from stretching across wide screens.
- #ticket-form uses display: flex to arrange the input, dropdown, and button in a horizontal row.
- Save style.css and refresh your browser.
The page now has a light grey background, a centered heading, and the form elements sit in a neat row.
- Add the form element styles below your existing CSS in style.css:
/* Style each form element */
#ticket-form input[type="text"] {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
}
#ticket-form select {
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
}
#ticket-form button {
padding: 10px 20px;
background-color: #1a1a2e;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
#ticket-form button:hover {
background-color: #16213e;
}
What does this CSS do?
- flex: 1 on the text input makes it stretch to fill whatever space the dropdown and button don't need.
- The button gets a dark navy background (#1a1a2e) with a subtle hover effect that lightens it slightly.
- Matching border-radius values on all three elements give the form a cohesive, rounded look.
- Save style.css and refresh your browser.
Your form now looks polished. The text input stretches to fill the row, the dropdown has clean borders, and the submit button is a dark navy rectangle with white text.
Styles not applying?
- Confirm that your index.html has <link rel="stylesheet" href="style.css"> in the <head> section.
- Make sure style.css is saved inside the public folder, not in the project root.
- Help me troubleshoot my CSS not loading.
- Add the ticket card layout styles below your existing CSS in style.css:
/* Ticket card layout */
.ticket-card {
background: white;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.ticket-info {
flex: 1;
}
.ticket-info p {
margin-bottom: 6px;
}
.ticket-actions {
display: flex;
gap: 8px;
}
What does this CSS do?
These styles define how each ticket card will look: a white rounded rectangle with a soft shadow, with the ticket description on the left (.ticket-info) and action buttons on the right (.ticket-actions). These styles target ticket cards that you'll add in the next substep.
- Save style.css.
Add placeholder tickets to preview the design
You just wrote card layout CSS, but there are no cards on the page to see it in action. Adding a couple of hardcoded ticket cards lets you preview the design immediately.
- Switch back to index.html in VS Code.
- Find the line <div id="ticket-list"></div> and replace it with this expanded version containing two example tickets:
<div id="ticket-list">
<!-- Placeholder tickets -->
<div class="ticket-card">
<div class="ticket-info">
<p><strong>Example: Login page not loading</strong></p>
<p>Priority: <span class="priority-high">High</span> | Status: <span class="status-open">Open</span></p>
</div>
<div class="ticket-actions">
<button class="btn-status">Start</button>
<button class="btn-delete">Delete</button>
</div>
</div>
<div class="ticket-card">
<div class="ticket-info">
<p><strong>Example: Email notifications delayed</strong></p>
<p>Priority: <span class="priority-medium">Medium</span> | Status: <span class="status-in-progress">In Progress</span></p>
</div>
<div class="ticket-actions">
<button class="btn-status">Resolve</button>
<button class="btn-delete">Delete</button>
</div>
</div>
</div>
What do these placeholder tickets do?
Each .ticket-card div has a .ticket-info section (description, priority, status) and a .ticket-actions section (status and delete buttons). These are temporary. In a later step you'll remove them and load real tickets from your API instead.
Before you refresh, what do you think the two ticket cards will look like? Will they have colors yet?
- Save index.html and refresh your browser.
You should see two white card-shaped rows below the form. The layout is there, but the buttons are unstyled and the priority and status text is plain black. That is because you have not added those color styles yet.
Don't see the ticket cards?
- Make sure you replaced the entire <div id="ticket-list"></div> line, including both the opening and closing tags, with the expanded version.
- Check that the .ticket-card divs are nested inside <div id="ticket-list">, not outside it.
- Help me debug my placeholder tickets.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html.
ⓧ I'd like to double check the full code
Your index.html should have these sections in order:
- The <head> section with the stylesheet link to style.css.
- A .container div wrapping the entire body content.
- An <h1> with "Support Ticket Tracker".
- A <form id="ticket-form"> with the text input, priority dropdown, and submit button.
- A <div id="ticket-list"> containing two placeholder .ticket-card divs.
- A <script> tag linking to app.js at the bottom of the body.
- Switch back to style.css in VS Code and add the button layout and variant styles below your existing CSS:
/* Button base layout and variants */
.ticket-actions button {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.btn-status {
background-color: #e0e0e0;
color: #333;
}
.btn-status:hover {
background-color: #d0d0d0;
}
.btn-delete {
background-color: #ff4d4d;
color: white;
}
.btn-delete:hover {
background-color: #e04343;
}
What does this CSS do?
The base .ticket-actions button rule removes default borders and sets consistent sizing. .btn-status gets a neutral grey background, while .btn-delete is a warning red to signal a destructive action.
- Save style.css and refresh your browser.
The "Start" and "Resolve" buttons are now light grey, and the "Delete" buttons are red. The priority and status text is still plain black.
- Add the final color-coding styles below your existing CSS in style.css:
/* Priority and status color coding */
.priority-low {
color: #2e7d32;
font-weight: bold;
}
.priority-medium {
color: #ef6c00;
font-weight: bold;
}
.priority-high {
color: #c62828;
font-weight: bold;
}
.status-open {
color: #1565c0;
}
.status-in-progress {
color: #ef6c00;
}
.status-resolved {
color: #2e7d32;
}
What does this CSS do?
Each priority level gets a distinct color: green for Low, orange for Medium, red for High. Status labels follow a similar pattern: blue for Open, orange for In Progress, green for Resolved. These visual cues let users scan the ticket list and spot urgent items at a glance.
Before you refresh, what color do you think the "High" priority text will be?
- Save style.css and refresh your browser.
Your dashboard is fully styled. The first ticket shows "High" in bold red and "Open" in blue. The second shows "Medium" in orange and "In Progress" in orange.
Colors not showing on the tickets?
- Check that the class names in your HTML match the CSS exactly. For example, priority-high in the CSS must match class="priority-high" in the HTML. Class names are case-sensitive.
- Make sure the <span> tags wrapping the priority and status text have the correct class. For example: <span class="priority-high">High</span>.
- Help me debug my priority and status colors.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css.
ⓧ I'd like to double check the full code
Your style.css should have these style sections in order. Scroll up and confirm each one is present.
- Reset (*), body, .container, and h1 styles.
- #ticket-form layout, input, select, button, and button hover styles.
- .ticket-card, .ticket-info, and .ticket-actions layout styles.
- .ticket-actions button base, .btn-status, and .btn-delete variant styles with hovers.
- .priority-low, .priority-medium, .priority-high, .status-open, .status-in-progress, and .status-resolved color classes.
Your dashboard has a polished form and styled placeholder tickets. Next up, you'll build the backend API that creates, reads, updates, and deletes real ticket data.
Build the REST API
Your dashboard looks the part, with a styled form, color-coded priorities, and ticket cards laid out exactly how they'll appear when real data flows through.
Right now, those ticket cards are hardcoded HTML. The form's submit button does nothing. To make the dashboard functional, you need a backend REST API that can receive ticket data, store it, and send it back on demand.
In this step, get ready to:
- Add GET and POST routes to read and create tickets.
- Add PUT and DELETE routes to update and remove tickets.
- Test the API in your browser.
Add the GET and POST routes
Your server currently does one thing: serve static files from the public folder. You need to teach it how to handle JSON data and respond to specific URL paths called routes.
First, you'll add middleware that lets Express understand JSON request bodies. Then you'll create two helper functions for reading and writing ticket data, and your first API route.
- In server.js, find this line:
app.use(express.static(path.join(__dirname, 'public')));
- Add the JSON parsing middleware directly above that line by pasting this code:
// Parse incoming JSON data from requests
app.use(express.json());
- Now add the helper functions and your first API route directly below the app.use(express.static(...)) line by pasting the following code:
// Read all tickets from the JSON file
function readTickets() {
const data = fs.readFileSync(DATA_FILE, 'utf8');
return JSON.parse(data);
}
// Save the updated tickets array to the JSON file
function writeTickets(tickets) {
fs.writeFileSync(DATA_FILE, JSON.stringify(tickets, null, 2), 'utf8');
}
// GET /api/tickets - return all tickets
app.get('/api/tickets', (req, res) => {
const tickets = readTickets();
res.json(tickets);
});
What does this code do?
app.use(express.json()) is middleware that runs on every incoming request. It checks if the request body contains JSON and, if so, parses it into a JavaScript object on req.body. Without it, your POST and PUT routes would receive undefined instead of the data the frontend sends.
readTickets() opens tickets.json, reads its text content, and converts the JSON string into a JavaScript array. writeTickets() does the reverse: it converts the array back to a formatted JSON string and saves it to the file.
The GET route at /api/tickets reads the ticket data from the file and sends it to whoever asked for it. When the browser visits that URL, it gets back the full list of tickets as JSON.
- Save server.js.
- Switch back to your terminal and stop the running server by pressing Ctrl+C.
- Restart the server by running:
node server.js
- Open your browser and visit http://localhost:3000/api/tickets.
- You should see an empty JSON array [] displayed on the page. That means your GET route is reading from tickets.json and returning its contents.
Seeing an error instead of an empty array?
- Check that tickets.json in your project root contains exactly [] (an empty array). An empty file or missing brackets will cause a JSON parse error.
- Make sure app.use(express.json()) appears above app.use(express.static(...)) in your file. Order matters for middleware.
- Confirm you saved server.js and restarted the server after your changes.
Why is my GET route returning an error instead of an empty array?
Your API can return tickets. Now it needs a way to create them. The POST route accepts a ticket description and priority from the frontend, builds a ticket object, and saves it to the file.
- In server.js, add the POST route directly below the GET route's closing }); by pasting the following code:
// POST /api/tickets - create a new ticket
app.post('/api/tickets', (req, res) => {
const tickets = readTickets();
const newTicket = {
id: Date.now().toString(),
description: req.body.description,
priority: req.body.priority,
status: 'Open',
created: new Date().toISOString()
};
tickets.push(newTicket);
writeTickets(tickets);
res.status(201).json(newTicket);
});
What does this code do?
When the frontend sends a POST request with a description and priority, this route builds a complete ticket object. It generates a unique id using Date.now().toString(), sets the status to Open, and records a creation timestamp.
The route then pushes the new ticket onto the existing array, writes it back to the JSON file, and responds with a 201 status code. A 201 means "created successfully" in HTTP.
- Save server.js.
- Stop the server with Ctrl+C and restart it by running node server.js again.
- You should see Server running on http://localhost:3000 in the terminal. This confirms your POST route has no syntax errors.
Server won't start?
- Check that the POST route's closing }); is on its own line. A missing bracket or parenthesis is the most common cause of startup failures.
- Make sure you pasted the POST route after the GET route's closing }); and not inside it.
My Express server crashes on startup after adding a POST route. Help me debug.
Add the PUT and DELETE routes
Creating and reading tickets covers half of CRUD (Create and Read). You still need Update and Delete. The PUT route changes a ticket's status, and the DELETE route removes a ticket entirely.
- In server.js, add the PUT route directly below the POST route by pasting the following code:
// PUT /api/tickets/:id - update a ticket's status
app.put('/api/tickets/:id', (req, res) => {
const tickets = readTickets();
const ticket = tickets.find(t => t.id === req.params.id);
if (!ticket) {
return res.status(404).json({ error: 'Ticket not found' });
}
ticket.status = req.body.status;
writeTickets(tickets);
res.json(ticket);
});
What does the :id in the route path mean?
The :id is a route parameter. When the frontend sends a request to /api/tickets/1234, Express captures 1234 and makes it available as req.params.id. This lets one route handle any ticket by its unique ID.
The route searches the tickets array for a match. If no ticket has that ID, it returns a 404 (not found) error. If it finds one, it updates the status field and saves.
- Add the DELETE route directly below the PUT route by pasting the following code:
// DELETE /api/tickets/:id - remove a ticket
app.delete('/api/tickets/:id', (req, res) => {
let tickets = readTickets();
const index = tickets.findIndex(t => t.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: 'Ticket not found' });
}
tickets.splice(index, 1);
writeTickets(tickets);
res.status(204).send();
});
What does this code do?
The DELETE route finds the ticket's position in the array using findIndex(). If findIndex() returns -1, that means no ticket was found, so it sends back a 404.
splice(index, 1) removes exactly one element at that position. The route responds with a 204 status code, which means "no content." It tells the frontend the deletion succeeded without sending any data back.
- Save server.js.
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and move on to test your API.
ⓧ I'd like to double check the full code
Your server.js file should now contain these sections in order. Scroll up and verify each one is present:
- The require statements and constants (express, fs, path, PORT, DATA_FILE).
- app.use(express.json()) followed by app.use(express.static(...)).
- readTickets() and writeTickets() helper functions.
- Four routes in this order: GET, POST, PUT, DELETE.
- app.listen(PORT, ...) at the bottom.
Test the GET route
All four routes are in place. Time to restart the server and confirm the API is live.
- Stop the server with Ctrl+C and restart it by running:
node server.js
Before you open the browser, think about what you expect the API to return. What is currently stored in tickets.json?
- Open your browser and navigate to http://localhost:3000/api/tickets.
You should see an empty JSON array [] displayed in the browser. No tickets have been created yet, so the array is empty. Your API is live and reading from the data file.
What about the dashboard?
If you navigate back to http://localhost:3000, you'll still see the hardcoded placeholder tickets. The dashboard doesn't know about the API yet. In the next step, you'll connect the two so real data flows between them.
Still seeing an error at /api/tickets?
- Look at your terminal output after running node server.js. If the server didn't print Server running on http://localhost:3000, read the error message for the line number and check for missing brackets or typos.
- Make sure each route (app.get, app.post, app.put, app.delete) ends with }); and that no route is nested inside another.
My Express server crashes or /api/tickets returns an error after adding all four routes. Help me debug.
Your API is ready to go. You have four routes that cover every operation a ticket tracker needs: reading, creating, updating, and deleting. Next up, you'll wire the frontend to this API so the dashboard displays real data instead of placeholder HTML.
Connect the Frontend to the Backend
Your app has two halves that exist in total isolation. The dashboard shows hardcoded placeholder tickets that never change. The REST API can accept, store, and return real ticket data, but nothing in the browser ever calls it.
This step wires the two layers together using the Fetch API. You will replace the static HTML with JavaScript that talks to your server, so every ticket you create, update, or delete flows through the API and persists in your data file.
In this step, get ready to:
- Fetch and display tickets from the API when the page loads.
- Submit new tickets through the form and see them appear instantly.
- Add status cycling and ticket deletion with live updates.
Fetch and display tickets on page load
The placeholder ticket cards you hardcoded in the HTML were helpful for previewing your CSS. Now it is time to replace them with real data from the API.
- In VS Code, open public/index.html.
- Find the two hardcoded <div class="ticket-card"> blocks inside <div id="ticket-list">:
<div id="ticket-list">
<!-- Placeholder tickets -->
<div class="ticket-card">
<div class="ticket-info">
<p><strong>Example: Login page not loading</strong></p>
<p>Priority: <span class="priority-high">High</span> | Status: <span class="status-open">Open</span></p>
</div>
<div class="ticket-actions">
<button class="btn-status">Start</button>
<button class="btn-delete">Delete</button>
</div>
</div>
<div class="ticket-card">
<div class="ticket-info">
<p><strong>Example: Email notifications delayed</strong></p>
<p>Priority: <span class="priority-medium">Medium</span> | Status: <span class="status-in-progress">In Progress</span></p>
</div>
<div class="ticket-actions">
<button class="btn-status">Resolve</button>
<button class="btn-delete">Delete</button>
</div>
</div>
</div>
- Delete everything between the opening <div id="ticket-list"> tag and its closing </div>, so the element is completely empty:
<div id="ticket-list"></div>
- Save index.html.
- Refresh http://localhost:3000 in your browser.
The placeholder ticket cards are gone. The form is still there, but the ticket list area below it is completely empty.
Now you will write the JavaScript that fills that empty space with real data from the API. Your app.js file is currently blank.
- Open public/app.js in VS Code and add the following code to set up ticket fetching and rendering:
const form = document.getElementById('ticket-form');
const ticketList = document.getElementById('ticket-list');
let allTickets = [];
// Fetch tickets from the API and display them
async function fetchTickets() {
const response = await fetch('/api/tickets');
allTickets = await response.json();
renderTickets(allTickets);
}
// Render ticket cards into the DOM
function renderTickets(tickets) {
ticketList.innerHTML = '';
if (tickets.length === 0) {
ticketList.innerHTML = '<p style="text-align:center;color:#888;">No tickets yet. Create one above!</p>';
return;
}
}
// Return the label for the status toggle button
function getNextStatusLabel(currentStatus) {
if (currentStatus === 'Open') return 'Start';
if (currentStatus === 'In Progress') return 'Resolve';
return 'Reopen';
}
fetchTickets();
What does this code do?
This code grabs references to two DOM elements: the form and the ticket list container. It stores them in variables so you can reuse them without searching the page every time.
The fetchTickets() function calls your GET /api/tickets route using fetch, parses the JSON response, and passes the tickets to renderTickets() for display.
Right now, renderTickets() only handles the empty state. If there are zero tickets, it shows a friendly message. You will add the card-rendering logic next.
- Save app.js.
- Refresh http://localhost:3000 in your browser. You should see the message "No tickets yet. Create one above!" centered below the form.
Still seeing a blank page?
- Make sure you saved both index.html and app.js.
- Open DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows) and check the Console tab for errors. A red error message usually points to a typo in your code.
- Confirm the <script src="app.js"></script> tag is still at the bottom of your index.html body.
Why isn't my 'No tickets yet' message showing up?
That message proves the frontend is talking to the backend. Your JavaScript called the API, received an empty array, and rendered a message accordingly. Now you need to add the code that renders actual ticket cards when data exists.
- In app.js, find the renderTickets function. Locate the closing } of the if block (the one after return;).
- Add the following code below that closing }, but still inside the renderTickets function:
tickets.forEach(ticket => {
const card = document.createElement('div');
card.className = 'ticket-card';
// Map status and priority to CSS class names
const statusClass = ticket.status === 'Open' ? 'status-open'
: ticket.status === 'In Progress' ? 'status-in-progress'
: 'status-resolved';
const priorityClass = ticket.priority === 'Low' ? 'priority-low'
: ticket.priority === 'Medium' ? 'priority-medium'
: 'priority-high';
// Build the ticket card HTML with action buttons
card.innerHTML = `
<div class="ticket-info">
<p><strong>${ticket.description}</strong></p>
<p>Priority: <span class="${priorityClass}">${ticket.priority}</span> |
Status: <span class="${statusClass}">${ticket.status}</span></p>
</div>
<div class="ticket-actions">
<button class="btn-status" onclick="cycleStatus('${ticket.id}', '${ticket.status}')">
${getNextStatusLabel(ticket.status)}
</button>
<button class="btn-delete" onclick="deleteTicket('${ticket.id}')">Delete</button>
</div>
`;
ticketList.appendChild(card);
});
What does this code do?
This loop runs once for each ticket in the array. For every ticket, it creates a new div element, assigns it the ticket-card class (picking up your CSS styling), and fills it with HTML showing the ticket's description, priority, and status.
Each card gets two buttons. The status button calls cycleStatus() to advance the ticket through Open, In Progress, and Resolved. The delete button calls deleteTicket() to remove it. You will write both functions shortly.
- Save app.js.
- Refresh your browser. You still see the "No tickets yet" message because your data file is empty. The card-rendering code will kick in once you create your first ticket in the next substep.
Submit new tickets from the form
The form in your HTML does nothing yet. When you click the submit button, the browser tries to reload the page (the default form behaviour). You need an event listener that intercepts the submission, collects the input values, and sends them to your POST /api/tickets route.
- In app.js, find the fetchTickets(); call at the very bottom of the file.
- Add the following code above that line:
// Return the next status in the cycle
function getNextStatus(currentStatus) {
if (currentStatus === 'Open') return 'In Progress';
if (currentStatus === 'In Progress') return 'Resolved';
return 'Open';
}
// Handle form submission to create a new ticket
form.addEventListener('submit', async (e) => {
e.preventDefault();
const description = document.getElementById('description').value.trim();
const priority = document.getElementById('priority').value;
if (!description) return;
// Send the new ticket data to the API
await fetch('/api/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description, priority })
});
form.reset();
fetchTickets();
});
What does this code do?
The getNextStatus() helper maps each status to the one that follows it: Open becomes In Progress, In Progress becomes Resolved, and Resolved cycles back to Open. The status buttons will use this in the next substep.
The form listener prevents the default page reload with e.preventDefault(). It reads the description and priority from the form inputs, then sends them to the server as a JSON body using a POST request.
After the server responds, form.reset() clears the input fields and fetchTickets() reloads the full ticket list so the new ticket appears immediately.
- Save app.js.
Before you test this, what do you think will happen when you type a description, pick a priority, and click Add Ticket?
- Refresh http://localhost:3000 in your browser.
- Type Login page not loading into the description field.
- Select High from the priority dropdown.
- Click Add Ticket.
Your ticket appears as a styled card below the form. The description, priority (in red for High), and status (Open, in blue) are all visible. The form clears itself, ready for the next ticket.
Ticket not appearing after you click Add Ticket?
- Open DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows) and check the Console tab. A POST error means your server may not be running.
- Make sure your server is still running in the terminal. If it stopped, restart it by running node server.js.
- Verify that the code you added is above the fetchTickets(); line at the bottom of the file, not below it.
Why isn't my form creating a ticket?
Add status update and delete functionality
Your tickets have a Start button and a Delete button, but clicking them does nothing because the cycleStatus() and deleteTicket() functions do not exist yet. These two functions will call your PUT and DELETE API routes.
- In app.js, find the fetchTickets(); call at the very bottom of the file.
- Add the following code above that line:
// Send a PUT request to cycle a ticket's status
async function cycleStatus(id, currentStatus) {
const nextStatus = getNextStatus(currentStatus);
await fetch(`/api/tickets/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: nextStatus })
});
fetchTickets();
}
// Send a DELETE request to remove a ticket
async function deleteTicket(id) {
await fetch(`/api/tickets/${id}`, {
method: 'DELETE'
});
fetchTickets();
}
What does this code do?
The cycleStatus() function uses getNextStatus() to figure out the next status in the cycle, then sends a PUT request to the server with the updated value. After the server saves the change, it calls fetchTickets() to refresh the displayed list.
The deleteTicket() function sends a DELETE request to remove the ticket from the server, then refreshes the list so the card disappears from the page.
- Save app.js.
- Refresh http://localhost:3000 in your browser.
- Click the Start button on your ticket. The status changes from "Open" (blue) to "In Progress" (orange), and the button label switches to "Resolve".
- Click Resolve. The status changes to "Resolved" (green) and the button label switches to "Reopen".
Buttons not responding to clicks?
- Open DevTools and check the Console tab. If you see a "cycleStatus is not defined" error, make sure the cycleStatus function is defined outside of any other function, at the top level of app.js.
- Make sure getNextStatus (without "Label" in the name) exists in your file. This is a separate function from getNextStatusLabel. Both are required.
Why are my status and delete buttons not working?
Now for the real test. If your data truly persists on the server, everything should survive a page refresh. What do you think will happen when you reload the page?
- Create a second ticket with a different description and priority to have more data to work with.
- Click the Delete button on one of your tickets. It disappears from the list.
- Refresh the page. Your remaining ticket is still there with its current status intact.
Every action flows through your API. The browser sends a request, the server updates tickets.json, and the frontend re-renders. That is the full stack loop in action.
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.js and index.html.
ⓧ I'd like to double check the full code
Scroll up through your app.js and confirm each section is present in this order:
- DOM references (form, ticketList) and the allTickets array at the top.
- The fetchTickets() function that calls the GET API.
- The full renderTickets() function with empty-state handling and the forEach loop that creates ticket cards.
- The getNextStatusLabel() function returning Start, Resolve, or Reopen.
- The getNextStatus() function cycling through Open, In Progress, Resolved.
- The form.addEventListener('submit', ...) handler.
- The cycleStatus() and deleteTicket() functions.
- The fetchTickets(); call on the very last line.
For index.html, confirm the <div id="ticket-list"> element is completely empty with no placeholder cards inside it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support Ticket Tracker</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Support Ticket Tracker</h1>
<form id="ticket-form">
<input type="text" id="description" placeholder="Describe the issue..." required>
<select id="priority">
<option value="Low">Low</option>
<option value="Medium" selected>Medium</option>
<option value="High">High</option>
</select>
<button type="submit">Add Ticket</button>
</form>
<div id="ticket-list"></div>
</div>
<script src="app.js"></script>
</body>
</html>
Your support ticket tracker is fully functional. You can create, read, update, and delete tickets, and every change persists on the server. Next up, you will tackle an optional Secret Mission that adds filtering and search to your dashboard.
Secret mission
Add Filtering and Search
Your ticket tracker works, but finding a specific ticket in a long list means scrolling through everything. Can you add a status filter and keyword search so users can narrow down their view instantly?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The Node.js server process running on port 3000.
- Your project folder on the Desktop (contains server.js, tickets.json, the public/ folder, node_modules/, and package.json).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep using your ticket tracker.
- Your server stays running at http://localhost:3000 as long as the terminal is open.
- Your ticket data persists in tickets.json and survives server restarts.
- If you close your terminal, restart the server anytime by opening a terminal in your project folder and running node server.js.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. All your project files and ticket data stay on disk for next time.
- In the VS Code terminal where the server is running, press Ctrl+C to stop the server.
- Your project files and tickets.json data remain on your Desktop.
- When you are ready to come back, open your project folder in VS Code, open a terminal, and run node server.js to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes everything you built in this project.
- In the VS Code terminal where the server is running, press Ctrl+C to stop the server.
- Close VS Code.
- Open Finder (macOS) or File Explorer (Windows) and navigate to your Desktop.
- Find your project folder, right-click it, and select Move to Trash (macOS) or Delete (Windows). This removes all source files, node_modules/, and your tickets.json data.
- Empty your Trash or Recycle Bin to free up disk space.
Nice Work!
Nice Work!
You just built a full stack web application from scratch. You went from an empty folder to a working support ticket tracker with a frontend, a backend, and persistent data storage.
Here's what you learned:
- Built a full stack web application with HTML, CSS, and JavaScript on the frontend and Node.js with Express on the backend.
- Created a REST API with four routes covering all CRUD operations and persisted data to a JSON file using the Node.js file system module.
- Connected a browser-based UI to a server using the fetch API, sending and receiving JSON data so tickets persist across page refreshes.
- Secret Mission: Added status filtering and keyword search that work together to narrow down displayed tickets.
Ready to quiz yourself?