8 min read

API Routing: Bridging Next.js and NestJS with Smart Proxy Configuration

Learn how to elegantly integrate Next.js frontend with NestJS backend using intelligent routing configuration for seamless development and production experience.

by Jonathan Beurel

Building modern web applications often requires combining different frameworks to leverage their unique strengths. In this article, I'll walk you through an elegant solution for integrating a Next.js frontend with a NestJS backend using intelligent routing configuration that creates a seamless development and production experience.

1. The Architecture Challenge

When building full-stack applications with separate frontend and backend services, developers typically face several challenges:

  • Authentication state management across different domains
  • Development workflow complexity with multiple servers
  • API URL management in different environments

Traditional approaches often involve deploying services separately, leading to authentication headaches and environment-specific configuration nightmares.

2. Our Solution: Unified API Surface

Our architecture leverages Next.js's powerful routing capabilities to create a unified API surface that seamlessly proxies requests to our NestJS backend. Here's how the magic happens:

Smart Routing Configuration

The heart of our solution lies in the Next.js configuration file (next.config.ts):

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/auth/:path*',
        destination: '/api/auth/:path*',
      },
      {
        source: '/api/:path*',
        destination: process.env.NEST_API_URL + '/:path*',
      },
    ];
  },
};

This configuration implements a two-tier routing strategy:

  1. NextAuth.js Preservation: Routes matching /api/auth/:path* remain within Next.js for authentication handling
  2. Backend Proxy: All other API routes (/api/:path*) are proxied to our NestJS backend via the NEST_API_URL environment variable

3. Request Flow Overview

sequenceDiagram
    participant Browser
 
    box rgba(230, 245, 255, 0.8) Next.js Frontend
    participant NextJS as Next.js App<br/>(Port 3000)
    participant Middleware as Next.js<br/>middleware.ts
    participant Auth as NextAuth
    end
 
    box rgba(240, 248, 235, 0.8) NestJS Backend
    participant NestJS as NestJS Backend<br/>(Port 8080)
    participant DB as Database
    end
 
    Note over Browser,DB: Sign In Flow
    Browser->>NextJS: POST /api/auth/signin
    NextJS->>Auth: Verify credentials
    Auth->>DB: Update User
    Auth-->>Browser: Session cookie set
 
    Note over Browser,DB: API Request Flow
    Browser->>NextJS: GET /api/workspaces<br/>(with session cookie)
 
 
    Note over NextJS,Auth: 🔐 AUTHENTICATION Layer<br/>(Who are you?)
    NextJS->>Middleware: Intercept API request
    Middleware->>Auth: Extract user session
    Auth-->>Middleware: Return user data {sub, email}
 
 
    Middleware->>NestJS: Proxy request with JWT
 
    Note over NestJS,DB: 🛡️ AUTHORIZATION Layer<br/>(What can you do?)
    NestJS->>NestJS: Apply Guards & Controllers
    NestJS->>DB: Execute business logic
    DB-->>NestJS: Return data
 
 
    NestJS-->>NextJS: HTTP Response
    NextJS-->>Browser: Forward response

Authentication Layer

Our Next.js middleware (middleware.ts) acts as the bridge between frontend authentication and backend authorization:

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/auth/')) return NextResponse.next();
 
  const session = await auth();
  const requestHeaders = new Headers(request.headers);
 
  if (session?.user?.jwt) {
    const { sub, email } = session.user.jwt;
    const token = await generateApiToken({ sub, email });
 
    requestHeaders.set('Authorization', `Bearer ${token}`);
  }
 
  return NextResponse.next({ request: { headers: requestHeaders } });
}

The middleware:

  • Skips NextAuth.js routes to avoid interference
  • Extracts user information from the active session
  • Generates short-lived JWT tokens
  • Injects Authorization headers transparently

Authorization Layer

Our NestJS backend is configured to validate these tokens:

The backend uses custom guards and decorators to protect endpoints and extract user information from the JWT tokens.

4. Development Experience Benefits

This architecture provides several key advantages:

Single Development Server

Developers only need to run one command to start both frontend and backend services, with automatic proxy configuration.

Hot Reloading

Both applications benefit from hot reloading during development, maintaining productivity.

5. Production Considerations

Environment Variables

  • NEST_API_URL: Points to your deployed NestJS backend
  • API_SECRET: Shared secret for JWT token signing/verification

Performance

  • Next.js acts as a lightweight proxy
  • No additional latency beyond standard HTTP forwarding
  • Can leverage Next.js caching strategies for API responses

6. When to Use This Approach

This architecture is ideal when you:

  • Want to maintain separation between frontend and backend codebases
  • Need unified authentication across services
  • Prefer simplified development workflows
  • Plan to deploy services independently but want unified API access
  • Value type safety and modern development practices

7. Conclusion

By leveraging Next.js's routing capabilities as an intelligent proxy layer, we've created an architecture that provides the benefits of microservices while maintaining the simplicity of a monolithic API surface. This approach eliminates common pain points around authentication and development complexity while maintaining the flexibility to scale and deploy services independently.

The combination of smart routing and middleware-powered authentication creates a robust foundation for full-stack applications that can grow with your needs while keeping the developer experience smooth and productive.