--- name: api-first-qa-testing description: Systematic QA testing of full-stack web apps by testing API endpoints first with curl/execute_code, then browser for visual verification. Much faster and more reliable than browser-only testing. version: 1.0.0 tags: [qa, testing, api, fullstack, debugging] --- # API-First QA Testing Systematic approach to QA testing full-stack web apps. Test APIs programmatically first (fast, reliable, catches most issues), then use browser only for visual/UX verification. ## When to Use - QA testing a full-stack web app with API backend + SPA frontend - Debugging "blank page" / "no data showing" issues - Running test suites defined in knowledge bases or specs - Verifying frontend-backend integration after changes ## Why API-First Browser testing is SLOW (page loads, caching, auth walls, JS rendering delays). Most bugs in full-stack apps are actually: 1. Missing API endpoints (frontend calls URL that doesn't exist) 2. Wrong column/field names in SQL queries 3. Date range filters excluding all data 4. Auth blocking fetch() calls (nginx basic auth pitfall) 5. Field name mismatches between frontend interfaces and API responses All of these are caught faster and more reliably by testing the API layer directly. ## Workflow ### Phase 1: Discover What Frontend Expects ```bash # Find all API calls the frontend makes grep -rn "useApi\|fetch(\|axios\|/api/" src/pages/ src/hooks/ src/components/ --include="*.tsx" --include="*.ts" ``` Build a map of: Page → API endpoint → Expected response shape ### Phase 2: Discover What Backend Provides ```bash # Find all registered routes grep -n "app\.\(get\|post\|put\|delete\)" server.ts ``` ### Phase 3: Find Mismatches Compare frontend expectations vs backend routes. Common issues: - Frontend calls `/api/classes/:id` but backend only has `/api/classes/:id/details` - Frontend expects `sales_total` field but backend returns `sales_today` - Frontend calls `/api/locations/:id/kpis` but endpoint doesn't exist (returns SPA HTML) - SPA catch-all returns 200 HTML for missing API routes (NOT 404), so frontend silently fails parsing HTML as JSON **Key detection pattern**: If `curl` returns HTML starting with `0, f"{len(d)} rows")) api_test("Search", "/api/customers?search=Lauren") api_test("Detail", "/api/customers/89086") ``` ### Phase 5: Fix and Verify For each failing endpoint: 1. Add the missing route to server.ts (match frontend's expected URL and response shape) 2. Fix SQL column names to match actual schema 3. Test with curl immediately 4. Rebuild frontend if field names changed ### Phase 6: Browser Verification (Visual Only) Only after APIs pass, use browser to verify: - Data renders in the UI - Branding/styling looks correct - Navigation flows work - Console has no errors ## Common Pitfalls ### Nginx basic auth blocks fetch() When `auth_basic` is set at server level, page loads fine but JS `fetch()` gets 401. Fix: add `location /api/ { auth_basic off; }` block. ### SPA catch-all masks missing endpoints Missing `/api/foo` returns 200 with HTML (the SPA index.html), not 404. Frontend tries to parse HTML as JSON → silently fails. Always test with curl first. ### Date range filters hide all data Endpoints using `WHERE date >= CURRENT_DATE - interval '7 days'` show nothing when data is older. Check `SELECT MIN(date), MAX(date)` to verify data exists in range. ### Express route ordering `/api/classes/:id` matches before `/api/classes/:id/reservations` only if they're in different path patterns. But `/api/customers/:id` WILL match `/api/customers/search` — static segments must be registered FIRST. ### Field name mismatches Frontend interface says `sales_today` but backend query aliases it as `sales_total`. Either update the frontend interface OR add alias mapping in the backend response. ### SQL parameterized query index bugs When building dynamic queries with arrays like `const params = [limit, offset]` then `params.push(searchTerm)`, the count query often does `params.slice(2)` to skip limit/offset — but the WHERE clause still references `$3`. After slicing, the search term is at index 0 but SQL says `$3`. Fix: track filter params and pagination params in SEPARATE arrays, then combine only for the final list query: ```typescript const filterParams = [] // search, segment, etc. const conditions = [] if (search) { filterParams.push(`%${search}%`) conditions.push(`name ILIKE $${filterParams.length}`) } // Count query: only filter params await pool.query(`SELECT count(*) FROM t ${where}`, filterParams) // List query: filter + pagination const listParams = [...filterParams, limit, offset] await pool.query(`SELECT * FROM t ${where} LIMIT $${filterParams.length+1} OFFSET $${filterParams.length+2}`, listParams) ``` ### Strict JSON parsing Large API responses may contain control characters that break `json.loads()`. Use `strict=False` parameter. ## Report Template ``` === FLOW N: Feature Name === PASS Endpoint description → result summary FAIL Endpoint description → error message SUMMARY: X PASS / Y PARTIAL / Z FAIL CRITICAL ISSUES: 1. Description — impact and suggested fix MINOR ISSUES: 1. Description — workaround ```