I run Next.js API Routes (now Route Handlers) for lightweight backend logic and NestJS for complex backend services — sometimes in the same project. The question I get most often is: 'When do I use NestJS vs just adding another Route Handler?' The answer depends on the complexity of your business logic, your team size, and your deployment model. Here's how I think about it.
Route Handlers (the App Router successor to API Routes) are excellent for: BFF (Backend for Frontend) patterns where you aggregate data from multiple services for a specific page, webhook receivers (Stripe, GitHub, Clerk), simple CRUD endpoints that map directly to a Prisma query, and Auth.js/NextAuth callback handlers. The key advantage is co-location — your frontend and backend code share the same repo, same TypeScript config, and same environment variables. For a solo developer or small team building a full-stack product, this colocation reduces cognitive overhead significantly.
Next.js Route Handlers on Vercel deploy as serverless functions. This means zero cold start for Node.js runtime (under 200ms typically), automatic scaling to zero, and no server management. The trade-off: no persistent memory between requests, no WebSockets (unless you use Vercel's separate realtime products), and execution time limits (10s on hobby, 60s on pro). For simple request-response patterns, serverless is the right model. For long-running processes or WebSocket connections, you need a persistent server — and that's where NestJS belongs.
Next.js Route Handler NestJS
──────────────────────── ─────────────────────────────
app/api/users/route.ts src/users/
├── users.module.ts
export async function GET() { ├── users.controller.ts
const users = await ├── users.service.ts
prisma.user.findMany() └── users.repository.ts
return Response.json(users)
} @Controller('users')
export class UsersController {
// Deployment: constructor(private usersService: UsersService) {}
// ↓ Vercel serverless function @Get()
// ↓ Cold start: ~200ms @UseGuards(JwtAuthGuard)
// ↓ Max execution: 60s async findAll() {
// ↓ No WebSockets return this.usersService.findAll()
// ↓ No persistent memory }
}
My production architecture: // Deployment:
┌─────────────────────┐ // ↓ VPS / Cloud Run / Railway
│ Next.js (Vercel) │ // ↓ Persistent memory
│ ├── Frontend pages │ // ↓ WebSocket gateways
│ └── BFF routes │──────► NestJS API (Railway/Cloud Run)
│ (session auth, │ // ↓ Bull job queues
│ aggregation) │ // ↓ Background workers
└─────────────────────┘From running both Next.js Route Handlers and NestJS in production: use Next.js Route Handlers as a thin proxy to NestJS when you need SSR data fetching integrated with your backend logic. The Route Handler validates the session (using Clerk/Auth.js), extracts the user ID, and calls the NestJS API with the internal service token. This pattern keeps your NestJS API clean (it doesn't know about session cookies or Next.js specifics) while giving your frontend the Server Component rendering benefits.
NestJS's module + controller + service + repository architecture feels verbose for simple CRUD. Where it earns its keep: complex business logic with multiple services that need to share state, middleware pipelines (guards, interceptors, pipes) applied consistently across routes, WebSocket Gateways colocated with REST endpoints, dependency injection for testing (mocking services cleanly), and background job processing with Bull. A 50-endpoint NestJS API is more maintainable than 50 Route Handler files — the forced structure prevents the spaghetti that grows in unstructured codebases.
# When does Next.js API Routes complexity break down?
# Example: 20+ endpoints with no structure
app/api/
├── users/route.ts # GET all, POST create
├── users/[id]/route.ts # GET one, PATCH, DELETE
├── users/[id]/roles/route.ts
├── products/route.ts
├── products/[id]/route.ts
├── orders/route.ts # complex business logic
├── orders/[id]/route.ts
├── orders/[id]/items/route.ts
├── invoices/route.ts # PDF generation, email triggers
└── ... # 30+ more files
# Problem: no shared guards, no DI for testing, no middleware pipeline
# NestJS equivalent (structured):
src/
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts # HTTP handlers
│ ├── users.service.ts # business logic
│ └── users.service.spec.ts # easily mockable via DI
├── products/
│ └── ...
├── orders/
│ ├── orders.module.ts
│ ├── orders.controller.ts
│ ├── orders.service.ts
│ └── orders.processor.ts # Bull queue processor
└── app.module.ts
# Shared middleware across all routes:
@Module({ providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }] })
// Every route protected in one line — impossible with Route Handlers without boilerplateThe strongest argument for a Next.js + NestJS combo in a monorepo is shared TypeScript types. In a Turborepo setup, you define your API request/response types in a shared packages/types package. Both the NestJS backend and Next.js frontend import from the same types — no drift, no manual sync, no runtime shape mismatches. This is the architecture I use for commercial projects: type safety enforced at the monorepo level.
I've seen Next.js projects where entire ERP backends were implemented as Route Handlers — hundreds of files in app/api/, no shared business logic, duplicated validation everywhere. Route Handlers don't enforce structure, so without discipline they become a dumping ground. If your API surface exceeds 20 endpoints, your business logic is non-trivial, or you need WebSockets or background jobs, invest in NestJS. The structure overhead is recouped in maintainability within weeks.
Next.js Route Handlers on Vercel are effortless to deploy — zero server management. NestJS requires a persistent server: a VPS, a container on Cloud Run/ECS, or a PaaS like Railway or Render. The infrastructure overhead is real but manageable. For most of my client projects, the architecture is: Next.js on Vercel (frontend + BFF Route Handlers) + NestJS on a $6/month VPS or Cloud Run (business logic API). This keeps costs low while maintaining proper separation.
My default stack: Next.js for the frontend with Route Handlers for BFF logic, NestJS for the primary backend API. I start with just Next.js Route Handlers on new projects to validate the product quickly, then introduce NestJS when the backend complexity warrants it — typically around the 20-30 endpoint mark or when I need WebSockets. For pure solo side projects, Next.js Route Handlers with Prisma cover everything I need and I skip NestJS entirely. Pragmatism over architecture purity.
Use Next.js Route Handlers if: (1) You're a solo developer or team of 2, (2) Deploying to Vercel and want zero server management, (3) Your backend is simple CRUD + webhooks, (4) You want colocation with your frontend. Use NestJS if: (1) Your team has dedicated backend developers, (2) You need WebSockets, background jobs, or complex middleware, (3) Your API surface has 30+ endpoints with complex business logic, (4) You need fine-grained testing with dependency injection mocking. Both can coexist — that's the power of the monorepo architecture.