Introduction
A support API needs to find open tickets and safely create, update and remove individual records. You will connect a Worker to D1, implement parameterized SQL access, and test how the API handles missing records and invalid input.
The HTTP router is supplied so the main task is database integration. This independent lab uses one disposable Worker and one D1 database, plus separate local data.
Use your own learning account and a fresh VM. Setup first prepares Node.js 22.22.0, then runs npm install for project-local Wrangler 4.131.1 and any assessment dependencies under /home/labex/project/ticket-database. Direct dependency versions are pinned; the installation creates its own lockfile. No cloud login or assessed database work runs in setup. On a personal machine, install the same Wrangler version with npm install --save-dev wrangler@4.131.1 in your project.
This exercise uses small synthetic records within the D1 Free allowances. Existing account usage counts toward those allowances. No purchased domain is needed. Keep this VM until resource deletion and logout have both been checked.
Authorize this VM and select the account
In this step, you connect this fresh terminal to your own learning account. A Dashboard login alone does not authorize the VM. D1 permission allows database creation, SQL changes and deletion; Workers permission allows deployment, and KV permission supports Wrangler cleanup inventory. Review the actual consent page, including Background Access, before authorizing.
Open the prepared project and inspect the pinned CLI:
cd /home/labex/project/ticket-database
npx wrangler --version
Expect 4.131.1. Start device authorization; --device displays a browser code, and --browser=false leaves the browser choice to you:
npx wrangler login --device --browser=false --scopes account:read user:read d1:write workers_scripts:write workers_kv:write
Open the displayed URL in your browser, enter the current code, confirm your learning account and the permissions, and authorize. Wait for the terminal to confirm success. Never paste passwords or tokens into project files.
npx wrangler whoami --json
Check loggedIn: true, then read the account name and id, even when only one account is listed. Copy the intended ID into the configuration below. The following shell variable uses 6 random bytes (12 hexadecimal characters) to avoid colliding with other learners. A here-document writes the JSON between JSON lines; $RUN expands inside it.
The backslash before $schema keeps that JSON key literal; $RUN still expands to this run’s unique name.
RUN=labex-c04-d02-$(openssl rand -hex 6)
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "YOUR_ACCOUNT_ID",
"main": "src/index.js",
"compatibility_date": "2026-09-15",
"workers_dev": true,
"preview_urls": false
}
JSON
Replace YOUR_ACCOUNT_ID before running the block. Keep this terminal open so RUN remains available. name identifies this run; account_id selects the account for cloud operations. The file is ordinary JSON, which is also valid JSONC. No Worker is deployed by writing it.
Prepare independent local and remote data
In this step, you create the D1 connection and seed the familiar ticket table. Setup supplies HTTP routing and input validation in src/index.js; the missing storage functions are in src/store.js. This keeps your work focused on SQL access.
Create a disposable cloud database. --binding DB gives application code a short name, --update-config records its real name and UUID in wrangler.jsonc, and --use-remote=false keeps development local:
npx wrangler d1 create "$RUN-db" --binding DB --update-config --use-remote=false
Read the created name and ID, then inspect the saved binding:
cat wrangler.jsonc
The DB entry must name this run's database. A binding is a configured connection between code and a resource. Its UUID identifies the cloud database, while --local uses a separate SQLite database in this VM. Always include either --local or --remote in SQL commands.
cat schema.sql
npx wrangler d1 execute DB --local --file schema.sql
npx wrangler d1 execute DB --remote --file schema.sql
Each target now has the same two seed tickets. DB is the name that the supplied handler receives as env.DB; it must match the binding in configuration.
Implement parameterized CRUD
In this step, you implement CRUD: create, read, update and delete. A prepared statement keeps SQL structure separate from input. Each ? is a parameter placeholder; .bind(...) supplies values in order. Never concatenate user input into SQL, even when it looks harmless.
WHERE restricts the affected records. .all() returns a result object whose results field is the row array. .first() returns one row or null. SQLite's RETURNING clause gives the changed row back without a separate lookup. For deletion, .run() exposes meta.changes, which tells the router whether a record actually existed.
Write the storage module:
cat > src/store.js <<'JS'
export async function list(db, status) {
const query = status === null
? db.prepare('SELECT id, subject, status, source FROM tickets ORDER BY id')
: db.prepare('SELECT id, subject, status, source FROM tickets WHERE status = ? ORDER BY id').bind(status);
const { results } = await query.all();
return results;
}
export async function get(db, id) {
return db.prepare('SELECT id, subject, status, source FROM tickets WHERE id = ?').bind(id).first();
}
export async function create(db, subject) {
return db.prepare("INSERT INTO tickets (subject, source) VALUES (?, 'api') RETURNING id, subject, status, source").bind(subject).first();
}
export async function update(db, id, status) {
return db.prepare('UPDATE tickets SET status = ? WHERE id = ? RETURNING id, subject, status, source').bind(status, id).first();
}
export async function remove(db, id) {
const result = await db.prepare('DELETE FROM tickets WHERE id = ?').bind(id).run();
return result.meta.changes === 1;
}
JS
Read src/index.js to see how the supplied routing uses these functions. Missing records become a controlled 404; malformed input becomes 400; a caught database failure becomes 503 without exposing SQL internals.
Start the local server as a background job so the terminal remains available:
npx wrangler dev --ip 0.0.0.0 > dev.log 2>&1 &
Read the startup log and wait for the listening message:
cat dev.log
curl -i http://localhost:8787/tickets?status=open
Expect HTTP 200 and only ticket 1. The server uses the local database. Keep its job number from the terminal for cleanup.
Exercise writes and rejected inputs locally
In this step, you test more than successful reads. curl -i shows HTTP status and headers; -H supplies the JSON content type, and -d sends a body with POST by default.
Create a subject containing SQL-looking punctuation:
curl -i http://localhost:8787/tickets -H 'Content-Type: application/json' -d "{\"subject\":\"Printer ' OR 1=1 --\"}"
Expect 201 with the subject preserved as data. Copy the returned numeric id into TICKET_ID below; do not assume IDs remain the same after repeated tests:
TICKET_ID=YOUR_RETURNED_ID
curl -i http://localhost:8787/tickets/$TICKET_ID
curl -i -X PATCH http://localhost:8787/tickets/$TICKET_ID -H 'Content-Type: application/json' -d '{"status":"closed"}'
curl -i -X DELETE http://localhost:8787/tickets/$TICKET_ID
curl -i http://localhost:8787/tickets/$TICKET_ID
Expect a 200 read, a 200 update with closed, a 204 deletion with no body, then 404 with {"error":"not_found"}. The original two tickets must remain intact.
Send malformed JSON and an invalid status:
curl -i http://localhost:8787/tickets -H 'Content-Type: application/json' -d '{'
curl -i -X PATCH http://localhost:8787/tickets/1 -H 'Content-Type: application/json' -d '{"status":"lost"}'
Expect HTTP 400 with invalid_json and invalid_status respectively. SQL parameterization prevents input from becoming SQL, while application validation rejects values outside the business rules. They solve different problems.
Deploy and test the bound database
In this step, you publish the handler and its D1 binding. The database is already seeded remotely; deployment does not copy local rows.
npx wrangler deploy
Copy the actual https://...workers.dev URL printed by deployment into a shell variable. This is a disposable synthetic API, so remove it after testing:
URL='YOUR_DEPLOYED_HTTPS_URL'
curl -i "$URL/tickets?status=open"
Expect 200 and ticket 1. If a fresh deployment temporarily returns a platform error, wait a few seconds and repeat this read for up to a minute. Continue only once both status and JSON match; persistent failures need investigation.
Repeat CRUD on the remote API, copying its own returned ID:
curl -i "$URL/tickets" -H 'Content-Type: application/json' -d '{"subject":"Remote test"}'
TICKET_ID=YOUR_RETURNED_ID
curl -i -X PATCH "$URL/tickets/$TICKET_ID" -H 'Content-Type: application/json' -d '{"status":"closed"}'
curl -i -X DELETE "$URL/tickets/$TICKET_ID"
curl -i "$URL/tickets/$TICKET_ID"
curl -i "$URL/tickets"
Expect 201, 200, 204, 404, then the two unchanged seed tickets. In Dashboard, open this exact Worker and its Bindings view. Confirm DB points to your database; follow the database link for a read-only inspection. A saved local binding is not proof of the deployed connection.

