Full-Stack Debugging & Logging
Learning Objectives
- You can trace one mutation through client, HTTP, service, and database evidence.
- You can use request identifiers without logging credentials or whole bodies.
- You can distinguish a causal fix from a change that merely hides a symptom.
A form displays “Something went wrong.” The message neither tells the user what to do next nor locates the cause for us. The request may have failed before leaving the browser, been rejected by the API, or reached a database constraint. Follow one operation through its boundaries so we can repair the cause and offer feedback that fits the outcome.
Keep the notes workflow from Chapters 4.3-4.5 running in practice-typed-http/.
Use Chapter 4.5’s supplied lab-note-api.ts continuation as
client/src/lib/api/note-api.ts; it provides the typed POST used below while
preserving the form’s interface. We will diagnose a failure in that same form,
operation, route, and stored data.
Inspect the request and response
Suppose the note form reports failure, but Network shows that its POST was accepted with 201. Follow the accepted body into the API module and component: did the module expect a different status, or did the component fail to update its feedback? Changing the input schema would not explain this observed success response.
A typed client catches some source mismatches when checked, but an old browser bundle or hand-written request can still send them. Look at the actual request rather than inferring it from the newest source file.
Record the method, URL, status, and a redacted description of the body shape. Do not paste session cookies or authentication headers into a help request.
Diagnose a missing request
0 / 5 points
A valid note form is submitted, but the Network panel shows no request. What is a useful first hypothesis and check?
Match a request to server logs
A request identifier connects logs for one operation without recording private content. For this local example, generate a server-controlled ID:
app.use("/api/notes", async (c, next) => { const requestId = crypto.randomUUID(); const started = performance.now(); c.header("X-Request-ID", requestId);
try { await next(); } finally { console.log(JSON.stringify({ requestId, method: c.req.method, route: "/api/notes", status: c.res.status, durationMs: Math.round(performance.now() - started), })); }});Place this middleware before the routes in the application module. A request ID is diagnostic metadata, not authorization. This middleware logs a fixed route label. It does not copy the caller’s URL, query, or body into the event. For a parameterized route, use a label such as /api/rooms/:id rather than the actual identifier.
If browser code needs to read X-Request-ID across origins, expose that response header through the CORS configuration. The Network panel can still show it for inspection. Later, security logging adds policy-relevant events without logging raw credentials. OWASP logging guidance is a useful reference for that boundary.
In practice-typed-http/, add the middleware to api/src/app.ts before its .route(...) mounts, then rebuild and send a new note:
docker compose up --build -d apicurl -i -H "Content-Type: application/json" --data '{"title":"Logging observation 1"}' http://localhost:8000/api/notesdocker compose logs --tail=20 apiFor one successful request, an illustrative log event is:
{"requestId":"7f76b053-a676-4674-9318-7a8cbd285749","method":"POST","route":"/api/notes","status":201,"durationMs":2}Choose a fresh title on each run; repeating it deliberately returns 409. Your identifier and duration will differ. Match the response’s X-Request-ID header to the event; the method, path, and status describe that same request. Use curl.exe in native Windows PowerShell.
The API module should preserve enough information for the component to distinguish invalid input, conflict, and unavailable service. Avoid throwing a new generic error at every layer and losing the original status and code.
An application’s unexpected-error handler should record a safe diagnostic linked to the request ID and return a generic public code and message. The middleware above records status and duration only; it does not implement that error handler. A user needs a next action, not a SQL stack trace.
Trace the request step by step
Use a narrow sequence:
- Was the event handled?
- Was the expected request sent?
- Did the correct API route handle it?
- Did validation succeed?
- Did the service accept the current state?
- Did the transaction commit?
- Did the client interpret the returned result?
A 409 is often correct server behavior, not a server defect. A 500 after an unexpected database error needs investigation, but should not expose the database URL to the browser. A successful response that the UI ignores points to a different boundary.
When a database check is useful, read the relevant row with a narrow query. Do not “fix” a test by manually modifying a persistent development row that the test should not use.
Diagnose a 400 response
0 / 5 points
The API returns 400 INVALID_INPUT. The current source uses title, but an old browser bundle might send noteTitle. What should you inspect first?
Reproduce a lost accepted result
Keep /lab, NotesPanel, and ValidatedNoteForm unchanged. First submit a
new title and confirm the ordinary 201 response. Then temporarily insert this
line immediately after the $post call in createNote, before its status branch:
if (response.status >= 200 && response.status < 300) { throw new Error("The client lost the accepted result"); // Deliberate defect.}Submit a new title. Network shows 201, but the form reports that the note could not be confirmed and the confirmed list does not gain a row. Reload the page: the GET includes that note. The API stored the request even though the client failed to adopt its response. Submitting again would not repair the response-handling bug.
Keep the deliberate throw and run the existing client suite. Chapter 4.3’s
creates a note through the documented POST test already expects the complete
accepted record, so it fails on this bug without a database or a new test. Follow
its rejected promise back to the response-handling branch. If a request instead
returns 404, check the route mount; if the browser cannot read it, inspect origin
settings. Those failures occur before this branch.
Remove the deliberate throw and rerun that suite. Submit one new title in the browser: both feedback and list should update before reload. The controlled response isolates the client interpretation; the earlier POST and subsequent GET established that the real API had stored the note. Keep the existing test as the regression check instead of appending another successful-creation case.
The broad failure tells us a workflow is broken; the narrower failure helps identify the responsibility.
Diagnose a successful save with stale display
0 / 5 points
A note request returns 201 with a normalized note, but the UI retains the old note. Which narrow regression would best isolate the likely boundary?
Check Your Understanding
- How can the latest source and an actual browser request disagree?
- How can a request ID connect a safe public error to useful server diagnostics without exposing private data?
- How do narrow tests and an end-to-end test help each other during debugging?