Production-Style Bug Triage API
Build a tested Fastify API that validates and prioritizes bug reports.
Introduction
30 Second Summary
Bug reports quickly lose their value when key details are missing. A software team needs a reliable way to keep urgent problems visible.
In this project, you will build a production-style bug triage REST API that validates reports before placing them in a severity-ordered queue. Integration tests protect that contract locally before GitHub Actions repeats the checks on every push.
What You'll Build
You'll demo a Fastify service as a submitted bug report lands in the correct place within a critical-first queue.
By the end of this project, you'll have:
- A live health endpoint that returns {"status":"ok"} while structured request logs appear in PowerShell.
- A validated report flow enforced by JSON Schema. You can demonstrate an issue ID or a 400 response with different payloads.
- A critical-first triage queue protected by integration tests. Each push earns a green GitHub Actions result when the contract still holds.
- Secret Mission: Add a validated severity filter that returns only the requested issues without changing the queue's critical-first order.
Are there any prerequisites?
Basic coding familiarity is enough. Step 1 includes the required runtime check plus full Windows workspace setup.
Before We Start
This opening checkpoint is your chance to commit to a clear outcome before the hands-on work begins. You are building a production-style bug triage API that turns incoming reports into a validated severity queue backed by request logs, integration tests, and automated checks through GitHub Actions.
Prepare the Windows Project Workspace
A reproducible backend project starts with a runtime that every developer can match. The version check prevents hidden environment differences from reaching your API.
This step pins Fastify at a known version. It also keeps generated dependencies out of version control.
In this step, get ready to:
- Verify the local development toolchain.
- Create the project workspace files.
- Install the pinned Fastify dependency.
Verify the confirmed tools
Node.js runs the service on your computer. Git tracks the project when you publish it later.
- Press the Windows key to open Windows Search.
- Type PowerShell.
- Select Windows PowerShell.
- Check your installed Node.js version by running:
node --version
What does this command do?
The command prints the installed Node.js version. This project needs major version 24 or newer.
✔️ I see version 24 or higher
Your Node.js runtime meets the project requirement. Keep this PowerShell window open for the next check.
ⓧ I see an older version
Version upgrades can be fiddly because an already-open terminal can keep the previous executable path.
- Open the official Node.js download page.
- Download Node.js v24.20.0 LTS for Windows.
- Complete the Windows installation using the default options.
- Approve the operating system permission prompt if it appears.
- Close Windows PowerShell after the installation finishes.
- Reopen Windows PowerShell through Windows Search.
- Repeat the version check shown above.
You should now see Node.js version 24 or newer.
ⓧ Command not found
Windows cannot currently find a Node.js installation. Installing the LTS release adds the runtime needed by this project.
- Open the official Node.js download page.
- Download Node.js v24.20.0 LTS for Windows.
- Complete the Windows installation using the default options.
- Approve the operating system permission prompt if it appears.
- Reopen Windows PowerShell through Windows Search.
- Repeat the version check shown above.
You should now see Node.js version 24 or newer.
- Verify Git by running:
git --version
What does this command do?
The command prints the installed Git version. A version line confirms that PowerShell can access Git.
You should see a line that begins with the Git product name followed by its installed version.
PowerShell cannot find Git?
Confirm that Git is installed on Windows. Reopen PowerShell after confirming the installation so the terminal reloads its executable paths.
Help me diagnose why PowerShell cannot find Git on Windows.
Create and open the project
A dedicated workspace keeps the API files together. Visual Studio Code opens that folder as one project.
- Return to the Windows PowerShell window from the version checks.
- Create the bug-triage-api folder by running these commands:
New-Item -ItemType Directory -Path bug-triage-api
Set-Location -Path bug-triage-api
code .
What do these commands do?
The New-Item -ItemType Directory -Path bug-triage-api command creates the project folder. The Set-Location -Path bug-triage-api command moves PowerShell into it.
The code . command opens the current folder in Visual Studio Code. You can now create every project file in one workspace.
You will see the empty bug-triage-api workspace in Visual Studio Code. Your PowerShell prompt now points to the same folder.
Visual Studio Code did not open?
Start Visual Studio Code through Windows Search if the command does not open it. Use the folder-opening control to select the bug-triage-api folder.
Help me open my current PowerShell folder in Visual Studio Code.
The package.json manifest defines the runtime requirement. It also records the commands and dependency version used by the project.
- Select the new-file icon in Visual Studio Code's left sidebar.
- Name the file package.json.
You will see a blank package.json editor tab inside the workspace.
- Add the package manifest by copying the code below:
{
"name": "bug-triage-api",
"version": "1.0.0",
"private": true,
"description": "A validated and tested API for prioritizing software bug reports.",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "node --test"
},
"engines": {
"node": ">=24"
},
"dependencies": {
"fastify": "5.12.1"
}
}
What does this file define?
- The JSON properties identify the project for npm.
- The scripts object defines one command for starting the server. It defines another command for running tests.
- The engines object requires Node.js version 24 or newer.
- The dependencies object pins Fastify to version 5.12.1.
- Save package.json.
- Confirm the editor tab no longer shows an unsaved-change indicator.
Is package.json rejected?
Check that every property name and text value keeps its double quotation marks. Confirm that the final property in each object has no trailing comma.
Help me compare my package.json with the required project manifest.
The .gitignore file tells version control which generated content to leave out of the repository.
- Select the new-file icon in Visual Studio Code's left sidebar.
- Name the file .gitignore.
You will see a blank .gitignore editor tab inside the workspace.
- Add the ignore rule by copying the line below:
node_modules/
What does this rule protect?
The node_modules/ rule excludes downloaded dependencies from version control. Other developers can recreate them from the package manifest and lockfile.
- Save .gitignore.
- Confirm .gitignore appears in the workspace sidebar.
Cannot see .gitignore?
Confirm that the filename begins with a period. Check that Visual Studio Code did not add a text-file extension.
Help me create a .gitignore file without an extra extension in Visual Studio Code.
The README.md file records how to run the planned service. It also documents the API contract that later steps implement.
- Select the new-file icon in Visual Studio Code's left sidebar.
- Name the file README.md.
You will see a blank README.md editor tab inside the workspace.
- Add the project documentation by copying the content below:
# Bug Triage API
A small Fastify service that validates software bug reports and returns a queue ordered by severity.
## Run locally
1. Run `npm install`.
2. Run `npm start`.
3. Open `http://localhost:3000/health`.
## Test
Run `npm test`.
## Endpoints
- `GET /health` returns the service status.
- `POST /issues` creates a validated bug report.
- `GET /issues` returns reports from critical to low severity.
What does this README capture?
The run section gives the local startup sequence. The test section records how to check the service.
The endpoints section defines the planned health check and issue workflow. This documentation becomes a checklist as you build the API.
- Save README.md.
- Confirm the editor displays the Bug Triage API heading.
Does the README look unformatted?
Confirm that the filename ends with .md. Check that the heading markers remain at the start of their lines.
Help me check the Markdown structure in my README.md file.
✔️ Awesome, I've got everything!
Your three project files are saved. Continue to the dependency installation.
ⓧ I'd like to double check the full code
Use these full-file references to compare every line in your workspace.
Reference for package.json:
{
"name": "bug-triage-api",
"version": "1.0.0",
"private": true,
"description": "A validated and tested API for prioritizing software bug reports.",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "node --test"
},
"engines": {
"node": ">=24"
},
"dependencies": {
"fastify": "5.12.1"
}
}
What should match?
This reference contains the complete package manifest for this step. Match its property names and values exactly.
Reference for .gitignore:
node_modules/
What should match?
This reference contains the only ignore rule needed in this step. The trailing slash identifies the dependency directory.
Reference for README.md:
# Bug Triage API
A small Fastify service that validates software bug reports and returns a queue ordered by severity.
## Run locally
1. Run `npm install`.
2. Run `npm start`.
3. Open `http://localhost:3000/health`.
## Test
Run `npm test`.
## Endpoints
- `GET /health` returns the service status.
- `POST /issues` creates a validated bug report.
- `GET /issues` returns reports from critical to low severity.
What should match?
This reference contains the complete project documentation for this step. Match the heading order and endpoint descriptions exactly.
Install the pinned dependency
npm reads package.json to install Fastify. It records the resolved dependency tree in package-lock.json.
- Return to the Windows PowerShell window inside bug-triage-api.
- Install the declared dependency by running:
npm install
What does this command create?
npm installs the dependency declared in package.json. The downloaded packages go into node_modules.
npm also creates package-lock.json with the exact resolved package versions. That lockfile gives future installations the same dependency tree.
- Return to Visual Studio Code's left sidebar.
- Confirm node_modules now appears in the workspace.
- Confirm package-lock.json now appears in the workspace.
Did npm install fail?
Confirm that PowerShell is inside the bug-triage-api folder. Check that package.json is saved before trying the installation again.
Help me diagnose an npm installation failure in my bug-triage-api workspace.
Before you run the final checks, which runtime line and dependency version do you expect PowerShell to print?
- Verify the complete workspace by running these checks:
node --version
git --version
npm ls fastify
What do these checks prove?
- The first check confirms that your active Node.js runtime meets the project requirement.
- The second check confirms that Git is available from PowerShell.
- The final check reads the installed dependency tree for Fastify.
You should see Node.js version 24 or newer. You should also see a Git version line plus fastify@5.12.1 in the dependency tree.
Does the dependency version differ?
Confirm that package.json pins Fastify to 5.12.1. Save the file before repeating the installation.
Help me correct the Fastify version in my npm dependency tree.
The groundwork is done. Your runtime now matches the project contract.
Your Windows workspace is ready. Next, you will connect a testable application factory to a visible health endpoint.
Launch a Visible Health Endpoint
Your workspace now has Fastify 5.12.1 installed. The next proof is a live endpoint that responds from your own machine.
Before you add bug rules, an application factory keeps the Fastify setup reusable.
A health check proves the local server is reachable. Its structured logs show each request passing through the service.
In this step, get ready to:
- Create a reusable Fastify application factory.
- Add a local server entry point with structured logging.
- Verify the health response in a browser.
Create the testable application factory
The factory holds the service configuration in one reusable function. Tests can create an isolated application later without opening a network port.
- Create app.js inside the bug-triage-api folder from the VS Code Explorer sidebar.
- Build the application factory by pasting this code into app.js:
'use strict'
const Fastify = require('fastify')
function buildApp (options = {}) {
const app = Fastify(options)
app.get('/health', async () => {
return { status: 'ok' }
})
return app
}
module.exports = { buildApp }
What Does This Code Do?
- The buildApp(options = {}) function creates a fresh application whenever it is called.
- The supplied options control features such as logging.
- The GET /health route returns a small service-status response.
- The exported buildApp function can be reused by the server entry point or a test.
- Save app.js.
- Confirm app.js appears alongside package.json in the Explorer sidebar.
Does the Factory File Look Wrong?
- Confirm the file is named app.js in lowercase.
- Confirm fastify remains listed under dependencies in package.json.
Help me check my application factory.
✔️ Awesome, I've got everything!
Your app.js file now contains the reusable factory and health route.
ⓧ I'd like to double check the full code
'use strict'
const Fastify = require('fastify')
function buildApp (options = {}) {
const app = Fastify(options)
app.get('/health', async () => {
return { status: 'ok' }
})
return app
}
module.exports = { buildApp }
What Should Match?
This reference shows the complete app.js file for this step.
Create the runtime entry point
The runtime entry point creates the application with logging enabled. It also owns the network startup process.
- Create server.js inside the bug-triage-api folder from the Explorer sidebar.
- Add the local server entry point by pasting this code into server.js:
'use strict'
const { buildApp } = require('./app')
const app = buildApp({ logger: true })
async function start () {
try {
await app.listen({ port: 3000 })
} catch (error) {
app.log.error(error)
process.exit(1)
}
}
start()
How Does the Server Start?
- The buildApp({ logger: true }) call creates the service with logging enabled.
- The start() function waits for the application to listen on port 3000.
- The catch block records a startup failure.
- The process.exit(1) call stops the failed process.
- Save server.js.
- Confirm server.js appears beside app.js in the Explorer sidebar.
Does Server.js Show a Problem?
- Confirm the import path is ./app.
- Confirm the final start() call appears below the function.
- Compare each brace with the complete file reference below.
Help me find the syntax problem in server.js.
✔️ Awesome, I've got everything!
Your server.js file now starts the application with structured logging.
ⓧ I'd like to double check the full code
'use strict'
const { buildApp } = require('./app')
const app = buildApp({ logger: true })
async function start () {
try {
await app.listen({ port: 3000 })
} catch (error) {
app.log.error(error)
process.exit(1)
}
}
start()
What Should Match?
This reference shows the complete server.js file for this step.
Run and observe the first endpoint
The health route returns a compact JSON response that confirms the service can handle a request. The matching log confirms that Fastify observed the same request.
Before you start the service, consider what the browser and PowerShell will each show after one health request.
- Start the local service from the PowerShell terminal by running:
npm start
What Happens During Startup?
The start script runs server.js. Fastify listens on port 3000 with logging enabled.
PowerShell remains attached to the running server so it can display each request log.
- Press the Windows key to open the search bar.
- Type the name of your installed web browser into the search bar.
- Select the browser result.
- Select the address bar.
- Enter http://localhost:3000/health.
- Press Enter to send the request.
You will see {"status":"ok"} in the browser.
- Switch back to PowerShell from earlier.
- Inspect the newest JSON request log.
You will see a structured log for the health request. Fastify includes a request ID that links log entries to that request.
Cannot Reach the Health Endpoint?
- Confirm PowerShell still shows the running server process.
- Compare the browser address with http://localhost:3000/health.
- Confirm both app.js and server.js are saved.
Help me debug the local health endpoint.
That is your first live proof. The service answers from your browser with a matching request log.
- Stop the local server by pressing Ctrl+C in PowerShell.
You will see the PowerShell prompt return. The local server is now stopped.
Your service now answers a live request with structured logs. Next, you will add the issue workflow that exposes the missing validation boundary.
Expose the Missing Validation
Your health endpoint proved that the Fastify service can start locally. Now the issue workflow needs a contract that protects the triage queue.
A bug report without severity cannot be prioritized. In this step, you deliberately run a naive endpoint so a failing integration test makes that weakness visible.
In this step, get ready to:
- Add an in-memory workflow for creating bug reports.
- Define the expected request contract with an integration test.
- Run the test to expose the status code mismatch.
Add the naive issue workflow
An in-memory store keeps issue objects inside the running application. Placing it inside buildApp() gives each application instance its own empty queue.
- In app.js, place const issues = [] directly below const app = Fastify(options).
- Copy the POST /issues route from the code below directly after the existing health route:
const issues = []
app.post('/issues', async (request, reply) => {
const issue = {
id: issues.length + 1,
title: request.body.title,
severity: request.body.severity,
status: 'open'
}
issues.push(issue)
request.log.info(
{ issueId: issue.id, severity: issue.severity },
'issue created'
)
return reply.code(201).send(issue)
})
What does this code do?
- The issues array stores every report created by this application instance.
- The handler builds an issue with an ID, title, severity, and open status.
- The request.log.info() call records the new issue ID and severity in the request log.
- The reply uses HTTP status 201 to confirm that the issue was created.
- Save app.js.
- Check that the updated service can start by running:
npm start
What does this command check?
The start script loads your updated app.js through server.js. A successful startup proves that the new route has valid JavaScript syntax.
You should see a structured log confirming that the service is listening on port 3000.
- Press Ctrl+C to stop the service.
Service not starting?
Check that const issues = [] sits inside buildApp(). Make sure the closing braces around the new route match the code above.
Help me find the syntax problem in my issue creation route.
The creation route stores reports. A queue endpoint makes the current contents observable.
- In app.js, add this route below the POST /issues route:
app.get('/issues', async () => {
return issues
})
How does the queue work?
The GET /issues route returns the current issues array. A fresh application starts with an empty array.
- Save app.js.
- Start the service with the new queue route by running:
npm start
What does this run prove?
The command starts the service with both issue routes registered. The queue is now available through the local server.
- Switch back to the browser from earlier.
- Enter http://localhost:3000/issues in the address bar.
You should see [] in the browser. This empty array confirms that the queue endpoint is running with no stored reports.
- Return to PowerShell.
- Press Ctrl+C to stop the service.
Queue route not loading?
Confirm that the address ends with /issues. Check that the PowerShell process is still running while you load the page.
Help me diagnose why my local issues route does not return an empty array.
✔️ Awesome, I've got everything!
Your issue creation route and queue route are in place.
ⓧ I'd like to double check the full code
'use strict'
const Fastify = require('fastify')
function buildApp (options = {}) {
const app = Fastify(options)
const issues = []
app.get('/health', async () => {
return { status: 'ok' }
})
app.post('/issues', async (request, reply) => {
const issue = {
id: issues.length + 1,
title: request.body.title,
severity: request.body.severity,
status: 'open'
}
issues.push(issue)
request.log.info(
{ issueId: issue.id, severity: issue.severity },
'issue created'
)
return reply.code(201).send(issue)
})
app.get('/issues', async () => {
return issues
})
return app
}
module.exports = { buildApp }
Write the expected contract as a test
A test can describe the behavior the API should enforce before that behavior exists. Fastify's app.inject() sends a request directly into the application without opening a network port.
- In the VS Code file sidebar from earlier, create a folder named test inside bug-triage-api.
- Inside test, create a file named app.test.js.
- Add the incomplete-report test to test/app.test.js by copying this code:
'use strict'
const { test } = require('node:test')
const assert = require('node:assert/strict')
const { buildApp } = require('../app')
test('POST /issues rejects a report without severity', async (t) => {
const app = buildApp()
t.after(() => app.close())
const response = await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Login page crashes' }
})
assert.equal(response.statusCode, 400)
})
What does this test prove?
- The test creates a fresh application instance with buildApp().
- The cleanup hook closes that application after the test finishes.
- The injected request sends a title without a severity value.
- The final assertion defines 400 as the required response for the incomplete report.
- Save test/app.test.js.
✔️ Awesome, I've got everything!
Your expected request contract is captured in an isolated test.
ⓧ I'd like to double check the full code
'use strict'
const { test } = require('node:test')
const assert = require('node:assert/strict')
const { buildApp } = require('../app')
test('POST /issues rejects a report without severity', async (t) => {
const app = buildApp()
t.after(() => app.close())
const response = await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Login page crashes' }
})
assert.equal(response.statusCode, 400)
})
Run the intentional failure
The test expects the API to reject an incomplete report. The current endpoint accepts any body that reaches its handler.
Before you run the test, do you think the endpoint returns the expected status or exposes the missing contract?
- Run the test suite from the PowerShell terminal in bug-triage-api with this command:
npm test
What does this command run?
The test script starts the Node.js test runner. It discovers test/app.test.js and executes the injected request.
You should see one failed test. The endpoint returns status 201 while the assertion expects status 400.
This failure is intentional. It proves that a report without severity currently enters the queue.
Test passed unexpectedly?
Confirm that the POST /issues route has no request schema. Check that the test payload contains only the title.
Help me restore the intended failing test for the missing severity validation.
You have turned an invisible API weakness into repeatable evidence. Next, you will enforce the request contract and make this test pass.
Enforce the Contract and Prioritize the Queue
Your failing integration test proved that the issue endpoint accepts incomplete reports. That failure gives you a precise contract to enforce.
In this step, Fastify uses JSON Schema to reject invalid input before the handler runs. Severity sorting then turns accepted reports into a useful triage queue.
In this step, get ready to:
- Enforce the required issue fields with a request schema.
- Order accepted issues from critical to low severity.
- Protect validation and ordering with passing integration tests.
Enforce the request contract
A request schema defines which bug reports may enter the service. Fastify checks this boundary before your issue-creation handler runs.
- In app.js, locate the route that begins with app.post('/issues', async (request, reply) => {.
- Replace that opening line with this route contract:
app.post('/issues', {
schema: {
body: {
type: 'object',
additionalProperties: false,
required: ['title', 'severity'],
properties: {
title: { type: 'string', minLength: 3 },
severity: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical']
}
}
}
}
}, async (request, reply) => {
What does this contract enforce?
- The body contract accepts a JSON object.
- The required fields ensure each report contains a title plus a severity.
- The additional properties rule blocks unexpected fields.
- The title rule blocks values shorter than three characters.
- The severity rule accepts only low, medium, high, or critical.
- Save app.js.
Before you rerun the test, do you expect Fastify to reject the request before the handler runs? Hold that prediction.
- Test the enforced contract by running:
npm test
What should you see?
The incomplete report now receives status 400. Your original integration test passes because the schema rejects the missing severity before an issue is created.
Still seeing status 201?
Check that the schema object sits between the route path and the async handler. Confirm that severity appears inside the required fields.
Compare your route opening with the code block above. A missing brace can prevent Fastify from registering the schema.
Help me find why my request schema still allows a report without severity.
Prioritize the issue queue
A triage queue needs a stable definition of urgency. Numeric ranks let the API compare severity values without changing the issue objects.
- In app.js, locate const Fastify = require('fastify').
- Add the severity ranks below that import:
const severityRank = {
low: 1,
medium: 2,
high: 3,
critical: 4
}
How do the ranks help?
The severityRank object gives each accepted severity a numeric priority. Higher numbers represent issues that should appear earlier in the queue.
- Save app.js.
- Check that the contract test remains healthy by running:
npm test
What does this check prove?
The existing invalid-report test should still pass. This confirms that the rank definition has not broken the request contract.
Did the contract test stop passing?
Place severityRank after the Fastify import. Keep it outside buildApp() so the object syntax does not interrupt the function.
Help me fix the severity rank definition in app.js.
The current queue returns issues in creation order. Sorting a copied array preserves the private store while producing the priority order clients need.
- In app.js, find the GET /issues handler whose body currently returns return issues.
- Replace the complete route with this sorted response:
app.get('/issues', async () => {
return [...issues].sort((first, second) => {
return severityRank[second.severity] - severityRank[first.severity]
})
})
What does this sorting code do?
- The spread syntax creates a copy of the private issues array.
- The comparison subtracts the first issue rank from the second issue rank.
- Higher-ranked issues move to the front without changing the original array.
- Save app.js.
✔️ Awesome, I've got everything!
Your request contract and severity ordering now match the target structure.
ⓧ I'd like to double check the full code
'use strict'
const Fastify = require('fastify')
const severityRank = {
low: 1,
medium: 2,
high: 3,
critical: 4
}
function buildApp (options = {}) {
const app = Fastify(options)
const issues = []
app.get('/health', async () => {
return { status: 'ok' }
})
app.post('/issues', {
schema: {
body: {
type: 'object',
additionalProperties: false,
required: ['title', 'severity'],
properties: {
title: { type: 'string', minLength: 3 },
severity: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical']
}
}
}
}
}, async (request, reply) => {
const issue = {
id: issues.length + 1,
title: request.body.title,
severity: request.body.severity,
status: 'open'
}
issues.push(issue)
request.log.info(
{ issueId: issue.id, severity: issue.severity },
'issue created'
)
return reply.code(201).send(issue)
})
app.get('/issues', async () => {
return [...issues].sort((first, second) => {
return severityRank[second.severity] - severityRank[first.severity]
})
})
return app
}
module.exports = { buildApp }
How to use this reference
This full file shows the request schema beside the private issue store. It also shows the severity ranks used by the copied queue.
Prove both behaviors
The original test protects the invalid-report boundary. A second isolated test now proves that creation order cannot override severity order.
- In test/app.test.js, place your cursor after the existing incomplete-report test.
- Add the queue-ordering integration test below:
test('GET /issues returns critical issues first', async (t) => {
const app = buildApp()
t.after(() => app.close())
await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Button text is clipped', severity: 'low' }
})
await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Checkout loses orders', severity: 'critical' }
})
const response = await app.inject({
method: 'GET',
url: '/issues'
})
assert.equal(response.statusCode, 200)
assert.deepEqual(
response.json().map((issue) => issue.severity),
['critical', 'low']
)
})
What does this test prove?
- The test creates its own application instance for isolation.
- The first injected request creates a low-severity report.
- The second injected request creates a critical report.
- The final assertion proves that the queue returns critical before low.
- Save test/app.test.js.
Before you run the complete suite, do you expect creation order or severity rank to control the response? Lock in your prediction.
- Verify both protected behaviors by running:
npm test
What should you see?
The summary reports two passing tests with zero failures. One test proves automatic rejection with status 400.
The other test proves that a low report created first still appears after the critical report. Your queue contract is now deterministic.
Is the ordering test failing?
Confirm that the low report is injected before the critical report. Check that the expected severity array lists critical before low.
In app.js, confirm that the comparison subtracts the first rank from the second rank. Reversing those operands produces low-first ordering.
Help me debug the queue-ordering integration test.
✔️ Awesome, I've got everything!
Your test file now protects invalid-report rejection and critical-first queue ordering.
ⓧ I'd like to double check the full code
'use strict'
const { test } = require('node:test')
const assert = require('node:assert/strict')
const { buildApp } = require('../app')
test('POST /issues rejects a report without severity', async (t) => {
const app = buildApp()
t.after(() => app.close())
const response = await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Login page crashes' }
})
assert.equal(response.statusCode, 400)
})
test('GET /issues returns critical issues first', async (t) => {
const app = buildApp()
t.after(() => app.close())
await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Button text is clipped', severity: 'low' }
})
await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Checkout loses orders', severity: 'critical' }
})
const response = await app.inject({
method: 'GET',
url: '/issues'
})
assert.equal(response.statusCode, 200)
assert.deepEqual(
response.json().map((issue) => issue.severity),
['critical', 'low']
)
})
How to use this reference
This full test file gives each behavior a separate application instance. Cleanup closes each instance after its assertions finish.
That is the API boundary secured: incomplete reports are rejected while valid issues arrive in priority order. Next, you will publish the project and make these tests run automatically on every push.
Automate the Quality Gate with GitHub Actions
Your bug triage API now rejects incomplete reports. It also places critical issues at the front of the queue.
Tests that run only on your computer can be forgotten. In this step, you will use continuous integration through GitHub Actions to run the complete test suite after every push.
In this step, get ready to:
- Add health endpoint coverage to the integration test suite.
- Define a push-triggered CI workflow.
- Confirm a successful CI run for the published repository.
Complete the local suite and CI workflow
Your current suite covers invalid input. It also covers severity ordering. A health endpoint test completes the core contract before automation takes over.
- In test/app.test.js, locate the blank line below the three import statements.
- Add the health endpoint test before the existing POST /issues test by copying this code:
test('GET /health reports service status', async (t) => {
const app = buildApp()
t.after(() => app.close())
const response = await app.inject({
method: 'GET',
url: '/health'
})
assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), { status: 'ok' })
})
What does this test prove?
- A fresh buildApp() instance keeps the test isolated from the other cases.
- The app.inject() call sends a request without opening a network port.
- The assertions prove that GET /health returns status 200 with the expected JSON body.
- Save test/app.test.js.
Before you run this, do you expect all three tests to pass? Hold that prediction.
- Check the expanded suite by running this command in the PowerShell terminal from earlier:
npm test
What does this command check?
The test runner discovers every test in test/app.test.js. It checks the health response alongside the validation contract plus queue ordering.
You should see three passing tests with zero failures. Your health endpoint now has the same automated protection as the issue workflow.
Is the health test failing?
Check that the new test sits below the imports. Confirm that its route is /health.
Compare the expected JSON object with the response returned by app.js.
Help me debug the failing health endpoint integration test.
✔️ Awesome, I've got everything!
Great. Double-check that test/app.test.js is saved before you create the workflow.
ⓧ I'd like to double check the full code
'use strict'
const { test } = require('node:test')
const assert = require('node:assert/strict')
const { buildApp } = require('../app')
test('GET /health reports service status', async (t) => {
const app = buildApp()
t.after(() => app.close())
const response = await app.inject({
method: 'GET',
url: '/health'
})
assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), { status: 'ok' })
})
test('POST /issues rejects a report without severity', async (t) => {
const app = buildApp()
t.after(() => app.close())
const response = await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Login page crashes' }
})
assert.equal(response.statusCode, 400)
})
test('GET /issues returns critical issues first', async (t) => {
const app = buildApp()
t.after(() => app.close())
await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Button text is clipped', severity: 'low' }
})
await app.inject({
method: 'POST',
url: '/issues',
payload: { title: 'Checkout loses orders', severity: 'critical' }
})
const response = await app.inject({
method: 'GET',
url: '/issues'
})
assert.equal(response.statusCode, 200)
assert.deepEqual(
response.json().map((issue) => issue.severity),
['critical', 'low']
)
})
What should match?
This reference contains the health test followed by the existing validation test plus sorting test. Each case creates its own application instance.
The CI workflow records the exact environment used for automated checks. Every push now has one repeatable path through dependency installation plus testing.
- In the VS Code Explorer sidebar, select the open bug-triage-api folder.
- Select the New File control.
- Enter .github/workflows/ci.yml as the file path.
You should now see ci.yml inside .github/workflows in the Explorer sidebar.
- Define the CI workflow by copying this configuration into .github/workflows/ci.yml.
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v7
with:
node-version: '24.x'
cache: 'npm'
- run: npm ci
- run: npm test
What does this workflow do?
- The push trigger starts the workflow whenever new commits reach the repository.
- The actions/checkout@v6 step makes the repository files available to the runner.
- The actions/setup-node@v7 step prepares Node.js 24.x with npm caching enabled.
- The npm ci step installs lockfile versions. The npm test step enforces the quality gate.
- Save .github/workflows/ci.yml.
The Explorer sidebar should continue to show ci.yml under .github/workflows.
Is the workflow file in the wrong place?
Confirm that the path starts with .github inside the open bug-triage-api folder. The workflow file must sit inside its workflows subfolder.
Help me check my GitHub Actions workflow path and YAML indentation.
✔️ Awesome, I've got everything!
Your workflow file is ready. Keep it saved for the final local check.
ⓧ I'd like to double check the full code
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v7
with:
node-version: '24.x'
cache: 'npm'
- run: npm ci
- run: npm test
What should match?
The complete file defines one CI workflow. Its test job installs locked dependencies before running the three local tests.
Run the final local check
The remote runner uses npm ci for a reproducible installation. That command depends on the lockfile created during the earlier dependency installation.
- In the VS Code Explorer sidebar, confirm that package-lock.json appears inside bug-triage-api.
Seeing package-lock.json confirms that the CI runner can install the same resolved dependency tree.
Before you run the final check, do you expect the new workflow file to change the local test result? Hold that prediction.
- Run the complete local suite from the PowerShell terminal with this command:
npm test
Why run the suite again?
This check proves the committed project state is healthy before publication. The remote workflow runs the same test script after installing dependencies.
You should see three passing tests with zero failures. That's your local quality gate complete: health, validation, plus severity ordering are all protected.
Are fewer than three tests passing?
Confirm that test/app.test.js contains the health test before the two existing issue tests. Save the file before rerunning the suite.
Help me find why npm test does not report three passing tests.
Publish and observe CI
A Git commit captures the tested project state. A public GitHub repository gives the workflow a place to run after each push.
Publishing makes your source code public. The supplied project files contain no credentials.
- Return to the browser from earlier.
- Sign in to GitHub.
- Start creating a repository from your account's repository area.
- Enter bug-triage-api as the repository name.
- Set the repository visibility to public.
- Leave the README, license, plus .gitignore initialization options disabled.
- Finish creating the repository.
GitHub should show the Quick Setup page for the empty bug-triage-api repository.
- Switch back to the PowerShell terminal in VS Code.
- Initialize the local repository on main by running this command:
git init -b main
What does this command do?
This command turns the open bug-triage-api folder into a Git repository. It creates main as the initial branch.
PowerShell should confirm that an empty Git repository was initialized on the main branch.
Does Git reject the branch option?
Git versions older than 2.28.0 may not support the branch option in the initialization command. Use the documented fallback by running these commands:
git init
git symbolic-ref HEAD refs/heads/main
The first command initializes the repository. The second command points its initial branch at main.
Help me initialize the repository on main with my Git version.
Your first commit creates a snapshot containing the API, its tests, plus the CI workflow.
- Stage all project files and create the first commit by running these commands:
git add .
git commit -m "First commit"
What do these commands record?
The first command stages every project file outside .gitignore. The second command saves that staged snapshot with the message First commit.
PowerShell should show that the first commit includes your application files, test suite, lockfile, plus workflow.
Did Git stop before creating the commit?
Read the message in PowerShell for the missing local setting. Complete the requested Git configuration before running the commit command again.
Help me resolve the Git configuration issue blocking my first commit.
A remote named origin connects the local commit history to the empty GitHub repository.
- Copy the repository URL from GitHub's Quick Setup page.
- Connect the local repository by running these commands with the copied URL in place of REMOTE-URL:
git remote add origin REMOTE-URL
git remote -v
What do these commands confirm?
The first command saves the copied GitHub URL under the name origin. The second command displays the saved remote connection.
You should see origin listed for both directions of the remote connection.
Is the remote missing or incorrect?
Confirm that you replaced REMOTE-URL with the URL shown on the repository's Quick Setup page. Keep the rest of the command unchanged.
Help me connect my local bug-triage-api repository to the correct GitHub remote.
Before you push, do you expect the workflow to start automatically from its push trigger? Hold that prediction.
- Upload the committed main branch by running this command:
git push -u origin main
What does this push trigger?
The command uploads the local main branch to origin. The workflow starts because .github/workflows/ci.yml listens for pushes.
PowerShell should confirm that the committed project was uploaded to GitHub.
Did the push fail?
Confirm that git remote -v shows the new repository URL. Check that your GitHub account can write to the repository.
Help me diagnose a failed push to my bug-triage-api GitHub repository.
- Return to the bug-triage-api repository in GitHub.
- Select the Actions tab.
- Select the CI workflow.
- Open the run triggered by your push.
- Confirm that the run finishes with a green successful result after all three tests pass.
You now have a live quality gate. Every future push runs the API contract tests on a clean GitHub-hosted runner.
Secret mission
Filter the Queue by Severity
Add an optional severity filter to the triage queue so consumers can request only the reports they need. You will validate the new query parameter without changing the queue's existing ordering behavior.
Clean Up Your Resources
Clean Up Your Resources
This project costs $0 to keep because its supplied workflow uses a standard runner in a public GitHub repository. Your choices are to keep it active, pause the workflow, or delete the local and remote copies.
Resources you used:
- The local bug-triage-api folder containing your source code, tests, node_modules, and package-lock.json.
- The public bug-triage-api GitHub repository containing its GitHub Actions CI workflow.
Keep everything running
No action is needed. Choose this option while you are still using the API or referring to the project.
- Keep the local bug-triage-api folder for future development.
- Leave the public repository available for future reference.
- Keep CI enabled so each push runs the test suite.
Pause - I'll come back to this later
Pausing prevents CI from running on future pushes. Your source code remains available locally and on GitHub.
- Stop any running local server by pressing Ctrl+C in its terminal.
- Select Actions in your GitHub repository.
- Select the CI workflow.
- Open the workflow menu.
- Choose Disable workflow.
The repository remains available. You can enable the workflow again when you return.
Delete - I don't want to use this again
This option removes the project from your computer. It also removes the public repository with its workflow.
Deletion Can Be Permanent
Removing the local folder deletes your code on this computer. GitHub warns that deleting the remote repository can be permanent.
- Find the bug-triage-api folder on your computer using File Explorer.
- Right-click the bug-triage-api folder.
- Select Delete.
- Confirm that bug-triage-api no longer appears in its original location.
Deleting the folder also removes node_modules and package-lock.json from your computer.
- Select Settings in the public bug-triage-api repository.
- Select General.
- Scroll to Danger Zone.
- Select Delete this repository.
- Complete GitHub's warning prompts to confirm the deletion.
You've cleared both copies. The CI workflow disappears with the repository.
Nice Work!
Nice Work!
Mission accomplished! Your production-style bug triage API now enforces a tested contract from local development through CI.
You've learned how to:
- Build a testable Fastify application with separate application and runtime entry points. Expose service health through GET /health. Create reports through POST /issues. Return a severity-ordered queue through GET /issues.
- Expose missing validation with a deliberately failing integration test. Enforce the JSON Schema contract so incomplete reports receive status 400. Prove critical-first ordering with isolated tests.
- Publish the project in a public GitHub repository. Automate every push with GitHub Actions. Use one test command across both environments.
- Secret Mission: Extended GET /issues with a validated optional severity filter. Added a fourth isolated integration test that proves critical filtering works without mutating the queue. Reused one set of allowed severities across both request contracts.
Ready to quiz yourself?