nest-feature-guard
Version:
A powerful, NestJS-first feature flag guard and decorator library with Redis caching support. Perfect for implementing feature toggles, A/B testing, gradual rollouts, and user-specific feature access control.
1,268 lines (1,010 loc) โข 35.4 kB
Markdown
<h1 align="center">
nest-feature-guard
</h1>
<p align="center">
<a href="https://www.npmjs.com/package/nest-feature-guard"><img alt="NPM version" src="https://img.shields.io/npm/v/nest-feature-guard.svg"></a>
<a href="https://www.npmjs.com/package/nest-feature-guard"><img alt="NPM downloads" src="https://img.shields.io/npm/dw/nest-feature-guard.svg"></a>
<a href="https://www.paypal.com/donate?hosted_button_id=Z9NGDEGSC3LPY" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"></a>
</p>
A powerful, NestJS-first feature flag guard and decorator library that provides fine-grained access control with Redis caching support. Perfect for implementing feature toggles, A/B testing, gradual rollouts, and user-specific feature access.
## ๐ Features
- **NestJS Integration**: First-class support for NestJS with decorators and guards
- **Flexible Access Control**: Support for user-specific, role-based, and global feature flags
- **Multiple Scopes**: Controller-level access control and service-level feature detection
- **Redis Caching**: High-performance Redis backend with customizable key prefixes
- **Pluggable Architecture**: Implement custom cache backends via the `FeatureGuardStore` interface
- **TypeScript Support**: Full TypeScript support with comprehensive type definitions
- **Admin Override**: Automatic admin user bypass for all feature flags
- **Request Enhancement**: Automatic feature flag state injection into request objects
## ๐ฆ Installation
### Basic Installation
```bash
npm install nest-feature-guard
# or
yarn add nest-feature-guard
# or
pnpm add nest-feature-guard
```
### With Redis Support
If you want to use the built-in Redis implementation:
```bash
npm install nest-feature-guard ioredis
# or
yarn add nest-feature-guard ioredis
# or
pnpm add nest-feature-guard ioredis
```
**Note**: `ioredis` is an optional peer dependency. You only need to install it if you plan to use the `RedisFeatureGuardStore`. You can implement custom storage backends using the `FeatureGuardStore` interface without Redis.
## ๐๏ธ Architecture Overview
The library consists of several key components:
- **FeatureGuard**: The main guard that enforces feature flag access control
- ****: Decorator for applying feature flags to routes and methods
- **FeatureGuardStore**: Interface for implementing custom cache backends
- **RedisFeatureGuardStore**: Default Redis implementation
- **FeatureFlagScope**: Enum defining different scopes (CONTROLLER, SERVICE)
## ๐ Quick Start
### Module Setup
Configure the FeatureGuardModule by choosing your storage backend:
```typescript
import { Module } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import Redis from 'ioredis'; // Only needed for Redis option
import {
FeatureGuard,
RedisFeatureGuardStore, // Only needed for Redis option
FEATURE_GUARD_STORE,
FEATURE_GUARD_REFLECTOR,
} from 'nest-feature-guard';
import { YourCustomStore } from './your-custom-store'; // Only needed for Custom option
// Redis setup (skip if using custom store)
const redis = new Redis({
host: 'localhost',
port: 6379,
});
export class FeatureGuardModule {}
```
**๐ก Setup Guide:**
1. **For Redis**: Uncomment Redis provider, comment out Custom provider
2. **For Custom Store**: Uncomment Custom provider, comment out Redis provider
3. **For Custom Store**: See [Custom Store Implementation](#-custom-store-implementation) for example implementations
### Request Interface Setup
Ensure your request interface includes the required fields:
```typescript
import { Request } from 'express';
export interface AppRequest extends Request {
__user_id?: string;
__is_admin?: boolean;
__feature_flags?: Record<string, boolean>;
}
```
### Middleware for User Context
Set up middleware to populate user information:
```typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Response } from 'express';
import { AppRequest } from './interfaces/app-request.interface';
export class AuthMiddleware implements NestMiddleware {
async use(req: AppRequest, res: Response, next: NextFunction) {
// Extract user information from JWT, session, etc.
const user = await this.extractUserFromToken(req);
req.__user_id = user?.id;
req.__is_admin = user?.isAdmin || false;
req.__feature_flags = {};
next();
}
}
```
## ๐ฏ Usage Examples
### Controller-Level Access Control
Use `` to protect entire routes:
```typescript
import { Controller, Get, Post, Body } from '@nestjs/common';
import { FeatureFlag, FeatureFlagScope } from 'nest-feature-guard';
export class BetaController {
// Only users with 'beta_access' flag can access this endpoint
getBetaDashboard() {
return { message: 'Welcome to the beta dashboard!' };
}
// Multiple feature flags can be applied in a single decorator
submitBetaFeedback( feedback: any) {
return { message: 'Feedback submitted successfully' };
}
}
```
### Service-Level Feature Detection
Use `SERVICE` scope to detect feature flags without blocking access.
```typescript
import { Controller, Get, Req } from '@nestjs/common';
import { FeatureFlag, FeatureFlagScope } from 'nest-feature-guard';
import { AppRequest } from './interfaces/app-request.interface';
export class ProductController {
constructor(private readonly productService: ProductService) {}
async getProducts( request: AppRequest) {
// request.__feature_flags will be populated with the feature flag states
return this.productService.getProducts(request);
}
async getEnhancedProducts( request: AppRequest) {
// All feature flag states are now available on request.__feature_flags
return this.productService.getEnhancedProducts(request);
}
}
```
**Key Points:**
- `SERVICE` scope always allows requests to proceed
- Feature flag states are set on `request.__feature_flags`
### Business Logic Integration
Services handle the actual business logic using feature flags:
```typescript
import { Injectable } from '@nestjs/common';
import { FeatureGuard } from 'nest-feature-guard';
import { AppRequest } from './interfaces/app-request.interface';
export class ProductService {
async getProducts(request: AppRequest) {
const products = await this.fetchBasicProducts();
// Business logic: Check feature flags and enhance accordingly
if (FeatureGuard.isFeatureEnabled(request, 'enhanced_search')) {
return this.addEnhancedSearchCapabilities(products);
}
return products;
}
async getEnhancedProducts(request: AppRequest) {
let products = await this.fetchBasicProducts();
// Progressive enhancement based on multiple flags
if (FeatureGuard.isFeatureEnabled(request, 'enhanced_search')) {
products = this.addSearchCapabilities(products);
}
if (FeatureGuard.isFeatureEnabled(request, 'ai_recommendations')) {
products = await this.addAIRecommendations(products);
}
return products;
}
}
```
### Advanced Usage Patterns
Complex scenarios, A/B testing, and progressive enhancement:
```typescript
export class AnalyticsController {
async getDashboard( request: AppRequest) {
const basicData = await this.getBasicAnalytics();
// Progressive feature enhancement
if (FeatureGuard.isFeatureEnabled(request, 'advanced_charts')) {
basicData.charts = await this.getAdvancedCharts();
}
if (FeatureGuard.isFeatureEnabled(request, 'real_time_data')) {
basicData.realTime = await this.getRealTimeData();
}
if (FeatureGuard.isFeatureEnabled(request, 'export_functionality')) {
basicData.exportOptions = this.getExportOptions();
}
return basicData;
}
// A/B testing example
async getExperimentalFeature( request: AppRequest) {
// Determine which variant to show
if (FeatureGuard.isFeatureEnabled(request, 'variant_a')) {
return this.getVariantA();
}
if (FeatureGuard.isFeatureEnabled(request, 'variant_b')) {
return this.getVariantB();
}
return this.getDefaultVariant();
}
}
```
## ๐ง Feature Flag Management
### Setting Feature Flags
```typescript
import { Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { FEATURE_GUARD_STORE, FeatureGuardStore } from 'nest-feature-guard';
export class FeatureManagementService {
constructor(
private readonly store: FeatureGuardStore,
) {}
// Enable feature for specific users
async enableFeatureForUsers(flag: string, userIds: string[]) {
await this.store.setFeatureFlag({
flag,
enabled: true,
userIds,
});
}
// Enable feature globally
async enableFeatureGlobally(flag: string) {
await this.store.setFeatureFlag({
flag,
enabled: true,
// No userIds = global access
});
}
// Disable feature (when disabled, no one has access regardless of userIds)
async disableFeature(flag: string) {
await this.store.setFeatureFlag({
flag,
enabled: false,
});
}
}
```
### Feature Flag Behavior Matrix
| `enabled` | `userIds` | Behavior |
| --------- | ---------------------- | ---------------------------------------------------- |
| `true` | `[]` (empty/undefined) | **Global Access**: Everyone has access |
| `true` | `['user1', 'user2']` | **Targeted Access**: Only listed users have access |
| `false` | `[]` (empty/undefined) | **Global Deny**: No one has access |
| `false` | `['user1', 'user2']` | **Global Deny**: No one has access (userIds ignored) |
### Dynamic Feature Flag Updates
```typescript
export class FeatureRolloutService {
constructor(
private readonly store: FeatureGuardStore,
) {}
// Gradual rollout: start with 10% of users
async startGradualRollout(flag: string, allUserIds: string[], percentage: number = 10) {
const sampleSize = Math.floor(allUserIds.length * (percentage / 100));
const selectedUsers = allUserIds.slice(0, sampleSize);
await this.store.setFeatureFlag({
flag,
enabled: true,
userIds: selectedUsers,
});
}
// A/B testing setup
async setupABTest(flagA: string, flagB: string, userIds: string[]) {
const midpoint = Math.floor(userIds.length / 2);
const groupA = userIds.slice(0, midpoint);
const groupB = userIds.slice(midpoint);
await Promise.all([
this.store.setFeatureFlag({ flag: flagA, enabled: true, userIds: groupA }),
this.store.setFeatureFlag({ flag: flagB, enabled: true, userIds: groupB }),
]);
}
}
```
## ๐ Custom Store Implementation
Implement your own store backend using the `FeatureGuardStore` interface. Here are examples for different storage strategies:
### In-Memory Store (Development/Testing)
```typescript
import { Injectable } from '@nestjs/common';
import { FeatureGuardStore, SetFeatureFlagOptions } from 'nest-feature-guard';
export class InMemoryFeatureStore implements FeatureGuardStore {
private features = new Map<string, { enabled: boolean; userIds?: string[] }>();
async setFeatureFlag({ flag, enabled, userIds }: SetFeatureFlagOptions): Promise<void> {
this.features.set(flag, { enabled, userIds });
}
async getFeature(flag: string): Promise<{ enabled: boolean; userIds?: string[] } | null> {
return this.features.get(flag) || null;
}
async hasFeatureFlag(flag: string, userId: string): Promise<boolean> {
const feature = this.features.get(flag);
if (!feature || !feature.enabled) return false;
if (!feature.userIds || feature.userIds.length === 0) return true;
return feature.userIds.includes(userId);
}
}
```
### Database Store (Production)
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FeatureGuardStore, SetFeatureFlagOptions } from 'nest-feature-guard';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
// TypeORM Entity for feature flags
export class FeatureFlag {
id: number;
flag: string;
enabled: boolean;
userIds: string[] | null;
createdAt: Date;
updatedAt: Date;
}
export class DatabaseFeatureStore implements FeatureGuardStore {
constructor(
private readonly featureFlagRepository: Repository<FeatureFlag>,
) {}
async setFeatureFlag({ flag, enabled, userIds }: SetFeatureFlagOptions): Promise<void> {
await this.featureFlagRepository.upsert(
{
flag,
enabled,
userIds: userIds || null,
},
['flag'], // conflict target
);
}
async getFeature(flag: string): Promise<{ enabled: boolean; userIds?: string[] } | null> {
const feature = await this.featureFlagRepository.findOne({ where: { flag } });
if (!feature) return null;
return {
enabled: feature.enabled,
userIds: feature.userIds || undefined,
};
}
async hasFeatureFlag(flag: string, userId: string): Promise<boolean> {
const feature = await this.featureFlagRepository.findOne({ where: { flag } });
if (!feature || !feature.enabled) return false;
if (!feature.userIds || feature.userIds.length === 0) return true;
return feature.userIds.includes(userId);
}
}
// Module setup with Database Store
export class FeatureGuardDatabaseModule {}
```
## ๐งช Testing
### Unit Testing Feature Guards
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FeatureGuard, FeatureGuardStore } from 'nest-feature-guard';
describe('FeatureGuard', () => {
let guard: FeatureGuard;
let store: jest.Mocked<FeatureGuardStore>;
let reflector: jest.Mocked<Reflector>;
beforeEach(async () => {
const mockStore = {
getFeature: jest.fn(),
hasFeatureFlag: jest.fn(),
setFeatureFlag: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
FeatureGuard,
{ provide: 'FeatureGuardStore', useValue: mockStore },
{ provide: 'FeatureGuardReflector', useValue: { get: jest.fn() } },
],
}).compile();
guard = module.get<FeatureGuard>(FeatureGuard);
store = module.get('FeatureGuardStore');
reflector = module.get('FeatureGuardReflector');
});
it('should allow access when feature is enabled and user has flag', async () => {
const mockContext = createMockExecutionContext({
__user_id: 'test-user',
__is_admin: false,
});
reflector.get.mockReturnValue('test_feature');
store.getFeature.mockResolvedValue({ enabled: true });
store.hasFeatureFlag.mockResolvedValue(true);
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
});
});
```
### Integration Testing
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { FeatureGuardModule } from './feature-guard.module';
describe('Feature Flag Integration', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [FeatureGuardModule, TestModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('should deny access when feature is disabled', () => {
return request(app.getHttpServer())
.get('/beta/dashboard')
.set('Authorization', 'Bearer valid-token')
.expect(403);
});
it('should allow access when feature is enabled', async () => {
// Enable feature for test user
await featureStore.setFeatureFlag({
flag: 'beta_access',
enabled: true,
userIds: ['test-user-id'],
});
return request(app.getHttpServer())
.get('/beta/dashboard')
.set('Authorization', 'Bearer valid-token')
.expect(200);
});
});
```
## ๐ Monitoring and Analytics
### Feature Flag Usage Tracking
```typescript
export class FeatureAnalyticsService {
constructor(
private readonly store: FeatureGuardStore,
) {}
async trackFeatureUsage(flag: string, userId: string, used: boolean) {
// Track feature flag usage for analytics
await this.analyticsService.track({
event: 'feature_flag_checked',
userId,
properties: {
flag,
used,
timestamp: new Date(),
},
});
}
async getFeatureUsageStats(flag: string): Promise<FeatureUsageStats> {
// Implementation for getting usage statistics
return this.analyticsService.getFeatureStats(flag);
}
}
```
## ๐ Security Considerations
### Admin Override Protection
```typescript
export class SecureFeatureGuard extends FeatureGuard {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// Log admin overrides for security auditing
if (request.__is_admin) {
await this.auditService.logAdminOverride({
userId: request.__user_id,
endpoint: request.url,
timestamp: new Date(),
});
}
return super.canActivate(context);
}
}
```
### Rate Limiting for Feature Flags
```typescript
export class RateLimitedFeatureGuard extends FeatureGuard {
constructor(
store: FeatureGuardStore,
reflector: Reflector,
private readonly rateLimiter: RateLimiterService,
) {
super(store, reflector);
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// Apply rate limiting to feature flag checks
await this.rateLimiter.checkLimit(
`feature-check:${request.__user_id}`,
100, // 100 checks per minute
60, // 1 minute window
);
return super.canActivate(context);
}
}
```
## ๐ Frontend Integration
### Feature Flag API for Frontend Access Management
The library can be extended to provide feature flags to frontend applications for client-side route protection and UI feature toggling.
```typescript
import { Controller, Get, Query, Req } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { FEATURE_GUARD_STORE, FeatureGuardStore } from 'nest-feature-guard';
export interface GetFeatureFlagsQuery {
userId?: string;
includeInternal?: boolean; // Whether to include internal/admin-only flags
}
export interface FeatureFlagResponse {
flag: string;
enabled: boolean;
hasAccess: boolean;
scope: 'public' | 'user' | 'admin';
}
export class FeatureFlagsController {
constructor(
private readonly store: FeatureGuardStore,
private readonly featureFlagService: FeatureFlagService,
) {}
/**
* Get all feature flags accessible to the current user
* This endpoint is used by frontend applications for route protection and UI features
*/
async getFeatureFlags(
query: GetFeatureFlagsQuery,
request: any,
): Promise<Record<string, boolean>> {
const userId = query.userId || request.__user_id || 'anonymous';
const isAdmin = request.__is_admin === true;
// Get all configured feature flags
const allFlags = await this.featureFlagService.getAllFlags();
const result: Record<string, boolean> = {};
for (const flagConfig of allFlags) {
const { flag, enabled, visibility } = flagConfig;
// Check if user has access to this specific flag
const hasUserAccess = await this.store.hasFeatureFlag(flag, userId);
// Determine final access based on visibility and user permissions
let hasAccess = false;
if (isAdmin) {
// Admins have access to all enabled flags
hasAccess = enabled;
} else if (visibility === 'public') {
// Public flags: enabled globally or user has specific access
hasAccess = enabled && (hasUserAccess || !flagConfig.userIds?.length);
} else if (visibility === 'user') {
// User-specific flags: only if user has explicit access
hasAccess = enabled && hasUserAccess;
}
// 'admin' visibility flags are not included for non-admin users
if (query.includeInternal || visibility !== 'admin') {
result[flag] = hasAccess;
}
}
return result;
}
/**
* Get detailed feature flag information (for admin dashboards)
*/
async getDetailedFeatureFlags( request: any): Promise<FeatureFlagResponse[]> {
const userId = request.__user_id;
const isAdmin = request.__is_admin === true;
if (!isAdmin) {
throw new ForbiddenException('Admin access required');
}
const allFlags = await this.featureFlagService.getAllFlags();
const result: FeatureFlagResponse[] = [];
for (const flagConfig of allFlags) {
const hasUserAccess = userId
? await this.store.hasFeatureFlag(flagConfig.flag, userId)
: false;
result.push({
flag: flagConfig.flag,
enabled: flagConfig.enabled,
hasAccess: hasUserAccess,
scope: flagConfig.visibility,
});
}
return result;
}
}
export class FeatureFlagService {
constructor(
private readonly store: FeatureGuardStore,
) {}
async getAllFlags(): Promise<
Array<{
flag: string;
enabled: boolean;
visibility: 'public' | 'user' | 'admin';
userIds?: string[];
}>
> {
// This would typically come from your database or configuration
// Here's a simplified example - implement based on your storage strategy
return [
{ flag: 'new_dashboard', enabled: true, visibility: 'public' },
{ flag: 'beta_features', enabled: true, visibility: 'user', userIds: ['beta_user1'] },
{ flag: 'admin_panel', enabled: true, visibility: 'admin' },
{ flag: 'experimental_ui', enabled: false, visibility: 'user' },
];
}
}
```
### Frontend Usage Examples
#### React Route Protection
```typescript
// hooks/useFeatureFlags.ts
import { useEffect, useState } from 'react';
interface FeatureFlags {
[key: string]: boolean;
}
export function useFeatureFlags(userId?: string): {
flags: FeatureFlags;
loading: boolean;
error: Error | null;
} {
const [flags, setFlags] = useState<FeatureFlags>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function fetchFlags() {
try {
setLoading(true);
const params = userId ? `?userId=${userId}` : '';
const response = await fetch(`/api/feature-flags${params}`);
if (!response.ok) {
throw new Error('Failed to fetch feature flags');
}
const data = await response.json();
setFlags(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
fetchFlags();
}, [userId]);
return { flags, loading, error };
}
// components/ProtectedRoute.tsx
import React from 'react';
import { useFeatureFlags } from '../hooks/useFeatureFlags';
interface ProtectedRouteProps {
featureFlag: string;
children: React.ReactNode;
fallback?: React.ReactNode;
userId?: string;
}
export function ProtectedRoute({
featureFlag,
children,
fallback = null,
userId,
}: ProtectedRouteProps) {
const { flags, loading } = useFeatureFlags(userId);
if (loading) {
return <div>Loading...</div>;
}
if (!flags[featureFlag]) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// App.tsx - Usage example
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ProtectedRoute } from './components/ProtectedRoute';
function App() {
const userId = getCurrentUserId(); // Your auth logic
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<HomePage />} />
{/* Protected beta dashboard */}
<Route
path='/beta'
element={
<ProtectedRoute
featureFlag='beta_dashboard'
userId={userId}
fallback={<div>Beta access required</div>}
>
<BetaDashboard />
</ProtectedRoute>
}
/>
{/* Admin-only routes */}
<Route
path='/admin'
element={
<ProtectedRoute
featureFlag='admin_panel'
userId={userId}
fallback={<div>Admin access required</div>}
>
<AdminPanel />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
}
```
#### Vue.js Integration
```typescript
// composables/useFeatureFlags.ts
import { ref, onMounted } from 'vue';
export function useFeatureFlags(userId?: string) {
const flags = ref<Record<string, boolean>>({});
const loading = ref(true);
const error = ref<Error | null>(null);
const fetchFlags = async () => {
try {
loading.value = true;
const params = userId ? `?userId=${userId}` : '';
const response = await fetch(`/api/feature-flags${params}`);
if (!response.ok) {
throw new Error('Failed to fetch feature flags');
}
flags.value = await response.json();
} catch (err) {
error.value = err as Error;
} finally {
loading.value = false;
}
};
onMounted(fetchFlags);
return { flags, loading, error, refetch: fetchFlags };
}
// components/FeatureGate.vue
<template>
<div v-if="!loading">
<slot v-if="hasAccess" />
<slot v-else name="fallback" />
</div>
<div v-else>Loading...</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useFeatureFlags } from '../composables/useFeatureFlags';
interface Props {
flag: string;
userId?: string;
}
const props = defineProps<Props>();
const { flags, loading } = useFeatureFlags(props.userId);
const hasAccess = computed(() => flags.value[props.flag] === true);
</script>
// Usage in components
<template>
<div>
<h1>Dashboard</h1>
<FeatureGate flag="new_analytics" :userId="currentUser.id">
<NewAnalyticsWidget />
<template #fallback>
<LegacyAnalyticsWidget />
</template>
</FeatureGate>
<FeatureGate flag="beta_features" :userId="currentUser.id">
<BetaFeaturesPanel />
</FeatureGate>
</div>
</template>
```
#### Angular Service
```typescript
// services/feature-flag.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
export class FeatureFlagService {
private flagsSubject = new BehaviorSubject<Record<string, boolean>>({});
public flags$ = this.flagsSubject.asObservable();
constructor(private http: HttpClient) {}
loadFlags(userId?: string): Observable<Record<string, boolean>> {
const params = userId ? { userId } : {};
return this.http.get<Record<string, boolean>>('/api/feature-flags', { params }).pipe(
map((flags) => {
this.flagsSubject.next(flags);
return flags;
}),
catchError((error) => {
console.error('Failed to load feature flags:', error);
return this.flagsSubject.asObservable();
}),
);
}
isEnabled(flag: string): Observable<boolean> {
return this.flags$.pipe(map((flags) => flags[flag] === true));
}
isEnabledSync(flag: string): boolean {
return this.flagsSubject.value[flag] === true;
}
}
// guards/feature-flag.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FeatureFlagService } from '../services/feature-flag.service';
export class FeatureFlagGuard implements CanActivate {
constructor(private featureFlagService: FeatureFlagService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const requiredFlag = route.data['featureFlag'];
if (!requiredFlag) {
return new Observable((observer) => observer.next(true));
}
return this.featureFlagService.isEnabled(requiredFlag).pipe(
map((enabled) => {
if (!enabled) {
this.router.navigate(['/access-denied']);
return false;
}
return true;
}),
);
}
}
// app-routing.module.ts
const routes: Routes = [
{
path: 'beta',
component: BetaDashboardComponent,
canActivate: [FeatureFlagGuard],
data: { featureFlag: 'beta_dashboard' },
},
{
path: 'admin',
component: AdminPanelComponent,
canActivate: [FeatureFlagGuard],
data: { featureFlag: 'admin_panel' },
},
];
```
### Security Considerations for Frontend Integration
```typescript
// Middleware to ensure secure feature flag exposure
export class FeatureFlagSecurityMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
// Rate limiting for feature flag requests
// Audit logging for sensitive flag access
// User context validation
next();
}
}
// Enhanced controller with security
export class SecureFeatureFlagsController {
// Cache responses for performance
async getFeatureFlags( request: AuthenticatedRequest) {
// Validate user context
if (!request.user) {
throw new UnauthorizedException('Authentication required');
}
// Log access for audit trail
this.auditService.logFeatureFlagAccess({
userId: request.user.id,
timestamp: new Date(),
endpoint: 'getFeatureFlags',
});
// Return flags with security filtering
return this.getFilteredFlags(request.user);
}
private async getFilteredFlags(user: User): Promise<Record<string, boolean>> {
// Implementation that filters sensitive flags based on user permissions
// and applies additional security rules
}
}
```
## ๐ Performance Optimization
### Caching Strategies
```typescript
export class OptimizedFeatureFlagStore implements FeatureGuardStore {
private readonly localCache = new Map<string, any>();
private readonly cacheTTL = 60000; // 1 minute
constructor(private readonly redis: Redis) {}
async getFeature(flag: string): Promise<{ enabled: boolean; userIds?: string[] } | null> {
// Check local cache first
const cacheKey = `feature:${flag}`;
const cached = this.localCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.data;
}
// Fallback to Redis
const result = await this.getFeatureFromRedis(flag);
// Cache locally
this.localCache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
return result;
}
}
```
## ๐ API Reference
### FeatureGuard Class
#### Methods
- `canActivate(context: ExecutionContext): Promise<boolean>`
- Main guard method that determines access based on feature flags
- Returns `true` if access is granted, `false` otherwise
- `static isFeatureEnabled(request: FeatureGuardRequest, flag: string): boolean`
- Utility method to check feature flag status in business logic
- Returns `true` if the feature is enabled for the current request
### Decorator
#### Signature
```typescript
FeatureFlag(flags: string | string[], options?: FeatureFlagOptions): MethodDecorator
```
#### Parameters
- `flags`: The feature flag identifier(s) - can be a single string or an array of strings
- `options`: Optional configuration object
- `scope`: `FeatureFlagScope.CONTROLLER` (default) or `FeatureFlagScope.SERVICE`
#### Behavior
When multiple flags are provided (either as an array or multiple decorators):
- **CONTROLLER scope**: ALL flags must be enabled and the user must have access to ALL flags
- **SERVICE scope**: All flags are evaluated and their states are set on the request object
#### Examples
```typescript
// Single flag
// Multiple flags in array (ALL must be satisfied)
// Multiple decorators (ALL must be satisfied)
// Service scope with multiple flags
```
### FeatureGuardStore Interface
#### Methods
- `setFeatureFlag(options: SetFeatureFlagOptions): Promise<void>`
- `getFeature(flag: string): Promise<{ enabled: boolean; userIds?: string[] } | null>`
- `hasFeatureFlag(flag: string, userId: string): Promise<boolean>`
## ๐ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ๐ Acknowledgments
- Built for the NestJS ecosystem
- Inspired by modern feature flag management best practices
- Thanks to all users!