Integrating GitLab Feature Flags in Next.js


In modern software development, the ability to release features quickly and safely is paramount. Feature flags (also known as feature toggles) have emerged as a critical technique, allowing teams to decouple code deployment from feature releases. GitLab, a comprehensive DevOps platform, offers a built-in Feature Flag module that integrates seamlessly into your workflow.
This article provides a comprehensive guide to understanding and utilizing GitLab Feature Flags, illustrated with conceptual examples inspired by a Next.js application integration.
What are Feature Flags?
At its core, a feature flag is a conditional switch in your code that allows you to turn specific functionality ON or OFF remotely, without needing to redeploy your application. Think of it as a remote control for your application's features.
Key Benefits:
Decoupled Deployment & Release: Ship code to production disabled, then enable it for users when ready.
Risk Reduction: Gradually roll out features to subsets of users (Canary Releases, Percentage Rollouts). Quickly disable faulty features without a rollback.
A/B Testing: Expose different feature variations (variants) to different user segments.
Trunk-Based Development: Merge unfinished features to the main branch safely behind a flag.
Operational Control: Enable/disable features for maintenance or based on system load.
GitLab Feature Flags: The Control Plane
GitLab provides a user-friendly interface within your project to manage feature flags. It leverages the open-source Unleash feature flag system's specification, making its API compatible with standard Unleash client SDKs.
1. Creating a Flag:
Navigate to
Deployments
->Feature Flags
in your GitLab project.Click
New feature flag
.Name: A unique identifier used in your code (e.g.,
new-checkout-flow
,enable-beta-dashboard
).Description: Explain what the flag controls.
Active (Main Toggle): This is the master switch. If Inactive, the flag is always OFF. If Active, the strategies below determine the outcome. Start with Inactive for safety.
2. Understanding Strategies:
Strategies define when a flag should be considered ON, assuming its main toggle is Active. GitLab evaluates strategies based on context provided by your application (via an SDK).
Strategy Types: GitLab offers various types:
All Users: Activates the flag for everyone (often used as a simple ON switch if the main toggle is Active).
Percent Rollout: Activates the flag for a random percentage of users.
User IDs: Activates the flag for specific logged-in user IDs.
Environments: Activates the flag based on the application's current running environment (e.g.,
development
,staging
,production
). This is our focus.(Other types may exist or be added)
3. Environment-Based Activation (Key Concept):
This is crucial for progressive rollouts (Dev -> UAT -> Prod). The logic is:
IF
Main Toggle
isActive
ANDApplication's Current Environment
MATCHES anEnvironment Scope
listed in an activeEnvironments Strategy
THEN Flag is ON (for that request)ELSE Flag is OFF (for that request)
Configuration: When adding/editing an "Environments" strategy, you'll select predefined "Environment Scopes" from a list (these scopes often correspond to environments defined in your GitLab CI/CD settings, like
development
,staging
,production
).You list only the environment scopes where you want the flag to be potentially active within that strategy.
(Based on user-provided context, the UI allows selecting these scopes rather than setting an explicit ON/OFF action within the strategy itself).
Integrating with Your Application (Conceptual Next.js Example)
You'll need an Unleash-compatible SDK in your application to communicate with the GitLab Feature Flag API. Let's consider @unleash/nextjs
.
1. Configuration (.env
- Placeholders):
# Your GitLab project's Unleash API endpoint
UNLEASH_SERVER_API_URL=https://gitlab.example.com/api/v4/feature_flags/unleash/YOUR_PROJECT_ID
# Instance ID generated in GitLab Feature Flag settings
UNLEASH_SERVER_INSTANCE_ID=YOUR_INSTANCE_ID_SECRET
# Name identifying your app to GitLab/Unleash
UNLEASH_APP_NAME=your-application-name
2. Core Logic (Conceptual utils/featureService.ts
):
This service fetches definitions from GitLab, caches them server-side briefly, and evaluates flags.
// utils/featureService.ts
import { getDefinitions, evaluateFlags, flagsClient } from '@unleash/nextjs';
interface DefinitionCacheEntry {
definitions: any; // Type from Unleash SDK ideally
timestamp: number;
}
// Simple server-side cache for raw definitions (per environment)
const definitionStore: Record<string, DefinitionCacheEntry> = {};
const DEFINITION_TTL = 60 * 1000; // Cache definitions for 1 minute
/**
* Fetches and evaluates all flags for a given environment context.
*/
async function getEvaluatedFlags(environment: string) {
const now = Date.now();
const cacheEntry = definitionStore[environment];
let definitions;
// Check server-side cache
if (cacheEntry && now - cacheEntry.timestamp < DEFINITION_TTL) {
definitions = cacheEntry.definitions;
} else {
try {
// Fetch raw flag rules from GitLab
definitions = await getDefinitions({
// Optional: Next.js fetch options e.g., for revalidation
fetchOptions: { next: { revalidate: 30 } },
});
// Update cache
definitionStore[environment] = { definitions, timestamp: now };
} catch (error) {
console.error(`Failed to fetch flag definitions for env ${environment}:`, error);
// On error, return a client where all flags are off
return flagsClient([]); // Use empty toggles for default-off client
}
}
// Evaluate the fetched rules against the current context
const context = { environment };
const { toggles } = evaluateFlags(definitions, context);
// Return a client object to check flags easily
const client = flagsClient(toggles);
// Optional: Send metrics asynchronously
client.sendMetrics().catch(err => console.warn('Failed sending metrics:', err));
return client;
}
/**
* Checks if a single feature is enabled for the environment.
*/
export async function isFeatureActive(
featureName: string,
environment: string,
): Promise<boolean> {
const client = await getEvaluatedFlags(environment);
return client.isEnabled(featureName);
}
/**
* Gets the status of multiple features.
*/
export async function checkMultipleFeatures(
featureNames: string[],
environment: string,
): Promise<Record<string, boolean>> {
const client = await getEvaluatedFlags(environment);
const results: Record<string, boolean> = {};
for (const name of featureNames) {
results[name] = client.isEnabled(name);
}
return results;
}
3. Checking Flags in a Next.js App:
Server-Side Rendering (
getServerSideProps
): Fetch flags during SSR for the initial page load.// pages/some-page.tsx (Simplified Example) import type { GetServerSideProps } from 'next'; import { checkMultipleFeatures } from '../utils/featureService'; interface PageProps { serverFlags: Record<string, boolean>; } export const getServerSideProps: GetServerSideProps<PageProps> = async (context) => { // Determine environment (e.g., from NODE_ENV or query params) const environment = process.env.NODE_ENV || 'development'; const featureList = ['new-checkout-flow', 'beta-dashboard']; try { const serverFlags = await checkMultipleFeatures(featureList, environment); return { props: { serverFlags } }; } catch (error) { console.error('SSR Flag fetch error:', error); return { props: { serverFlags: {} } }; // Default flags off on error } }; // --- Your React Component using serverFlags ---
Client-Side Updates (API Route + Hook): Use an API route as a proxy to check flags from the browser, and a hook (e.g., with TanStack Query) for efficient data fetching and caching on the client.
TypeScript
// pages/api/check-flags.ts (Simplified API Route) import type { NextApiRequest, NextApiResponse } from 'next'; import { checkMultipleFeatures } from '../../utils/featureService'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const names = (req.query.names as string)?.split(','); // Determine environment, potentially allowing override via query param const environment = (req.query.environment as string) || process.env.NODE_ENV || 'development'; if (!names || names.length === 0) { return res.status(400).json({ error: 'No feature names provided' }); } try { const flags = await checkMultipleFeatures(names, environment); return res.status(200).json({ flags }); } catch (error) { console.error('API Flag fetch error:', error); return res.status(500).json({ error: 'Failed to fetch flags' }); } }
The hook
// hooks/useFeatureChecks.ts (Simplified Hook with TanStack Query) import { useQuery } from '@tanstack/react-query'; const fetchFlagsFromApi = async (names: string[], environment?: string) => { if (names.length === 0) return {}; const params = new URLSearchParams({ names: names.join(',') }); if (environment) params.append('environment', environment); const response = await fetch(`/api/check-flags?${params.toString()}`); if (!response.ok) throw new Error('Network response was not ok'); const data = await response.json(); return data.flags || {}; // Return flags object or empty object }; export function useFeatureChecks(names: string[], environment?: string) { const queryKey = ['featureFlags', names.sort().join(','), environment ?? 'default']; return useQuery<Record<string, boolean>, Error>({ queryKey: queryKey, queryFn: () => fetchFlagsFromApi(names, environment), staleTime: 60 * 1000, // Cache client-side for 1 minute enabled: names.length > 0, }); }
Example Rollout Workflow (Dev -> UAT -> Prod)
Let's use a flag named new-reporting-ui
.
Initial State (Off Everywhere):
GitLab: Main toggle
Active
. Add an "Environments" strategy but leave the list of environment scopes empty.Result: Flag is OFF in
dev
,uat
,prod
.
Enable for Dev:
GitLab: Edit the strategy, add the
development
environment scope.Result: ON in
dev
, OFF inuat
,prod
.
Enable for UAT:
GitLab: Edit the strategy, add the
uat
environment scope (it now listsdevelopment
,uat
).Result: ON in
dev
,uat
. OFF inprod
.
Enable for Production:
GitLab: Edit the strategy, add the
production
environment scope (it now listsdevelopment
,uat
,production
).Result: ON in
dev
,uat
,prod
.
Rollback (Disable in Prod):
GitLab: Edit the strategy, remove the
production
environment scope.Result: ON in
dev
,uat
. OFF inprod
.
Beyond Simple Toggles: Variants
GitLab/Unleash also supports "Variants". Instead of just ON/OFF, a flag can return a specific named variant (e.g., 'A', 'B', 'control') with an optional payload (JSON data). This is powerful for A/B testing UI variations or remotely configuring components. The SDKs provide methods like getVariant()
to access this.
Conclusion
GitLab Feature Flags provide a powerful, integrated way to manage feature releases, reduce risk, and enable advanced deployment strategies. By understanding the interplay between the main toggle, strategies (especially environment-based ones), and SDK integration, you can gain fine-grained control over your application's functionality directly from the GitLab UI, truly separating deployment from release. Start simple, leverage environment strategies, and explore more advanced features like percentage rollouts and variants as your needs evolve.
Subscribe to my newsletter
Read articles from Opeyemi Ojo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
