← Back to Blog
feature flagsnextjsreacttutorial

Feature Flags in Next.js: The Complete Guide (App Router)

Rollgate Team··13 min read
Feature Flags in Next.js: The Complete Guide (App Router)

Why Feature Flags in Next.js?

Next.js is not a typical React app. With the App Router, your code runs in multiple environments — the server, the edge, and the browser — sometimes in the same request. This makes feature flags both more powerful and more complex than in a single-page application.

Here is why feature flags matter specifically for Next.js:

Server-Side Rendering creates a decision point. When a Server Component renders on the server, you need the flag value before sending HTML to the client. If you evaluate flags only on the client, users see a flash of the wrong content (or a loading spinner) before the correct variant appears.

Edge Middleware runs before rendering. Next.js middleware can redirect or rewrite requests at the edge, before any component renders. This is the ideal place for A/B testing entire pages — route users to /pricing-a or /pricing-b based on a flag, with zero layout shift.

Hybrid apps need a unified approach. A real Next.js application uses Server Components, Client Components, API routes, and middleware. You need feature flags that work consistently across all four, with the same flag key returning the same value for the same user regardless of where you evaluate it.

Feature flags in Next.js let you do gradual rollouts without redeploying, run A/B tests at the edge, and kill problematic features in seconds — all while keeping the performance benefits of server rendering.

Architecture: Where to Evaluate Flags

Before writing code, understand the four places you can evaluate feature flags in a Next.js App Router application:

LocationSDKRuns OnBest For
Server Components@rollgate/sdk-nodeServer (Node.js)Initial render, SEO content, data-dependent flags
Client Components@rollgate/sdk-reactBrowserInteractive UI, real-time updates, user preferences
Middleware@rollgate/sdk-nodeEdge/NodeA/B testing pages, redirects, geo-targeting
API Routes@rollgate/sdk-nodeServer (Node.js)Backend logic, rate limits, feature-gated endpoints

The general rule: evaluate flags as early as possible. If a flag controls what the user sees on page load, evaluate it on the server. If a flag controls interactive behavior after the page loads, evaluate it on the client.

Server-Side Feature Flags

Server Components in Next.js are async by default, which makes them a natural fit for feature flag evaluation. You fetch the flag value during rendering, and the user receives the correct HTML immediately — no loading state, no flash of wrong content.

Setup

Install the Node.js SDK:

npm install @rollgate/sdk-node

Create a shared Rollgate client that you can import anywhere on the server:

// lib/rollgate.ts
import { RollgateClient } from '@rollgate/sdk-node';

export const rollgate = new RollgateClient({
  apiKey: process.env.ROLLGATE_SERVER_KEY!,
  baseUrl: 'https://api.rollgate.io',
});

Using Flags in Server Components

// app/dashboard/page.tsx
import { rollgate } from '@/lib/rollgate';
import { getCurrentUser } from '@/lib/auth';

export default async function DashboardPage() {
  const user = await getCurrentUser();

  const showNewDashboard = await rollgate.isEnabled('new-dashboard', {
    userId: user.id,
    attributes: {
      plan: user.plan,
      email: user.email,
      createdAt: user.createdAt,
    },
  });

  if (showNewDashboard) {
    return <NewDashboard user={user} />;
  }

  return <LegacyDashboard user={user} />;
}

The userId ensures consistent evaluation — the same user always sees the same variant, even across multiple requests. The attributes enable targeting rules like "enable for Pro plan users" or "enable for users created after March 2026."

Server Components with Multiple Flags

When you need several flags in one component, evaluate them in parallel to avoid sequential waterfalls:

// app/settings/page.tsx
import { rollgate } from '@/lib/rollgate';
import { getCurrentUser } from '@/lib/auth';

export default async function SettingsPage() {
  const user = await getCurrentUser();

  const context = {
    userId: user.id,
    attributes: { plan: user.plan },
  };

  const [showBilling, showTeamSettings, showApiKeys] = await Promise.all([
    rollgate.isEnabled('settings-billing-v2', context),
    rollgate.isEnabled('team-management', context),
    rollgate.isEnabled('api-keys-section', context),
  ]);

  return (
    <div>
      <ProfileSettings />
      {showBilling && <BillingV2 />}
      {showTeamSettings && <TeamSettings />}
      {showApiKeys && <ApiKeysSection />}
    </div>
  );
}

