How to Use Next.js as a Front-End Framework: A Complete Authentication Guide with Nest.js

You've set up your Next.js application as your frontend, carefully crafting the UI and implementing the routing. But now you're faced with a crucial challenge: how do you properly handle authentication? Should you add another authentication layer? Is storing JWT in localStorage secure enough? And how does this all work with a separate backend like Nest.js?

If you're feeling overwhelmed by these questions, you're not alone. Many developers struggle with implementing secure authentication when using Next.js as a frontend-only framework. The good news is that there's a clear path forward, and I'll show you exactly how to implement it.

Why Next.js as a Frontend-Only Framework?

Next.js has evolved beyond its initial server-side rendering roots. While it excels at SSR and SSG, many developers are now using it purely for its frontend capabilities:

  • File-based routing: Intuitive and maintainable route management

  • Built-in optimization: Automatic code splitting and image optimization

  • Rich ecosystem: Seamless integration with popular CSS frameworks and tools

  • Developer experience: Hot reloading and excellent debugging tools

However, when using Next.js this way, authentication becomes a critical concern that needs careful consideration.

The Authentication Challenge

The most common pain points developers face when implementing authentication in a Next.js frontend include:

  1. Security Concerns: Many developers default to storing JWT tokens in localStorage, which exposes them to XSS attacks.

  2. Token Management: Implementing token rotation and refresh mechanisms can become complex, especially when dealing with server-side actions and cookies.

  3. Integration Complexity: Connecting Next.js with backend frameworks like Nest.js while maintaining secure sessions can be overwhelming.

Let's address these challenges one by one with practical, secure solutions that you can implement today.

Setting Up Secure Authentication

1. Project Structure

First, let's set up our project structure properly. A clean separation between frontend and backend is crucial:

your-project/
├── frontend/          # Next.js application
│   ├── src/
│   ├── public/
│   └── package.json
├── backend/           # Nest.js application
│   ├── src/
│   └── package.json
└── package.json       # Root package.json for managing workspaces

2. Implementing JWT Authentication

Instead of storing tokens in localStorage, we'll use httpOnly cookies for enhanced security. Here's how to set it up:

// frontend/src/services/auth.ts
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  withCredentials: true // Important for cookie handling
});

export const login = async (credentials: LoginCredentials) => {
  const response = await api.post('/auth/login', credentials);
  return response.data;
};

On the Nest.js side:

// backend/src/auth/auth.controller.ts
@Post('login')
async login(@Res({ passthrough: true }) response: Response) {
  const token = await this.authService.generateToken();
  
  response.cookie('jwt', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });

  return { success: true };
}

3. Token Refresh Implementation

One of the most challenging aspects is implementing token refresh. Here's a robust solution using axios interceptors:

// frontend/src/services/axios.ts
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        await api.post('/auth/refresh');
        return api(originalRequest);
      } catch (refreshError) {
        // Redirect to login if refresh fails
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Protected Routes and Authentication Flow

1. Creating Protected Routes

To secure your routes in Next.js, implement a higher-order component that checks for authentication:

// frontend/src/components/ProtectedRoute.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/hooks/useAuth';

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !isAuthenticated) {
      router.push('/login');
    }
  }, [isAuthenticated, loading, router]);

  if (loading) {
    return <div>Loading...</div>;
  }

  return isAuthenticated ? <>{children}</> : null;
}

2. Authentication Hook

Create a custom hook to manage authentication state:

// frontend/src/hooks/useAuth.ts
import { create } from 'zustand';

interface AuthStore {
  isAuthenticated: boolean;
  loading: boolean;
  setAuthenticated: (value: boolean) => void;
  setLoading: (value: boolean) => void;
}

export const useAuth = create<AuthStore>((set) => ({
  isAuthenticated: false,
  loading: true,
  setAuthenticated: (value) => set({ isAuthenticated: value }),
  setLoading: (value) => set({ loading: value }),
}));

3. API Route Protection

In your Nest.js backend, implement middleware to verify JWT tokens:

// backend/src/auth/jwt.guard.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(err: any, user: any) {
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

Apply the guard to your protected routes:

// backend/src/users/users.controller.ts
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

Best Practices and Common Pitfalls

Security Considerations

  1. Always Use httpOnly Cookies

    • Never store sensitive information in localStorage

    • Set appropriate cookie flags (secure, sameSite)

    • Implement CSRF protection for additional security

  2. Implement Rate Limiting

    // backend/src/auth/rate-limit.guard.ts
    import { ThrottlerGuard } from '@nestjs/throttler';
    
    @Injectable()
    export class RateLimitGuard extends ThrottlerGuard {
      protected errorMessage = 'Too Many Requests';
    }
    
  3. Handle Token Expiration Gracefully

    • Implement automatic token refresh

    • Clear authentication state on logout

    • Redirect to login page when refresh fails

Common Pitfalls to Avoid

  1. Cross-Origin Issues

    • Ensure proper CORS configuration:

    // backend/src/main.ts
    app.enableCors({
      origin: process.env.FRONTEND_URL,
      credentials: true,
    });
    
  2. Token Management

    • Don't store tokens in localStorage

    • Implement proper token rotation

    • Handle token refresh race conditions

  3. Error Handling

    • Implement proper error boundaries

    • Provide meaningful error messages

    • Log authentication failures for monitoring

Conclusion

Using Next.js as a frontend-only framework with Nest.js as your backend can create a powerful and secure application architecture. By following these best practices and implementing proper authentication mechanisms, you can avoid common security pitfalls while maintaining a clean separation of concerns.

Remember to:

  • Use httpOnly cookies for token storage

  • Implement proper token refresh mechanisms

  • Protect your routes both on frontend and backend

  • Handle errors and edge cases gracefully

For more information and advanced configurations, refer to the Next.js documentation and Nest.js authentication guide.

Additional Resources

Raymond Yeh

Raymond Yeh

Published on 20 March 2025

Get engineers' time back from marketing!

Don't let managing a blog on your site get in the way of your core product.

Wisp empowers your marketing team to create and manage content on your website without consuming more engineering hours.

Get started in few lines of codes.

Choosing a CMS
Related Posts
How to Handle Authentication Across Separate Backend and Frontend for Next.js Website

How to Handle Authentication Across Separate Backend and Frontend for Next.js Website

Learn how to implement secure authentication in Next.js with Express backend using httpOnly cookies, JWT tokens, and middleware. Complete guide with code examples.

Read Full Story
Best Practices in Implementing JWT in Next.js 15

Best Practices in Implementing JWT in Next.js 15

Comprehensive guide to JWT implementation in Next.js 15: Learn secure token storage, middleware protection, and Auth.js integration. Master authentication best practices today.

Read Full Story
Auth.js vs BetterAuth for Next.js: A Comprehensive Comparison

Auth.js vs BetterAuth for Next.js: A Comprehensive Comparison

Comprehensive comparison of Auth.js vs BetterAuth for Next.js, covering setup, documentation, support, and implementation. Make the right choice for your authentication needs.

Read Full Story
Loading...