docs: add case study on fixing API route timeouts
- Comprehensive debugging journey from timeout to working endpoints - Details on Turborepo monorepo build order for Vercel - Prisma cold start optimization strategies - Progressive debugging approach with code examples - Performance metrics before/after optimization - All examples reference tpmjs.com as production domain 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a07b705f80
commit
23dd6b1a33
1 changed files with 324 additions and 0 deletions
324
CLAUDE.md
324
CLAUDE.md
|
|
@ -355,3 +355,327 @@ vercel link
|
||||||
- Check CI logs before making blind fixes
|
- Check CI logs before making blind fixes
|
||||||
- Vercel deployments are blocked until GitHub Actions pass (configured in vercel.json)
|
- Vercel deployments are blocked until GitHub Actions pass (configured in vercel.json)
|
||||||
- Pre-commit/pre-push hooks run the same checks as CI - if they pass locally, CI should pass too
|
- Pre-commit/pre-push hooks run the same checks as CI - if they pass locally, CI should pass too
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Case Study: Fixing API Route Timeouts on tpmjs.com
|
||||||
|
|
||||||
|
This is a detailed account of debugging and fixing API route timeouts in production. The investigation revealed critical insights about deploying Turborepo monorepos to Vercel with Prisma.
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
After deploying tpmjs.com to production, all API endpoints were timing out:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl https://tpmjs.com/api/health
|
||||||
|
# Request timed out after 60 seconds
|
||||||
|
|
||||||
|
$ curl https://tpmjs.com/api/tools
|
||||||
|
# Request timed out after 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
The Next.js UI worked perfectly - pages loaded, navigation functioned - but every API route request resulted in a timeout. No errors appeared in Vercel logs, and the requests never even reached the serverless functions.
|
||||||
|
|
||||||
|
### Initial Investigation
|
||||||
|
|
||||||
|
**Step 1: Verify Build Output**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vercel inspect <deployment-url>
|
||||||
|
```
|
||||||
|
|
||||||
|
The build showed pages but **no API routes** listed as lambda functions:
|
||||||
|
|
||||||
|
```
|
||||||
|
Builds
|
||||||
|
├── ○ / (static page)
|
||||||
|
├── ○ /playground (static page)
|
||||||
|
└── ○ /tool/[slug] (static page)
|
||||||
|
|
||||||
|
# Expected to see:
|
||||||
|
├── λ api/health
|
||||||
|
├── λ api/tools
|
||||||
|
└── λ api/sync/changes
|
||||||
|
```
|
||||||
|
|
||||||
|
This confirmed Vercel wasn't treating the project as Next.js - it was using static site generation and dropping all API routes.
|
||||||
|
|
||||||
|
**Step 2: Check Vercel Configuration**
|
||||||
|
|
||||||
|
Examined `apps/web/vercel.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web",
|
||||||
|
"installCommand": "pnpm install"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The build command looked correct, but the custom build command was bypassing Vercel's Next.js detection.
|
||||||
|
|
||||||
|
### Root Cause #1: Workspace Dependencies Not Built
|
||||||
|
|
||||||
|
Deployed the site and checked the build logs. Found this critical error:
|
||||||
|
|
||||||
|
```
|
||||||
|
Module not found: Can't resolve '@tpmjs/env'
|
||||||
|
Module not found: Can't resolve '@tpmjs/types/tpmjs'
|
||||||
|
Module not found: Can't resolve '@tpmjs/ui/Badge/Badge'
|
||||||
|
Package @prisma/client can't be external
|
||||||
|
```
|
||||||
|
|
||||||
|
**49 module resolution errors** - the workspace packages weren't being built before the web app tried to import them.
|
||||||
|
|
||||||
|
**The Fix:**
|
||||||
|
|
||||||
|
Changed the build command from:
|
||||||
|
```json
|
||||||
|
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web"
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```json
|
||||||
|
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `...` suffix in `--filter=@tpmjs/web...` tells pnpm to build ALL dependencies first:
|
||||||
|
|
||||||
|
1. Build `@tpmjs/env`
|
||||||
|
2. Build `@tpmjs/types`
|
||||||
|
3. Build `@tpmjs/ui`
|
||||||
|
4. Build `@tpmjs/utils`
|
||||||
|
5. Build `@tpmjs/db` (Prisma generate)
|
||||||
|
6. Finally build `@tpmjs/web`
|
||||||
|
|
||||||
|
After this change, the build succeeded and API routes appeared as lambda functions in `vercel inspect`.
|
||||||
|
|
||||||
|
### Root Cause #2: Prisma Cold Start Performance
|
||||||
|
|
||||||
|
With the build fixed, API routes were deployed but still timing out. Testing revealed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health endpoint (no database) - WORKS
|
||||||
|
$ curl https://tpmjs.com/api/health
|
||||||
|
{"status":"ok","timestamp":"2025-11-28T11:32:29.295Z"}
|
||||||
|
|
||||||
|
# Tools endpoint (with database) - TIMEOUT
|
||||||
|
$ curl https://tpmjs.com/api/tools
|
||||||
|
# ...60 second timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
**Local Database Performance Test:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Test the exact queries used in production
|
||||||
|
const count = await prisma.tool.count(); // 3.244s ⚠️
|
||||||
|
const tools = await prisma.tool.findMany({
|
||||||
|
orderBy: [
|
||||||
|
{ qualityScore: 'desc' },
|
||||||
|
{ npmDownloadsLastMonth: 'desc' },
|
||||||
|
{ createdAt: 'desc' },
|
||||||
|
],
|
||||||
|
take: 20,
|
||||||
|
}); // 593ms ⚠️
|
||||||
|
```
|
||||||
|
|
||||||
|
The parallel `count()` + `findMany()` queries were taking **3.8 seconds** due to Prisma cold start in serverless environments.
|
||||||
|
|
||||||
|
**The Fix:**
|
||||||
|
|
||||||
|
Optimized the endpoint in two ways:
|
||||||
|
|
||||||
|
1. **Removed expensive count query** - Using `count()` in every request is slow and usually unnecessary:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Slow parallel queries
|
||||||
|
const [tools, totalCount] = await Promise.all([
|
||||||
|
prisma.tool.findMany({ where, take: limit, skip: offset }),
|
||||||
|
prisma.tool.count({ where }), // ← 3+ seconds!
|
||||||
|
]);
|
||||||
|
|
||||||
|
// After: Fast single query with limit+1 technique
|
||||||
|
const tools = await prisma.tool.findMany({
|
||||||
|
where,
|
||||||
|
take: limit + 1, // Fetch one extra to check if more exist
|
||||||
|
skip: offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasMore = tools.length > limit;
|
||||||
|
const actualTools = hasMore ? tools.slice(0, limit) : tools;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Reduced max page size** from 100 to 50 items for better performance
|
||||||
|
|
||||||
|
**Results:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl https://tpmjs.com/api/tools
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [...], # Returns in <1 second
|
||||||
|
"pagination": {
|
||||||
|
"limit": 20,
|
||||||
|
"offset": 0,
|
||||||
|
"hasMore": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Optimizations Applied
|
||||||
|
|
||||||
|
**Added maxDuration to all API routes:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const maxDuration = 60; // ← Prevent premature timeouts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verified database indexes exist:**
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Tool {
|
||||||
|
// ... fields ...
|
||||||
|
|
||||||
|
@@index([category])
|
||||||
|
@@index([isOfficial])
|
||||||
|
@@index([qualityScore])
|
||||||
|
@@index([npmDownloadsLastMonth])
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All necessary indexes were present - the issue was cold start latency, not missing indexes.
|
||||||
|
|
||||||
|
### Key Lessons Learned
|
||||||
|
|
||||||
|
**1. Monorepo Build Order Matters**
|
||||||
|
|
||||||
|
When deploying Turborepo monorepos to Vercel, workspace dependencies MUST be built first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ Wrong - only builds the web app
|
||||||
|
pnpm --filter=@tpmjs/web build
|
||||||
|
|
||||||
|
# ✅ Correct - builds dependencies first
|
||||||
|
pnpm --filter=@tpmjs/web... build
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Prisma in Serverless = Slow First Request**
|
||||||
|
|
||||||
|
Prisma Client initialization in serverless environments adds 1-3 seconds of latency on cold starts. Strategies to mitigate:
|
||||||
|
|
||||||
|
- Use connection pooling (Neon, PlanetScale)
|
||||||
|
- Eliminate unnecessary queries (especially `count()`)
|
||||||
|
- Cache query results when possible
|
||||||
|
- Consider Prisma Accelerate for critical paths
|
||||||
|
|
||||||
|
**3. Progressive Debugging Approach**
|
||||||
|
|
||||||
|
Start simple and progressively add complexity:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Step 1: Does endpoint respond at all?
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ status: 'ok' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Can we connect to database?
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
hasDatabase: !!process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Can we query the database?
|
||||||
|
export async function GET() {
|
||||||
|
const count = await prisma.tool.count();
|
||||||
|
return NextResponse.json({ count });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Full implementation with optimizations
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Use Vercel CLI for Debugging**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what's actually deployed
|
||||||
|
vercel inspect <deployment-url>
|
||||||
|
|
||||||
|
# Look for lambda functions (λ)
|
||||||
|
Builds
|
||||||
|
├── λ api/health ✅
|
||||||
|
├── λ api/tools ✅
|
||||||
|
|
||||||
|
# If you see only static pages (○), API routes aren't deployed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Final Configuration
|
||||||
|
|
||||||
|
**`apps/web/vercel.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build",
|
||||||
|
"installCommand": "pnpm install"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**API Route Template:**
|
||||||
|
```typescript
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const maxDuration = 60;
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Avoid count() - use limit+1 technique instead
|
||||||
|
const items = await prisma.tool.findMany({
|
||||||
|
orderBy: { qualityScore: 'desc' },
|
||||||
|
take: 21, // Request 1 more than needed
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasMore = items.length > 20;
|
||||||
|
const data = hasMore ? items.slice(0, 20) : items;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
pagination: { hasMore },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Metrics
|
||||||
|
|
||||||
|
**Before Optimization:**
|
||||||
|
- `/api/health`: Timeout (60s+)
|
||||||
|
- `/api/tools`: Timeout (60s+)
|
||||||
|
- Build: Failed (module resolution errors)
|
||||||
|
|
||||||
|
**After Optimization:**
|
||||||
|
- `/api/health`: ~50ms ✅
|
||||||
|
- `/api/tools`: ~800ms ✅
|
||||||
|
- Build: Success (all deps built) ✅
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
API timeouts in serverless environments often stem from build configuration issues or database cold starts. For Turborepo + Vercel + Prisma:
|
||||||
|
|
||||||
|
1. Use `pnpm --filter=package...` to build dependencies
|
||||||
|
2. Avoid `count()` queries in hot paths
|
||||||
|
3. Add `maxDuration` to API routes
|
||||||
|
4. Use `vercel inspect` to verify lambda deployment
|
||||||
|
5. Test database performance locally before deploying
|
||||||
|
|
||||||
|
The full working implementation is live at [tpmjs.com](https://tpmjs.com).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue