Debugging API Issues with Claude Code
Last week I spent two hours debugging a 500 error that turned out to be a timezone issue in a date comparison. Classic. But the way I got there was different than how I would’ve done it a year ago.
Here’s how I actually debug API issues with Claude Code now.
The Problem With “Help Me Debug This”
When something breaks, my instinct is to paste the error and ask Claude to fix it. That almost never works. Claude doesn’t have context. It doesn’t know my schema, my middleware stack, or that weird edge case in the auth flow that everyone on the team knows about.
The fix isn’t a better prompt. It’s better context.
Start With the Error, But Don’t Stop There
When I hit an API error, I grab three things before even opening Claude Code:
- The actual error message and stack trace
- The request that caused it (method, path, headers, body)
- Recent logs around that timestamp
I used to just paste the stack trace. Now I know that’s maybe 20% of what Claude needs.
Pulling Logs From Production
My server runs in the cloud, not on my laptop. So I can’t just grep a local log file—I need to pull logs from Cloud Logging, Datadog, or whatever logging service I’m using.
I have a slash command that fetches recent errors:
---
description: "Fetch recent production errors"
allowed-tools: Bash(gcloud:*)
argument-hint: [minutes]
---
Fetch errors from the last $1 minutes (default 30):
!gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="my-api" AND severity>=ERROR' \
--limit 20 \
--freshness="${1:-30}m" \
--format=json
Summarize these errors and identify any patterns.
The --format=json gives Claude the full log entry structure. If your logs use jsonPayload (structured logging) instead of textPayload, Claude can still parse them.
/prod-errors 15 grabs the last 15 minutes of errors and injects them into the conversation. Beats opening the Cloud Console and copy-pasting.
If you’re using Datadog, Sentry, or something else, same idea—just swap the CLI command. Most logging services have a CLI or API you can hit.
Reproducing the Request
For API issues, I need Claude to understand the exact request that failed. I keep a scratch file for this:
## Failed Request
POST /api/invoices/create
Authorization: Bearer [redacted]
Content-Type: application/json
{
"customer_id": 42,
"due_date": "2025-01-15",
"items": [...]
}
## Response
500 Internal Server Error
{
"detail": "Internal server error"
}
## Stack Trace
[paste traceback here]
Then I tell Claude: “Read debug-context.md and help me figure out what’s wrong.”
Having it in a file means Claude can reference it throughout the conversation without me re-pasting it.
Following the Request Through
Here’s my actual debugging flow:
“Trace this request through the codebase.”
I ask Claude to find the route handler, follow the function calls, identify where it could fail. This usually takes a few tool calls—Claude reads the route, reads the service layer, reads the database model.
“What could cause a 500 at this point?”
Once Claude understands the code path, I ask for failure modes. Usually there are three or four possibilities. Claude will say things like “if customer_id doesn’t exist, this query returns None and the next line would throw.”
“Add logging to narrow it down.”
If I can’t tell which failure mode I hit, I ask Claude to add temporary logging. Not print statements—actual structured logging I can grep.
logger.info("invoice_create", customer_id=customer_id, items_count=len(items))
Reproduce the error, check the logs, now I know where it broke.
The Database Angle
Half my bugs end up being database-related. Wrong query, missing index, constraint violation that the ORM swallowed.
I have a slash command for this:
---
description: "Debug a database issue"
allowed-tools: Bash(psql:*), Read, Grep
---
I'm debugging a database issue. Here's the context:
$ARGUMENTS
Help me:
1. Find the relevant SQLAlchemy model
2. Check the actual table schema
3. Identify any mismatches between model and schema
4. Look for recent migrations that might be related
/db-debug "invoices table returning wrong customer data" kicks off an investigation. Claude reads the model, runs a quick schema check, and usually spots the issue faster than I would scrolling through migrations.
When I’m Stuck
Sometimes I’m genuinely stuck. The logs don’t help, the code looks fine, and I’ve been staring at it for an hour.
That’s when I dump everything into Claude and ask: “What am I missing?”
Here's what I know:
- Request works locally, fails in staging
- Error is "connection refused" but the database is definitely up
- Started happening after yesterday's deploy
- Only affects this one endpoint
What should I check next?
Claude will often suggest something I hadn’t considered. Environment variables that differ between environments. Connection pool exhaustion. DNS resolution issues. At minimum, it gives me a new thread to pull.
Things I’ve Learned
Redact credentials before pasting. I have a hook that warns me if I’m about to send something that looks like a secret to Claude. Saved me from embarrassment more than once.
Logs are more valuable than code. When debugging, I spend more time showing Claude log output than source code. The code is already in the repo—Claude can read it. The runtime behavior is what’s missing.
Reproduce first, then ask for help. If I can’t reproduce the bug reliably, Claude can’t help. I always make sure I can trigger the error on demand before starting a debugging session.
Temp files are fine. I create debug.md or scratch.txt all the time. They get gitignored. Having a place to dump context is more important than keeping my repo pristine.
The Workflow, Summarized
- Grab error, request, and logs
- Put them in a context file (or let hooks inject them)
- Ask Claude to trace the request through the code
- Narrow down to likely failure points
- Add logging if needed, reproduce, confirm
- Fix the actual issue
Nothing revolutionary. But having Claude do the code navigation and suggest failure modes saves me a lot of scrolling and context-switching.
The two-hour timezone bug? Claude found it in about ten minutes once I gave it the full context. The date was being compared as a string in one place and a datetime in another. I’d looked right past it three times.