@moonspot/phlag-client
v0.2.0
Published
JavaScript/TypeScript client library for the Phlag feature flag management system
Maintainers
Readme
Phlag Client
JavaScript/TypeScript client library for the Phlag feature flag management system
This library provides a simple, type-safe interface for querying feature flags from a Phlag server. It handles authentication, environment management, error handling, and caching so you can focus on feature rollouts.
Features
- 🎯 Type-safe flag retrieval - Get boolean, number, or string values with full TypeScript support
- 🌐 Environment-aware - Configure once, query a specific environment
- 🔄 Multi-environment fallback - Query multiple environments with automatic fallback (v0.2.0+)
- 🔄 Immutable environment switching - Easy multi-environment queries
- ⚡ Simple API - Clean, fluent interface with convenience methods
- 🛡️ Robust error handling - Specific exceptions for different error conditions
- 💾 Built-in caching - Optional file-based (Node.js) and in-memory caching
- ✅ Fully tested - Comprehensive test coverage with Vitest
Requirements
- Node.js 18.0.0 or higher
- A running Phlag server instance
Installation
npm install @moonspot/phlag-clientQuick Start
import { PhlagClient } from '@moonspot/phlag-client';
// Create a client for a specific environment
const client = new PhlagClient({
baseUrl: 'http://localhost:8000',
apiKey: 'your-64-character-api-key',
environment: 'production',
});
// Check if a feature is enabled
if (await client.isEnabled('feature_checkout')) {
// Show the new checkout flow
}
// Get typed configuration values
const maxItems = await client.getFlag('max_items'); // returns number or null
const priceMultiplier = await client.getFlag('price_multiplier'); // returns number or null
const welcomeMessage = await client.getFlag('welcome_message'); // returns string or nullExamples
The examples/ directory contains practical usage examples:
- basic.js/ts - Simple feature flag checks
- multi-environment.js - Querying flags across environments
- caching.js - Performance optimization with caching
- cache-warming.js - Preloading cache at startup
- error-handling.js - Handling different error types
- configuration.js - Advanced configuration options
See examples/README.md for details on running examples.
Performance & Caching
For high-traffic applications, enable caching to dramatically reduce API calls:
const client = new PhlagClient({
baseUrl: 'http://localhost:8000',
apiKey: 'your-api-key',
environment: 'production',
cache: true, // Enable caching
cacheTtl: 300, // Cache for 5 minutes (default)
});
// First call fetches all flags from API (1 request)
const enabled = await client.isEnabled('feature_checkout');
// Subsequent calls use cached data (0 requests)
const max = await client.getFlag('max_items');
const price = await client.getFlag('price_multiplier');How Caching Works
When caching is enabled:
- First request: Client fetches ALL flags for the environment via
/all-flagsendpoint - Cache storage: Flags stored in memory AND persisted to disk (Node.js)
- Subsequent requests: Served from in-memory cache (no API calls)
- Cache expiration: After TTL expires, next request refreshes from API
- Cross-request persistence: Cache file survives between Node.js process restarts
Cache Management
Warming the cache (preload before first request):
await client.warmCache(); // Immediately fetches and caches all flagsClearing the cache (force fresh fetch):
await client.clearCache(); // Removes cache file and in-memory data
// Next request will fetch fresh from API
const value = await client.getFlag('feature');Checking cache status:
if (client.isCacheEnabled()) {
console.log('Cache file:', client.getCacheFile());
console.log('TTL:', client.getCacheTtl(), 'seconds');
}When to Use Caching
✅ Good use cases:
- High-traffic applications with frequent flag checks
- Flags that change infrequently (hourly, daily)
- Reducing API load and network latency
- Improving response times (sub-millisecond flag checks)
❌ When to avoid caching:
- You need real-time flag updates (seconds matter)
- Flags change very frequently
- Low-traffic applications (caching overhead not worth it)
- Single flag check per request
Performance Impact
Without caching:
- API calls: N (one per
getFlag()call) - Network overhead: ~10-50ms per call
- Total overhead: N × 10-50ms
With caching:
- API calls: 1 per TTL period (default 5 minutes)
- First request: ~10-50ms (fetch all flags)
- Subsequent requests: <1ms (memory lookup)
- Cache file I/O: ~1-2ms on first load per process
Example savings (100 flag checks per request, 1000 requests/minute):
- Without cache: 100,000 API calls/minute
- With cache (300s TTL): ~20 API calls/minute (99.98% reduction)
API Reference
PhlagClient
constructor(options: PhlagClientOptions)
Creates a new client instance.
Parameters:
options.baseUrl- Base URL of your Phlag server (e.g.,http://localhost:8000)options.apiKey- 64-character API key from the Phlag admin paneloptions.environment- Environment name or array of environments for fallback (e.g.,'production'or['my-branch', 'staging'])options.timeout- Request timeout in milliseconds (default:10000)options.cache- Enable caching (default:false)options.cacheFile- Custom cache file path (default: auto-generated in temp dir)options.cacheTtl- Cache time-to-live in seconds (default:300)
async getFlag(name: string): Promise<FlagValue>
Retrieves a flag value.
Returns: The flag value (boolean, number, string, or null)
Throws:
AuthenticationError- Invalid API keyInvalidFlagError- Flag doesn't exist (cache disabled only)InvalidEnvironmentError- Environment doesn't existNetworkError- Network communication failedPhlagError- Other errors
async isEnabled(name: string): Promise<boolean>
Convenience method for checking SWITCH flags.
Returns: true if the flag value is boolean true, false otherwise
getEnvironment(): string[]
Gets the current environment(s) as an array.
Returns: Always returns an array, even for single environments
- Single environment:
['production'] - Multiple environments:
['my-branch', 'staging', 'development']
Note: In v1.x this returned a string. See migration notes below.
withEnvironment(environment: string | string[]): PhlagClient
Creates a new client for a different environment or environment chain (immutable pattern).
async warmCache(): Promise<void>
Preloads the flag cache immediately. No-op if caching is disabled.
async clearCache(): Promise<void>
Clears in-memory and file cache. No-op if caching is disabled.
isCacheEnabled(): boolean
Checks if caching is enabled.
getCacheFile(): string
Gets the cache file path (even if file doesn't exist yet).
getCacheTtl(): number
Gets the cache TTL in seconds.
Error Handling
The client throws specific exceptions for different error conditions:
import {
PhlagError,
AuthenticationError,
InvalidFlagError,
NetworkError,
} from '@moonspot/phlag-client';
try {
const value = await client.getFlag('my_flag');
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid API key (401)
console.error('Bad API key:', error.message);
} else if (error instanceof InvalidFlagError) {
// Flag doesn't exist (404) - only when cache disabled
console.error('Flag not found:', error.message);
} else if (error instanceof NetworkError) {
// Connection failed, timeout, etc.
console.error('Network error:', error.message);
} else if (error instanceof PhlagError) {
// Other errors
console.error('Phlag error:', error.message);
}
}All exceptions extend PhlagError, so you can catch them all with a single block if needed.
Multi-Environment Fallback
New in v0.2.0: Query multiple environments with automatic fallback. Perfect for dev/QA workflows where you want to test branch-specific flags with fallback to shared environments.
How it Works
When you configure multiple environments, the client queries them in order and returns the first non-null value:
const client = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'your-api-key',
environment: ['my-feature-branch', 'staging', 'development'],
});
// Queries environments in order:
// 1. Check 'my-feature-branch' first
// 2. If null, check 'staging'
// 3. If still null, check 'development'
const value = await client.getFlag('new_checkout_flow');Important: Only null triggers the fallback. Valid values like false, 0, or "" will NOT fall through:
// Example flag states across environments:
// my-feature-branch: null (flag not set)
// staging: false (flag explicitly disabled)
// development: true (flag enabled)
// Result: false (from staging, stops there)
const enabled = await client.isEnabled('feature_beta');Use Cases
Branch-specific development:
// Test your branch's flags, fall back to staging for unset flags
const client = new PhlagClient({
baseUrl: 'http://localhost:8000',
apiKey: 'dev-api-key',
environment: ['feature-new-checkout', 'staging'],
});QA testing with overrides:
// QA environment with fallback to production
const client = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'api-key',
environment: ['qa-overrides', 'production'],
cache: true,
});Gradual rollout testing:
// Test canary deployment flags with production fallback
const client = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'api-key',
environment: ['canary', 'production'],
});Performance with Multiple Environments
Without caching:
- Queries environments in parallel using
Promise.all() - Returns as soon as first non-null value found
- Network overhead: max(environment response times)
With caching:
- Fetches ALL environments' flags in parallel on first request
- Merges caches with first environment taking precedence
- Subsequent requests: <1ms (memory lookup)
const client = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'api-key',
environment: ['branch-123', 'staging', 'dev'],
cache: true,
cacheTtl: 300,
});
// First request: Fetches all 3 environments in parallel (1 round trip)
const value1 = await client.getFlag('feature_a');
// Subsequent requests: Pure memory lookup
const value2 = await client.getFlag('feature_b');Checking Current Environment(s)
const client = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'api-key',
environment: ['my-branch', 'staging'],
});
// v0.2.0: Always returns array
console.log(client.getEnvironment()); // ['my-branch', 'staging']
// Single environment still returns array
const prodClient = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'api-key',
environment: 'production',
});
console.log(prodClient.getEnvironment()); // ['production']Working with Multiple Environments
You can switch environments without creating new client instances:
const prodClient = new PhlagClient({
baseUrl: 'http://phlag.example.com',
apiKey: 'your-api-key',
environment: 'production',
cache: true,
});
// Create a new client for staging (immutable pattern)
const stagingClient = prodClient.withEnvironment('staging');
// Each has its own cache
console.log(prodClient.getEnvironment()); // ['production']
console.log(stagingClient.getEnvironment()); // ['staging']
// Query both environments
const prodEnabled = await prodClient.isEnabled('feature_beta');
const stagingEnabled = await stagingClient.isEnabled('feature_beta');
// Switch to multi-environment fallback
const devClient = prodClient.withEnvironment(['my-branch', 'staging', 'dev']);
console.log(devClient.getEnvironment()); // ['my-branch', 'staging', 'dev']Migration from v0.1.x to v0.2.0
Breaking Change: getEnvironment() Return Type
v1.x:
const env = client.getEnvironment(); // Returns: 'production' (string)v0.2.0:
const env = client.getEnvironment(); // Returns: ['production'] (string[])Migration:
If you're checking the environment name:
// v1.x
if (client.getEnvironment() === 'production') { ... }
// v0.2.0 - Option 1: Check first element
if (client.getEnvironment()[0] === 'production') { ... }
// v0.2.0 - Option 2: Check if array includes value
if (client.getEnvironment().includes('production')) { ... }If you're logging the environment:
// v1.x
console.log('Environment:', client.getEnvironment()); // "production"
// v0.2.0
console.log('Environment:', client.getEnvironment()); // ["production"]
console.log('Environment:', client.getEnvironment().join(', ')); // "production"New Features in v0.2.0
- ✅ Multi-environment fallback support
- ✅ Parallel environment queries for better performance
- ✅
withEnvironment()now accepts string arrays - ✅ Improved caching for multi-environment scenarios
Development
Building
npm run buildTesting
npm testProject Status
This library is production-ready and actively maintained:
Version 0.2.0 (Current)
- ✅ Multi-environment fallback support
- ✅ Parallel environment queries
- ✅ Enhanced caching for multiple environments
- ✅ Breaking change:
getEnvironment()returnsstring[]
Version 1.0.0
- ✅ TypeScript project setup
- ✅ HTTP client with fetch API
- ✅ Error handling with specific exception types
- ✅ PhlagClient with getFlag() and isEnabled()
- ✅ In-memory & file-based caching (Node.js)
- ✅ Cache management methods
- ✅ Comprehensive test suite
- ✅ TypeScript type definitions
Troubleshooting
Stale Cache Data
If you're seeing outdated flag values when caching is enabled:
Problem: Cache hasn't expired yet
Solutions:
- Use shorter TTL:
new PhlagClient({ ..., cache: true, cacheTtl: 60 }) - Manually clear:
await client.clearCache() - Disable caching if you need real-time updates
Cache File Permissions
If cache files aren't being created:
Problem: No write permission to temp directory or custom cache path
Solution: Ensure Node.js has write access to the cache directory
const cacheFile = client.getCacheFile();
const cacheDir = require('path').dirname(cacheFile);
console.log('Cache directory:', cacheDir);Note: Cache write failures are logged but don't throw exceptions. The client gracefully degrades to in-memory-only caching.
License
BSD 3-Clause License
Copyright (c) 2025, Brian Moon
Development
This project uses ESLint for linting and Prettier for code formatting. See DEVELOPMENT.md for detailed development setup, available scripts, and workflow guidelines.
Quick commands:
npm run build- Compile TypeScriptnpm test- Run test suitenpm run format- Format code with Prettiernpm run lint- Check code with ESLint (requiresnpm install)
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines on:
- Setting up your development environment
- Coding standards and style guide
- Testing requirements
- Pull request process
Credits
Built by Brian Moon ([email protected])
Support
For bugs and feature requests, please use the GitHub issue tracker.
For questions, contact [email protected].