This example shows the deployed Worker connected through DB to its D1 database. The random suffix identifies this example run; your names will differ. The table’s Value link opens the database selected by the deployed binding.
Delete the disposable resources
In this step, you remove only this lab's resources while the VM is still authorized. Finish all functional checks first. Keep configuration until deletion verification is complete.
npx wrangler delete
Confirm only the Worker name in this run’s configuration.
npx wrangler d1 delete DB
Inspect the prompt and confirm only this run's database. Then list databases:
npx wrangler d1 list --json
Your recorded database name and UUID must be absent from a successful response. Other resources may remain. An authentication or network error is inconclusive: resolve access and repeat the read before continuing. Run this step's verification while still logged in.
Stop the local development job too. List jobs and terminate only the wrangler dev job you started (replace %1 if its job number differs):
jobs
kill %1
End this VM authorization
In this step, you end the authorization only after the independent deletion check passes. Logout removes this VM's stored Wrangler authorization; closing a VM alone is not cloud cleanup.
npx wrangler logout
npx wrangler whoami --json
Expect loggedIn: false. This unauthenticated query can exit nonzero; that is expected only when the structured response explicitly says you are logged out. Complete verification, then close the lab environment.
Summary
You practiced add ticket search to a worker. You checked observable database results, kept the selected account and local state explicit, and removed the disposable resources before logging out.