Client-Side Feature Flags

Not everything runs on the server. Interactive components — modals, tooltips, dynamic forms, real-time features — live in Client Components. For these, use the React SDK, which provides a context provider and hooks.

Setup

Install the React SDK:

npm install @rollgate/sdk-react

Add the RollgateProvider in your root layout. Since providers require 'use client', create a wrapper component:

// components/providers.tsx
'use client';

import { RollgateProvider } from '@rollgate/sdk-react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <RollgateProvider
      apiKey={process.env.NEXT_PUBLIC_ROLLGATE_CLIENT_KEY!}
      baseUrl="https://api.rollgate.io"
    >
      {children}
    </RollgateProvider>
  );
}
// app/layout.tsx
import { Providers } from '@/components/providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Using Flags in Client Components

// components/feedback-widget.tsx
'use client';

import { useFlag } from '@rollgate/sdk-react';

export function FeedbackWidget() {
  const showFeedback = useFlag('feedback-widget');

  if (!showFeedback) return null;

  return (
    <div className="fixed bottom-4 right-4">
      <button className="rounded-full bg-blue-600 p-3 text-white shadow-lg">
        💬 Feedback
      </button>
    </div>
  );
}

The useFlag hook returns the flag's current value and automatically re-renders when the value changes (for example, if you toggle the flag in the Rollgate dashboard while the user has the page open).

User Context on the Client

To enable user targeting on the client, pass user context to the provider:

// components/providers.tsx
'use client';

import { RollgateProvider } from '@rollgate/sdk-react';

interface ProvidersProps {
  children: React.ReactNode;
  user?: {
    id: string;
    plan: string;
    email: string;
  };
}

export function Providers({ children, user }: ProvidersProps) {
  return (
    <RollgateProvider
      apiKey={process.env.NEXT_PUBLIC_ROLLGATE_CLIENT_KEY!}
      baseUrl="https://api.rollgate.io"
      context={user ? {
        userId: user.id,
        attributes: { plan: user.plan, email: user.email },
      } : undefined}
    >
      {children}
    </RollgateProvider>
  );
}

Middleware Feature Flags

Next.js middleware runs before any rendering happens. This makes it the ideal location for feature flag evaluations that control routing: A/B testing entire pages, geo-based redirects, or blocking access to unreleased sections.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { RollgateClient } from '@rollgate/sdk-node';

const rollgate = new RollgateClient({
  apiKey: process.env.ROLLGATE_SERVER_KEY!,
  baseUrl: 'https://api.rollgate.io',
});

