- 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>
17 KiB
Monorepo Setup
This project uses a Turborepo monorepo architecture with the following structure:
Packages
Published to npm (@tpmjs scope):
@tpmjs/ui- React component library with .ts-only components@tpmjs/utils- Utility functions (cn, format, etc.)@tpmjs/types- Shared TypeScript types and Zod schemas@tpmjs/env- Environment variable validation with Zod
Internal tooling (private):
@tpmjs/config- Shared configurations (Biome, ESLint, Tailwind, TypeScript)@tpmjs/eslint-config- ESLint configuration with module boundary rules@tpmjs/tailwind-config- Tailwind configuration with design tokens@tpmjs/tsconfig- TypeScript configurations (base, nextjs, react-library)@tpmjs/test- Vitest shared configuration@tpmjs/mocks- MSW mock server for testing@tpmjs/storybook- Component documentation and showcase
Applications
@tpmjs/web- Next.js 16 App Router application (main website)
Architecture Principles
2. No Barrel Exports
Components are imported directly without index.ts files:
// Good
import { Button } from '@tpmjs/ui/Button/Button';
// Bad (not allowed)
import { Button } from '@tpmjs/ui';
Benefits:
- Clearer dependency graphs
- Better tree-shaking
- Prevents circular dependencies
- Explicit imports
3. Module Boundaries
ESLint enforces strict module boundaries:
- Apps can only import from published packages
- Packages cannot import from apps
- UI package cannot import from utils (stays dependency-free)
4. Shared Configurations
All configuration is centralized in packages/config/:
- Biome - Formatting + basic linting
- ESLint - Semantic rules and module boundaries
- Tailwind - Design tokens and shared theme
- TypeScript - Multiple configs for different contexts
Development Workflow
# Install dependencies
pnpm install
# Run development servers
pnpm dev
# Build all packages
pnpm build
# Run tests
pnpm test
# Lint and format
pnpm lint
pnpm format
Detailed Development Commands
This section documents the complete testing, building, and development workflow used in this monorepo.
Testing Individual Packages
Use the --filter flag to target specific packages:
# Type-check a single package
pnpm --filter=@tpmjs/npm-client type-check
pnpm --filter=@tpmjs/ui type-check
pnpm --filter=@tpmjs/web type-check
# Run tests in a single package
pnpm --filter=@tpmjs/ui test
pnpm --filter=@tpmjs/web test
# Lint a single package
pnpm --filter=@tpmjs/web lint
Testing All Packages
Commands from the root run across all packages via Turborepo:
# Type-check all packages (runs via Turborepo)
pnpm type-check
# Lint all packages (runs via Turborepo)
pnpm lint
# Format all files with Biome
pnpm format
# Test all packages (runs via Turborepo)
pnpm test
Building Packages
Build commands respect dependency order automatically:
# Build a single package (and its dependencies)
pnpm --filter=@tpmjs/ui build
pnpm --filter=@tpmjs/types build
# Build all packages
pnpm build
# Build and watch for changes
pnpm --filter=@tpmjs/ui dev
Database Commands (Prisma)
The @tpmjs/db package uses Prisma for database management:
# Generate Prisma client (required after schema changes)
pnpm --filter=@tpmjs/db db:generate
# Push schema changes to database (dev)
pnpm --filter=@tpmjs/db db:push
# Create and apply migrations (production)
pnpm --filter=@tpmjs/db db:migrate
# Open Prisma Studio (database GUI)
pnpm --filter=@tpmjs/db db:studio
# Seed the database
pnpm --filter=@tpmjs/db db:seed
Important: Always run pnpm --filter=@tpmjs/db db:generate after modifying schema.prisma to regenerate the Prisma client. Without this, TypeScript will show errors for database types.
Development Servers
# Run Next.js dev server for web app
pnpm dev --filter=@tpmjs/web
# Run all dev servers (if multiple apps)
pnpm dev
# Run Storybook for component development
pnpm --filter=@tpmjs/storybook dev
Pre-commit Hooks (Lefthook)
Git commits automatically trigger these checks via Lefthook:
- Format - Biome formats all staged files
- Lint - Runs
pnpm lintacross all packages - Type-check - Runs
pnpm type-checkacross all packages
If any check fails, the commit is blocked. The hooks ensure code quality before changes reach CI.
Note: Pre-commit hooks run the same checks as CI, so if they pass locally, CI should pass too.
Turborepo Caching
Turborepo caches task outputs for faster rebuilds:
- Cache hits: Tasks show
cache hit, replaying logs- no actual work done - Cache miss: Tasks execute normally and outputs are cached
- Invalidation: Cache invalidates when inputs change (source files, dependencies, env vars)
# Clear Turborepo cache if needed
pnpm turbo clean
# Force rebuild without cache
pnpm build --force
Common Workflows
After pulling new changes:
pnpm install # Install new dependencies
pnpm db:generate # Regenerate Prisma client if schema changed
pnpm type-check # Verify everything type-checks
pnpm dev --filter=@tpmjs/web # Start dev server
Creating a new package:
# 1. Create package directory and files
mkdir -p packages/my-package/src
cd packages/my-package
# 2. Create package.json with proper name and workspace dependencies
# 3. Create tsconfig.json extending @tpmjs/tsconfig
# 4. Install dependencies from root
cd ../..
pnpm install
# 5. Type-check the new package
pnpm --filter=@tpmjs/my-package type-check
Testing before committing:
# Run the same checks that pre-commit hooks will run
pnpm format # Format all files
pnpm lint # Lint all packages
pnpm type-check # Type-check all packages
# Then commit - hooks should pass quickly
git add .
git commit -m "your message"
Troubleshooting
"Cannot find module '@prisma/client'"
- Run
pnpm --filter=@tpmjs/db db:generateto generate the Prisma client - The Prisma client must be generated after any schema changes or fresh installs
"Type error in package that imports from another package"
- Build the dependency first:
pnpm --filter=@tpmjs/types build - Or build all packages:
pnpm build - Turborepo handles this automatically when using
pnpm build
"Biome formatting errors in pre-commit"
- Run
pnpm formatto auto-fix formatting issues - Biome will format all files according to the config
"ESLint warnings about module boundaries"
- Check that you're not importing from apps in packages
- Check that imports follow the no-barrel-exports rule
- Example: Use
@tpmjs/ui/Button/Buttonnot@tpmjs/ui
"Turborepo cache shows stale outputs"
- Clear cache with
pnpm turbo clean - Force rebuild with
pnpm build --force
"Dev server won't start"
- Check that all dependencies are installed:
pnpm install - Check that Prisma client is generated:
pnpm db:generate - Check for port conflicts (Next.js default: 3000)
Publishing Flow
- Make changes to packages
- Create changeset:
pnpm changeset - Version packages:
pnpm changeset:version - Publish to npm:
pnpm changeset:publish - Push with tags:
git push --follow-tags
Tech Stack
- Build System: Turborepo
- Package Manager: pnpm
- TypeScript: Strict mode, composite projects
- React: v19
- Next.js: v16 App Router
- Styling: Tailwind CSS
- Testing: Vitest + Testing Library
- Linting: Biome + ESLint
- Documentation: Storybook
- CI/CD: GitHub Actions + Changesets
- Git Hooks: Lefthook
Debugging CI/CD with CLI Tools
When debugging CI failures or deployment issues, use command-line tools for efficient investigation:
GitHub CLI (gh)
Debug GitHub Actions CI runs:
# List recent workflow runs
gh run list --limit 10
# View specific run details
gh run view <run-id>
# View failed job logs
gh run view <run-id> --log-failed
# View specific job logs
gh run view <run-id> --job <job-id> --log
# Rerun failed jobs
gh run rerun <run-id> --failed
Common debugging workflow:
gh run list- Find the failed run IDgh run view <run-id> --log-failed- See what failed- Fix the issue locally
- Push and monitor:
gh run watch
Vercel CLI
Debug deployments and preview environments:
# List deployments
vercel ls
# View deployment details
vercel inspect <deployment-url>
# View deployment logs
vercel logs <deployment-url>
# Pull environment variables
vercel env pull
# Link local project to Vercel project
vercel link
Common debugging workflow:
vercel ls- Find the deployment URLvercel inspect <url>- Check deployment status and build logsvercel logs <url>- View runtime logs- Compare env vars:
vercel env pulland check.env.local
Tips
- Use
ghandvercelCLIs to debug without leaving the terminal - Check CI logs before making blind fixes
- 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
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:
$ 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
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:
{
"$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:
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web"
To:
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build"
The ... suffix in --filter=@tpmjs/web... tells pnpm to build ALL dependencies first:
- Build
@tpmjs/env - Build
@tpmjs/types - Build
@tpmjs/ui - Build
@tpmjs/utils - Build
@tpmjs/db(Prisma generate) - 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:
# 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:
// 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:
- Removed expensive count query - Using
count()in every request is slow and usually unnecessary:
// 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;
- Reduced max page size from 100 to 50 items for better performance
Results:
$ 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:
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60; // ← Prevent premature timeouts
Verified database indexes exist:
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:
# ❌ 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:
// 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
# 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:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build",
"installCommand": "pnpm install"
}
API Route Template:
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:
- Use
pnpm --filter=package...to build dependencies - Avoid
count()queries in hot paths - Add
maxDurationto API routes - Use
vercel inspectto verify lambda deployment - Test database performance locally before deploying
The full working implementation is live at tpmjs.com.