@joemark0008/feature-flags
v1.2.1
Published
Framework-agnostic feature flags library with REST API integration
Maintainers
Readme
@joemark0008/feature-flags
A framework-agnostic TypeScript library for feature flags with REST API integration and React support.
Features
- 🎯 Framework Agnostic: Core library works with any JavaScript framework
- ⚛️ React Integration: Built-in hooks and provider for React applications
- 🌐 REST API Support: Flexible adapter system for any REST API backend
- 💾 Multiple Storage Options: REST API, localStorage, or in-memory storage
- 🔄 Caching: Intelligent caching with configurable timeout
- 🛡️ Type Safe: Full TypeScript support with strong typing
- 🎚️ Fallback Strategy: Graceful degradation when API is unavailable
- 📊 Multi-tenant: Organization and user-scoped flag management
Installation
npm install @joemark0008/feature-flagsFor React integration, ensure you have React as a peer dependency:
npm install react🚀 Quick Start with Demo
Want to see it in action first? Try our complete demo:
git clone https://github.com/joemark0008/feature-flag.git
cd feature-flag/demo/feature-flags-demo
npm install
npm run devOpen http://localhost:5173 to explore all features interactively!
Quick Start
Basic Setup
import { FeatureFlagManager, RestApiAdapter, createRestApiConfig } from '@joemark0008/feature-flags'
// Define your feature flags
const flagConfigs = [
{
key: 'dashboardAccess',
label: 'Dashboard Access',
description: 'Allow users to access the dashboard',
category: 'access',
defaultValue: false,
},
{
key: 'newFeature',
label: 'New Feature',
description: 'Enable the new experimental feature',
category: 'feature',
defaultValue: false,
},
]
// Configure REST API
const apiConfig = createRestApiConfig('https://your-api.com', 'your-auth-token')
// Create storage adapter
const storage = new RestApiAdapter(apiConfig)
// Initialize manager
const flagManager = new FeatureFlagManager(storage, flagConfigs, {
cacheTimeout: 5 * 60 * 1000, // 5 minutes
enableLocalStorage: true, // Fallback to localStorage
})
// Use in your application
const context = { organizationId: 'org-123', userId: 'user-456' }
const isDashboardEnabled = await flagManager.isEnabled('dashboardAccess', context)
if (isDashboardEnabled) {
// Show dashboard
}React Integration
import React from 'react'
import { FeatureFlagProvider, useFeatureFlag } from '@joemark0008/feature-flags/react'
function App() {
const context = {
organizationId: user.organizationId,
userId: user.id,
}
return (
<FeatureFlagProvider
manager={flagManager}
context={context}
loadingComponent={LoadingSpinner}
>
<Dashboard />
</FeatureFlagProvider>
)
}
function Dashboard() {
const hasDashboardAccess = useFeatureFlag('dashboardAccess')
const hasNewFeature = useFeatureFlag('newFeature')
if (!hasDashboardAccess) {
return <AccessDenied />
}
return (
<div>
<h1>Dashboard</h1>
{hasNewFeature && <NewFeatureComponent />}
</div>
)
}API Reference
FeatureFlagManager
The core class for managing feature flags.
class FeatureFlagManager {
constructor(
storage: FeatureFlagStorage,
flagConfigs: FeatureFlagConfig[],
options?: FeatureFlagManagerOptions
)
async isEnabled(flagKey: string, context: FeatureFlagContext): Promise<boolean>
async getFlags(context: FeatureFlagContext): Promise<Record<string, boolean>>
async setFlag(flagKey: string, enabled: boolean, context: FeatureFlagContext, scope?: 'organization' | 'user'): Promise<void>
async setFlags(flags: Record<string, boolean>, context: FeatureFlagContext, scope?: 'organization' | 'user'): Promise<void>
async getFlagDefinitions(): Promise<FeatureFlagConfig[]>
clearCache(): void
}Storage Adapters
RestApiAdapter
import { RestApiAdapter } from '@joemark0008/feature-flags'
const adapter = createFeatureFlagAdapter(process.env.NODE_ENV)Demo Application
🎯 Try the live demo! A complete React application showcasing all features:
cd demo/feature-flags-demo
npm install
npm run devDemo Features:
- 🔄 Real-time flag toggling with admin panel
- 🎨 Dynamic theming (light/dark mode)
- 💾 localStorage persistence
- 📱 Responsive design with beautiful UI
- 🔧 Developer tools for debugging
- 📚 Complete code examples
The demo includes 5 sample feature flags demonstrating:
- Dashboard redesign toggle
- Dark mode theme switching
- Premium features access control
- Beta chat functionality
- Advanced analytics display
Examples
#### LocalStorageAdapter
```typescript
import { LocalStorageAdapter } from '@joemark0008/feature-flags'
const adapter = new LocalStorageAdapter('my-feature-flags')MemoryAdapter
Memory Adapter
For testing:
import { MemoryAdapter } from '@joemark0008/feature-flags'
const memoryAdapter = new MemoryAdapter({ newDashboard: true }, flagConfigs)
const manager = new FeatureFlagManager(memoryAdapter, flagConfigs)GraphQL Adapter
For GraphQL APIs:
import { GraphQLAdapter } from '@joemark0008/feature-flags'
const graphqlAdapter = new GraphQLAdapter({
endpoint: 'https://your-api.com/graphql',
headers: { Authorization: 'Bearer token' },
authInterceptor: (headers) => ({
...headers,
'X-API-Key': process.env.API_KEY
})
})
const manager = new FeatureFlagManager(graphqlAdapter, flagConfigs)Using GraphQL Adapter with Existing APIs
The GraphQL adapter is designed to work seamlessly with your existing GraphQL infrastructure. Here are common integration patterns:
1. Custom Queries for Existing Schema
Map the adapter to your existing GraphQL types:
const adapter = new GraphQLAdapter({
endpoint: 'https://your-existing-api.com/graphql',
headers: {
'Authorization': 'Bearer your-token'
},
queries: {
// Map to your existing query structure
getFlags: `
query GetFeatureToggles($userId: ID!, $orgId: ID!) {
user(id: $userId) {
organization(id: $orgId) {
featureToggles {
name
isActive
metadata
}
}
}
}
`,
setFlag: `
mutation UpdateToggle($input: UpdateToggleInput!) {
updateFeatureToggle(input: $input) {
id
success
toggle {
name
isActive
}
}
}
`
}
})2. Integration with Apollo Client
Reuse your existing Apollo Client setup:
import { ApolloClient } from '@apollo/client'
const apolloClient = new ApolloClient({...})
// Use the same endpoint and auth
const featureFlagAdapter = new GraphQLAdapter({
endpoint: apolloClient.link.options.uri,
authInterceptor: (headers) => ({
...headers,
// Reuse Apollo's auth logic
'Authorization': apolloClient.cache.data?.authToken
}),
queries: {
getFlags: `
query GetFeatureFlags($context: FeatureFlagContextInput!) {
currentUser {
featureAccess(context: $context) {
feature
hasAccess
}
}
}
`
}
})3. Relay Integration
For Relay-style pagination and connections:
// Use the built-in Relay support
const relayAdapter = GraphQLAdapter.withRelay({
endpoint: 'https://your-relay-api.com/graphql',
authInterceptor: (headers) => ({
...headers,
'Authorization': `Bearer ${getAuthToken()}`
})
})
// Or customize Relay queries
const customRelayAdapter = new GraphQLAdapter({
endpoint: 'https://api.example.com/graphql',
queries: {
getFlags: `
query GetFeatureFlags($context: FeatureFlagContextInput!, $first: Int, $after: String) {
viewer {
featureFlags(context: $context, first: $first, after: $after) {
edges {
node {
key
enabled
rolloutPercentage
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`
}
})4. Custom Data Transformation
Handle different response formats by extending the adapter:
class CustomGraphQLAdapter extends GraphQLAdapter {
async getFlags(context: FeatureFlagContext): Promise<Record<string, boolean>> {
const operation = {
query: this.queries.getFlags,
variables: { context },
}
const response = await this.executeGraphQL(operation)
// Transform your custom response format
const flags: Record<string, boolean> = {}
// Example: nested structure transformation
response.data?.user?.organization?.featureToggles?.forEach(toggle => {
flags[toggle.name] = toggle.isActive
})
return flags
}
}
const adapter = new CustomGraphQLAdapter({
endpoint: 'https://your-api.com/graphql',
// ... your config
})5. Environment-Specific Configuration
Handle different environments with existing GraphQL endpoints:
const createFeatureFlagAdapter = (environment: string) => {
const configs = {
development: {
endpoint: 'http://localhost:4000/graphql',
authInterceptor: (headers) => ({
...headers,
'x-api-key': 'dev-key'
})
},
production: {
endpoint: 'https://api.yourcompany.com/graphql',
authInterceptor: (headers) => ({
...headers,
'Authorization': `Bearer ${getProductionToken()}`
})
}
}
return new GraphQLAdapter({
...configs[environment],
queries: {
getFlags: `
query GetFeatureFlags($env: Environment!, $context: UserContext!) {
featureFlags(environment: $env, context: $context) {
key
enabled
rolloutPercentage
}
}
`
}
})
}
const adapter = createFeatureFlagAdapter(process.env.NODE_ENV)6. Error Handling for Existing APIs
Handle your API's specific error patterns:
const adapter = new GraphQLAdapter({
endpoint: 'https://your-api.com/graphql',
authInterceptor: (headers) => {
const token = localStorage.getItem('authToken')
return {
...headers,
'Authorization': token ? `Bearer ${token}` : undefined
}
},
queries: {
getFlags: `
query GetFlags($context: FeatureFlagContextInput!) {
featureAccess(input: $context) {
... on FeatureAccessSuccess {
flags {
name
enabled
}
}
... on FeatureAccessError {
message
code
}
}
}
`
}
})React Hooks
// Get feature flags context
const { flags, loading, error, refetch } = useFeatureFlags()
// Check individual flag
const isEnabled = useFeatureFlag('flagKey')
// Toggle flags (admin UI)
const toggleFlag = useFeatureFlagToggle()
await toggleFlag('flagKey', true, 'organization')
// Get flags with state
const { flags, loading, error, refetch } = useFeatureFlagsWithState()Backend Integration
Your REST API should implement these endpoints:
# Get flags for context
GET /api/feature-flags?organizationId=org123&userId=user456
Response: { "dashboardAccess": true, "newFeature": false }
# Set individual flag
PUT /api/feature-flags/dashboardAccess
Body: { "enabled": true, "context": { "organizationId": "org123" }, "scope": "organization" }
PUT /api/feature-flags/:flagKey
Body: { "enabled": true, "context": { "organizationId": "org123" }, "scope": "organization" }
# Bulk update flags
PUT /api/feature-flags
Body: {
"flags": { "newDashboard": true, "betaFeatures": false },
"context": { "organizationId": "org123" },
"scope": "organization"
}
# Get flag definitions
GET /api/feature-flags/definitions
Response: [{ "key": "newDashboard", "label": "New Dashboard", ... }]GraphQL API Integration
For GraphQL APIs, your schema should support these operations:
# Schema types
type FeatureFlag {
key: String!
enabled: Boolean!
}
type FeatureFlagDefinition {
key: String!
label: String!
description: String!
category: String!
defaultValue: Boolean!
dependencies: [String!]
}
input FeatureFlagContextInput {
organizationId: String
userId: String
environment: String
}
input SetFeatureFlagInput {
flagKey: String!
enabled: Boolean!
context: FeatureFlagContextInput!
scope: String
}
# Queries
type Query {
featureFlags(context: FeatureFlagContextInput!): [FeatureFlag!]!
featureFlagDefinitions: [FeatureFlagDefinition!]!
}
# Mutations
type Mutation {
setFeatureFlag(input: SetFeatureFlagInput!): SetFeatureFlagResult!
setFeatureFlags(input: SetFeatureFlagsInput!): SetFeatureFlagsResult!
}Configuration
Manager Options
interface FeatureFlagManagerOptions {
cacheTimeout?: number // Cache timeout in ms (default: 5 minutes)
fallbackToDefaults?: boolean // Use defaults on error (default: true)
enableLocalStorage?: boolean // Use localStorage fallback (default: true)
onError?: (error: Error) => void // Error handler
}Flag Configuration
interface FeatureFlagConfig {
key: string // Unique identifier
label: string // Human-readable name
description: string // Description for admin UI
category: 'access' | 'feature' | 'experiment' // Flag category
defaultValue: boolean // Default value when not set
dependencies?: string[] // Other flags this depends on
}Context
interface FeatureFlagContext {
organizationId?: string // Organization scope
userId?: string // User scope
userRoles?: string[] // User roles for permission-based flags
environment?: string // Environment (dev, staging, prod)
[key: string]: any // Additional context properties
}Advanced Usage
Custom Authentication
// REST API with custom auth
const apiAdapter = new RestApiAdapter({
baseUrl: 'https://your-api.com',
endpoints: { /* ... */ },
authInterceptor: (config) => ({
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${getAuthToken()}`,
'X-API-Key': process.env.API_KEY
}
})
})
// GraphQL with custom authentication
const graphqlAdapter = new GraphQLAdapter({
endpoint: 'https://your-api.com/graphql',
authInterceptor: (headers) => ({
...headers,
'Authorization': `Bearer ${getAuthToken()}`,
'X-Tenant-ID': getCurrentTenant()
})
})Custom GraphQL Queries
const customGraphQLAdapter = GraphQLAdapter.withCustomQueries({
endpoint: 'https://your-api.com/graphql',
queries: {
getFlags: `
query GetUserFeatureFlags($userId: ID!, $orgId: ID!) {
user(id: $userId) {
organization(id: $orgId) {
featureFlags {
name
isEnabled
rolloutPercentage
}
}
}
}
`,
setFlag: `
mutation ToggleFeature($input: ToggleFeatureInput!) {
toggleFeature(input: $input) {
feature {
name
enabled
}
errors {
field
message
}
}
}
`
}
})
// Relay-style pagination support
const relayAdapter = GraphQLAdapter.withRelay({
endpoint: 'https://your-api.com/graphql',
headers: { Authorization: 'Bearer token' }
})Examples
Admin Toggle Component
import { useFeatureFlagToggle, useFeatureFlags } from '@joemark0008/feature-flags/react'
function AdminPanel() {
const { flags } = useFeatureFlags()
const toggleFlag = useFeatureFlagToggle()
const handleToggle = async (flagKey: string) => {
try {
await toggleFlag(flagKey, !flags[flagKey])
} catch (error) {
console.error('Failed to toggle flag:', error)
}
}
return (
<div>
{Object.entries(flags).map(([key, enabled]) => (
<div key={key}>
<label>
<input
type="checkbox"
checked={enabled}
onChange={() => handleToggle(key)}
/>
{key}
</label>
</div>
))}
</div>
)
}Custom Storage Adapter
import { FeatureFlagStorage } from '@joemark0008/feature-flags'
class GraphQLAdapter implements FeatureFlagStorage {
async getFlags(context: FeatureFlagContext): Promise<Record<string, boolean>> {
// Implement GraphQL query
}
async setFlag(flagKey: string, enabled: boolean, context: FeatureFlagContext): Promise<void> {
// Implement GraphQL mutation
}
// ... other methods
}License
MIT
Contributing
We welcome contributions! Please see our contributing guidelines for more details.