export async function middleware(request: NextRequest) {
  // Use a cookie for consistent assignment
  const userId = request.cookies.get('visitor-id')?.value ?? crypto.randomUUID();

  const newPricingPage = await rollgate.isEnabled('new-pricing-page', {
    userId,
  });

  // A/B test: rewrite to a different page variant
  if (request.nextUrl.pathname === '/pricing' && newPricingPage) {
    const response = NextResponse.rewrite(new URL('/pricing-b', request.url));
    // Persist the visitor ID so they always see the same variant
    response.cookies.set('visitor-id', userId, { maxAge: 60 * 60 * 24 * 30 });
    return response;
  }

  // Gate access to unreleased features
  const betaEnabled = await rollgate.isEnabled('beta-access', { userId });

  if (request.nextUrl.pathname.startsWith('/beta') && !betaEnabled) {
    return NextResponse.redirect(new URL('/waitlist', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/pricing', '/beta/:path*'],
};

This pattern is especially powerful for A/B testing landing pages. Users are routed to different page variants before any JavaScript runs, so there is zero layout shift and search engines see a complete page.

Combining Server and Client Flags

In many cases, you evaluate a flag on the server and pass the result to a Client Component. This avoids a second network request on the client and eliminates the loading state.

// app/page.tsx (Server Component)
import { rollgate } from '@/lib/rollgate';
import { getCurrentUser } from '@/lib/auth';
import { HeroBanner } from '@/components/hero-banner';

export default async function HomePage() {
  const user = await getCurrentUser();

  const showNewHero = await rollgate.isEnabled('new-hero-banner', {
    userId: user?.id,
  });

  return (
    <main>
      <HeroBanner variant={showNewHero ? 'new' : 'classic'} />
      {/* ... */}
    </main>
  );
}
// components/hero-banner.tsx (Client Component)
'use client';

import { useState } from 'react';

interface HeroBannerProps {
  variant: 'new' | 'classic';
}

export function HeroBanner({ variant }: HeroBannerProps) {
  const [email, setEmail] = useState('');

  if (variant === 'new') {
    return (
      <section className="bg-gradient-to-r from-blue-600 to-purple-600 py-20">
        <h1 className="text-5xl font-bold text-white">Ship Faster with Feature Flags</h1>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter your email"
          className="mt-6 rounded-lg px-4 py-3"
        />
      </section>
    );
  }

  return (
    <section className="bg-gray-900 py-16">
      <h1 className="text-4xl font-bold text-white">Feature Flag Management</h1>
    </section>
  );
}

This pattern gives you the best of both worlds: server-evaluated flags (fast, no flash) with client-side interactivity (state, event handlers).

Feature Flags in API Routes

Next.js Route Handlers are regular server-side code. Use @rollgate/sdk-node the same way you would in any Node.js backend:

// app/api/search/route.ts
import { NextResponse } from 'next/server';
import { rollgate } from '@/lib/rollgate';
import { getCurrentUser } from '@/lib/auth';

export async function GET(request: Request) {
  const user = await getCurrentUser();
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q') ?? '';

  const useNewSearchAlgorithm = await rollgate.isEnabled('search-v2', {
    userId: user?.id,
    attributes: { plan: user?.plan },
  });

  const results = useNewSearchAlgorithm
    ? await searchV2(query)
    : await searchV1(query);

  return NextResponse.json({ results });
}

This is useful for gating backend behavior: new algorithms, different rate limits per plan, or enabling experimental API responses.

// app/api/export/route.ts
import { NextResponse } from 'next/server';
import { rollgate } from '@/lib/rollgate';
import { getCurrentUser } from '@/lib/auth';

export async function POST(request: Request) {
  const user = await getCurrentUser();

  const csvExportEnabled = await rollgate.isEnabled('csv-export', {
    userId: user?.id,
    attributes: { plan: user?.plan },
  });

  if (!csvExportEnabled) {
    return NextResponse.json(
      { error: 'CSV export is not available for your account' },
      { status: 403 }
    );
  }

  const data = await request.json();
  const csv = await generateCsv(data);

  return new NextResponse(csv, {
    headers: { 'Content-Type': 'text/csv' },
  });
}

Performance Considerations

Feature flags add network requests. In a Next.js app with multiple rendering environments, this can become a bottleneck if you are not careful. Here is how to keep things fast.

Cache Flag Values

The Rollgate Node SDK caches flag evaluations by default. On the server, flag values are cached in memory and refreshed periodically, so subsequent isEnabled() calls for the same flag do not make additional API requests.

// lib/rollgate.ts
import { RollgateClient } from '@rollgate/sdk-node';

export const rollgate = new RollgateClient({
  apiKey: process.env.ROLLGATE_SERVER_KEY!,
  baseUrl: 'https://api.rollgate.io',
  cache: {
    ttl: 30_000, // Cache flag values for 30 seconds
  },
});

Avoid Waterfalls

Never evaluate flags sequentially when you can evaluate them in parallel:

// Bad: sequential — each flag waits for the previous one
const flagA = await rollgate.isEnabled('flag-a', ctx);
const flagB = await rollgate.isEnabled('flag-b', ctx);
const flagC = await rollgate.isEnabled('flag-c', ctx);

// Good: parallel — all flags evaluated at once
const [flagA, flagB, flagC] = await Promise.all([
  rollgate.isEnabled('flag-a', ctx),
  rollgate.isEnabled('flag-b', ctx),
  rollgate.isEnabled('flag-c', ctx),
]);

SSE for Real-Time Updates

The React SDK uses Server-Sent Events to receive flag changes in real time. When you toggle a flag in the Rollgate dashboard, connected clients update within seconds — without polling. This is particularly useful for kill switches, where you need to disable a feature across all active sessions immediately.

Minimize Client-Side Flag Checks

Evaluate flags on the server whenever possible. Each client-side flag check requires the browser to fetch flag values from the Rollgate API. If a flag controls static content that does not change after page load, evaluate it in a Server Component and pass the result as props.

Testing Feature Flags in Next.js

Feature flags introduce branching logic. Every flag creates two code paths, and both need testing.

Testing Server Components

Mock the Rollgate client in your tests to control flag values:

// __tests__/dashboard.test.tsx
import { render, screen } from '@testing-library/react';
import { rollgate } from '@/lib/rollgate';
import DashboardPage from '@/app/dashboard/page';

jest.mock('@/lib/rollgate', () => ({
  rollgate: {
    isEnabled: jest.fn(),
  },
}));

describe('DashboardPage', () => {
  it('renders the new dashboard when flag is enabled', async () => {
    (rollgate.isEnabled as jest.Mock).mockResolvedValue(true);

    const page = await DashboardPage();
    render(page);

    expect(screen.getByText('New Dashboard')).toBeInTheDocument();
  });

  it('renders the legacy dashboard when flag is disabled', async () => {
    (rollgate.isEnabled as jest.Mock).mockResolvedValue(false);

    const page = await DashboardPage();
    render(page);

    expect(screen.getByText('Legacy Dashboard')).toBeInTheDocument();
  });
});

Testing Client Components

The React SDK exports a MockRollgateProvider for tests:

// __tests__/feedback-widget.test.tsx
import { render, screen } from '@testing-library/react';
import { MockRollgateProvider } from '@rollgate/sdk-react/testing';
import { FeedbackWidget } from '@/components/feedback-widget';

describe('FeedbackWidget', () => {
  it('renders when flag is enabled', () => {
    render(
      <MockRollgateProvider flags={{ 'feedback-widget': true }}>
        <FeedbackWidget />
      </MockRollgateProvider>
    );

    expect(screen.getByText('💬 Feedback')).toBeInTheDocument();
  });

  it('does not render when flag is disabled', () => {
    render(
      <MockRollgateProvider flags={{ 'feedback-widget': false }}>
        <FeedbackWidget />
      </MockRollgateProvider>
    );

    expect(screen.queryByText('💬 Feedback')).not.toBeInTheDocument();
  });
});

Test Both Code Paths

A common mistake is only testing the "flag on" path. Always test both variants. Feature flags are temporary — when you remove a flag, you want confidence that the remaining code path works correctly.

FAQ

Can I use feature flags with Next.js Static Site Generation (SSG)?

Yes, but with a caveat. Statically generated pages are built at build time, so the flag value is baked into the HTML. If you change the flag after the build, the static page will not reflect the change until the next build (or ISR revalidation). For dynamic flags, use Server Components with dynamic rendering or evaluate flags on the client.

Do feature flags affect SEO?

When evaluated on the server, no. The search engine crawler sees the fully rendered HTML, just like any other page. When evaluated on the client, flags that control visible content can cause layout shift, which may impact Core Web Vitals. Prefer server-side evaluation for SEO-critical content.

How do I handle feature flags during build time?

Use environment variables to distinguish between build and runtime. For static pages, you can evaluate flags at build time using generateStaticParams or getStaticProps (Pages Router). For App Router, prefer runtime evaluation in Server Components with export const dynamic = 'force-dynamic'.

What happens if the Rollgate API is down?

The Rollgate SDKs include built-in resilience: circuit breakers, retry logic, and local caching. If the API is unreachable, the SDK serves the last known flag values from its cache. You can also configure default values that apply when no cached data is available. See the SDK documentation for configuration options.

How many feature flags should I use in one page?

There is no hard limit, but keep it practical. If you have more than five flags on a single page, consider whether some of them can be consolidated. Each flag is a maintenance cost — when you are done with a rollout, clean up the flag to reduce complexity.

Should I use server or client flags?

Use this decision tree:

  • Does the flag control content visible on initial page load? Server.
  • Does the flag control interactive behavior after load? Client.
  • Does the flag control routing or access? Middleware.
  • Does the flag control backend logic? API route.

Get Started

Implementing feature flags in Next.js does not require rethinking your architecture. The patterns above map directly to how Next.js App Router already works: async Server Components fetch data on the server, Client Components handle interactivity, middleware handles routing, and API routes handle backend logic. Feature flags slot into each of these naturally.

Rollgate gives you a single dashboard to manage flags across all these environments, with SDKs built specifically for each pattern.

Ready to add feature flags to your Next.js app?