@timmons-group/snack-pack
v0.1.2
Published
High-level scaffolding components for rapid serverless API development. This package provides REST API utilities, authentication middleware, and endpoint builders to accelerate application development.
Downloads
957
Maintainers
Keywords
Readme
Shared Node Architecture Component Kit (SNACK) Pack
High-level scaffolding components for rapid serverless API development. This package provides REST API utilities, authentication middleware, and endpoint builders to accelerate application development.
Installation
npm install @timmons-group/snack-packComponents
REST API Utilities
The REST module provides a comprehensive set of utilities for building robust REST APIs with automatic database connectivity, user authentication, and error handling.
Core Providers
provideDatabase
Automatically injects a configured database connection into your handler function.
import { provideDatabase } from '@timmons-group/snack-pack/REST';
const myHandler = async (apiEvent, databaseDriver) => {
const users = await databaseDriver.query('SELECT * FROM users');
return {
statusCode: 200,
body: JSON.stringify(users)
};
};
export const handler = provideDatabase(myHandler);provideUser
Extracts and validates user information from the request, providing authenticated user context.
import { provideUser } from '@timmons-group/snack-pack/REST';
const myHandler = async (apiEvent, user) => {
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello, ${user.name}!` })
};
};
export const handler = provideUser(myHandler);provideErrorHandling
Wraps your handler with comprehensive error handling, automatically returning appropriate HTTP error responses.
import { provideErrorHandling } from '@timmons-group/snack-pack/REST';
const myHandler = async (apiEvent) => {
// If this throws an error, it will be caught and return a 500 response
throw new Error('Something went wrong');
};
export const handler = provideErrorHandling(myHandler);provideRESTDefaults
Combines database connectivity, user authentication, error handling, and permission checking in a single provider.
import { provideRESTDefaults } from '@timmons-group/snack-pack/REST';
const myHandler = async (apiEvent, { databaseDriver, user }) => {
// Access to both database and authenticated user
const userData = await databaseDriver.query(
'SELECT * FROM user_data WHERE user_id = $1',
[user.id]
);
return {
statusCode: 200,
body: JSON.stringify(userData)
};
};
// Requires 'read' and 'write' permissions
export const handler = provideRESTDefaults(myHandler, ['read', 'write']);Permission System
The REST providers support a flexible permission system:
import { provideRESTDefaults } from '@timmons-group/snack-pack/REST';
// Single permission
export const readHandler = provideRESTDefaults(handler, ['read']);
// Multiple permissions (user must have ALL permissions)
export const adminHandler = provideRESTDefaults(handler, ['read', 'write', 'admin']);
// No permissions required
export const publicHandler = provideRESTDefaults(handler, []);REST Endpoint Builder
A fluent API for building REST endpoints with explicit control over which providers to include.
Basic Usage
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
const internalHandler = async (apiEvent, { databaseDriver, user }) => {
const result = await databaseDriver.query('SELECT * FROM data');
return {
statusCode: 200,
body: JSON.stringify(result)
};
};
// Full configuration
export const handler = new RESTEndpointBuilder(internalHandler)
.withDatabase()
.withUser()
.withErrorHandling()
.withPermissions(['read', 'write'])
.build();Simplified Configuration
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
// Equivalent to the above example
export const handler = new RESTEndpointBuilder(internalHandler)
.withRESTDefaults(['read', 'write'])
.build();Selective Providers
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
// Only database and error handling, no authentication
const publicHandler = async (apiEvent, { databaseDriver }) => {
const publicData = await databaseDriver.query('SELECT * FROM public_data');
return { statusCode: 200, body: JSON.stringify(publicData) };
};
export const handler = new RESTEndpointBuilder(publicHandler)
.withDatabase()
.withErrorHandling()
.build();Available Methods
.withDatabase()- Add database connectivity.withUser()- Add user authentication.withErrorHandling()- Add error handling wrapper.withPermissions(permissions)- Add permission checking.withRESTDefaults(permissions)- Add all common providers.build()- Create the final handler
Authentication
Complete OAuth authentication system with Cognito integration and JWT token handling.
OAuth Endpoints
Pre-built OAuth endpoints for handling authentication flow:
import { handler as oAuthHandler } from '@timmons-group/snack-pack/Auth/Endpoints';
// Provides complete OAuth flow endpoints
export const handler = oAuthHandler;Available endpoints:
- POST /oauth/callback - Exchange authorization code for tokens
- POST /oauth/refresh - Refresh access tokens using refresh token
- POST /oauth/logout - Logout and clear authentication cookies
- GET /oauth/config - Get OAuth configuration for client applications
Example usage with API Gateway:
// For serverless.yml or AWS SAM
functions:
oauth:
handler: oauth.handler
events:
- http:
path: oauth/{proxy+}
method: ANYClient-side integration:
The OAuth endpoints are designed to work with the useAuth React hook pattern:
// React client example
const { login, logout, user, isAuthenticated } = useAuth({
oauthConfigUrl: 'https://api.example.com/oauth/config',
callbackUrl: 'https://api.example.com/oauth/callback',
refreshUrl: 'https://api.example.com/oauth/refresh',
logoutUrl: 'https://api.example.com/oauth/logout'
});Authentication Middleware
import { authenticateUser } from '@timmons-group/snack-pack/Auth';
export const handler = async (event) => {
try {
const user = await authenticateUser(event);
// User is authenticated, proceed with business logic
return { statusCode: 200, body: 'Authenticated' };
} catch (error) {
return { statusCode: 401, body: 'Unauthorized' };
}
};Authentication Utilities
The authentication system provides several utility functions for working with users and permissions:
User Management
import {
getUser,
getUserFromID,
getAnonymousUser
} from '@timmons-group/snack-pack/Auth';
// Get user from Lambda event
const user = await getUser(event, databaseDriver);
// Get user by ID (for internal operations)
const user = await getUserFromID('user-123');
// Get anonymous user object
const anonymousUser = await getAnonymousUser(databaseDriver);Permission Checking
import {
hasPermission,
hasAnyPermissions,
hasAllPermissions
} from '@timmons-group/snack-pack/Auth';
// Check single permission
if (hasPermission(user, 'read_users')) {
// User has read_users permission
}
// Check if user has any of the specified permissions
if (hasAnyPermissions(user, ['read_users', 'write_users'])) {
// User has at least one of these permissions
}
// Check if user has all specified permissions
if (hasAllPermissions(user, ['read_users', 'write_users'])) {
// User has both permissions
}Token Utilities
import {
decodeToken,
validateToken,
extractFromToken
} from '@timmons-group/snack-pack/Auth';
// Decode JWT token
const { header, payload } = decodeToken(tokenString);
// Validate token with OAuth provider
const oAuthDriver = await getOAuthDriver();
const validation = await validateToken(decodedToken, tokenString, oAuthDriver);
// Extract user information from token
const { valid, user } = await extractFromToken(tokenString, oAuthDriver, databaseDriver);OAuth Driver
import { getOAuthDriver } from '@timmons-group/snack-pack/Auth';
const oAuthDriver = await getOAuthDriver();
const config = await oAuthDriver.buildOAuthConfig();
const userGroups = await oAuthDriver.getGroups(token);Cognito Integration
import { CognitoUserPool } from '@timmons-group/snack-pack/Auth';
// Configure Cognito user pool
const userPool = new CognitoUserPool({
userPoolId: process.env.USER_POOL_ID,
clientId: process.env.CLIENT_ID
});Complete Examples
Simple CRUD API
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
import { responses } from '@timmons-group/snack-utils';
// GET /users
const getUsers = async (apiEvent, { databaseDriver, user }) => {
const users = await databaseDriver.query('SELECT * FROM users');
return responses.jsonSuccess(users);
};
// POST /users
const createUser = async (apiEvent, { databaseDriver, user }) => {
const userData = JSON.parse(apiEvent.body);
const result = await databaseDriver.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[userData.name, userData.email]
);
return responses.jsonSuccess(result[0]);
};
// Export handlers
export const getUsersHandler = new RESTEndpointBuilder(getUsers)
.withRESTDefaults(['read'])
.build();
export const createUserHandler = new RESTEndpointBuilder(createUser)
.withRESTDefaults(['write'])
.build();Public API with Database Only
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
const getPublicData = async (apiEvent, { databaseDriver }) => {
const data = await databaseDriver.query('SELECT * FROM public_content');
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
};
};
export const handler = new RESTEndpointBuilder(getPublicData)
.withDatabase()
.withErrorHandling()
.build();Authentication-Only Endpoint
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
const getUserProfile = async (apiEvent, { user }) => {
return {
statusCode: 200,
body: JSON.stringify({
id: user.id,
name: user.name,
email: user.email
})
};
};
export const handler = new RESTEndpointBuilder(getUserProfile)
.withUser()
.withErrorHandling()
.build();Configuration
Database Configuration
The REST providers automatically use the global database configuration from @timmons-group/snack-blocks. You can configure this manually or use a configuration file.
Manual Configuration
import { ConfigurationBuilder, setGlobalConfig } from '@timmons-group/snack-blocks';
const config = await new ConfigurationBuilder()
.withSSM(process.env.SSM_PATH + "connectionString")
.build();
setGlobalConfig(config);Configuration File (Recommended for Production)
Create a snack-config.js file and set the SNACK_CONFIG environment variable:
# Environment variable
SNACK_CONFIG=./snack-config.js
# Or use project root alias (requires package.json configuration)
SNACK_CONFIG=@/snack-config.jsProject Root Alias Setup
To use the @/ prefix, configure your package.json with import aliases:
{
"name": "your-project",
"type": "module",
"imports": {
"@/*": "./*"
}
}Complete snack-config.js example:
import { ConfigurationBuilder } from '@timmons-group/snack-blocks';
import { AuthConfigurationBuilder } from '@timmons-group/snack-pack/Auth/ConfigurationBuilder';
// Database Configuration
const databaseConfigurationBuilder = new ConfigurationBuilder();
export const databaseConfiguration = await (
databaseConfigurationBuilder
.withSSM(process.env.SSM_PATH + "connectionString")
.build()
);
// Authentication Configuration
export const authConfiguration = await (
new AuthConfigurationBuilder()
.withCognito(process.env.POOL_ID, process.env.CLIENT_ID)
.withGroupPermissions("SystemAdministrators", ["system_admin"])
.withGroupPermissions("UserManagers", ["read_users", "write_users"])
.withDefaultPermissions(["sign_in", "read_profile"])
.withAnonymousPermissions(["sign_in"])
.build()
);Authentication Configuration
The preferred method for configuring authentication is using the AuthConfigurationBuilder which provides a fluent API for setting up Cognito integration, permissions, and access control.
Using AuthConfigurationBuilder (Recommended)
import { AuthConfigurationBuilder } from '@timmons-group/snack-pack/Auth/ConfigurationBuilder';
const authConfigurationBuilder = new AuthConfigurationBuilder();
export const authConfiguration = await (
authConfigurationBuilder
.withCognito(process.env.POOL_ID, process.env.CLIENT_ID)
.withGroupPermissions("SystemAdministrators", [
TAGS.SYSTEM_ADMIN
])
.withDefaultPermissions([
ACLS.SIGN_IN,
])
.withAnonymousPermissions([
ACLS.SIGN_IN,
])
.build()
);Advanced Authentication Configuration
The AuthConfigurationBuilder supports advanced features for complex authentication scenarios:
Database-Backed Permissions (PAM)
Store permissions in a PostgreSQL database for dynamic permission management:
const authConfiguration = await (
new AuthConfigurationBuilder()
.withCognito(process.env.POOL_ID, process.env.CLIENT_ID)
.withPAMPermissionSource("auth_schema") // Uses auth_schema.groupacl table
.build()
);Required database table structure:
CREATE TABLE auth_schema.groupacl (
groupkey VARCHAR(255) PRIMARY KEY,
acl JSON -- Array of permission strings
);
-- Example data
INSERT INTO auth_schema.groupacl VALUES
('SystemAdministrators', '["system_admin", "user_management"]'),
('UserManagers', '["read_users", "write_users"]');Custom Providers
Extend authentication with custom logic for claims, groups, and permissions:
const authConfiguration = await (
new AuthConfigurationBuilder()
.withCognito(process.env.POOL_ID, process.env.CLIENT_ID)
// Add custom claims to user object
.withCustomClaimProvider(async (user) => {
const profile = await getUserProfile(user.id);
return {
department: profile.department,
level: profile.level,
manager: profile.managerId
};
})
// Add groups from external sources
.withCustomGroupProvider(async (user) => {
const ldapGroups = await getLDAPGroups(user.id);
return ldapGroups;
})
// Add permissions from external systems
.withCustomPermissionsProvider(async (user) => {
const externalPerms = await getExternalPermissions(user.id);
return externalPerms;
})
.build()
);Multiple Permission Sources
Combine multiple permission sources for complex authorization:
const authConfiguration = await (
new AuthConfigurationBuilder()
.withCognito(process.env.POOL_ID, process.env.CLIENT_ID)
// Static permissions (code-defined)
.withGroupPermissions("Administrators", ["admin_access"])
// Database permissions
.withPAMPermissionSource("auth_schema")
// Custom permission source
.withPermissionSource({
name: "external_api",
type: "custom",
provider: async (group) => {
return await fetchPermissionsFromAPI(group);
}
})
.build()
);Complete Advanced Example
const authConfiguration = await (
new AuthConfigurationBuilder()
.withCognito(process.env.POOL_ID, process.env.CLIENT_ID)
// Basic permissions
.withGroupPermissions("SystemAdministrators", ["system_admin"])
.withGroupPermissions("UserManagers", ["read_users", "write_users"])
.withDefaultPermissions(["sign_in", "read_profile"])
.withAnonymousPermissions(["sign_in"])
// Database-backed permissions
.withPAMPermissionSource("auth_schema")
// Custom providers
.withCustomClaimProvider(async (user) => {
// Enrich user with organization data
const orgData = await getOrganizationData(user.id);
return {
organization: orgData.name,
role: orgData.role,
permissions: orgData.customPermissions
};
})
.withCustomGroupProvider(async (user) => {
// Add project-based groups
const projects = await getUserProjects(user.id);
return projects.map(p => `project_${p.id}`);
})
.build()
);Environment Variables
Set the following environment variables for authentication:
# Configuration
SNACK_CONFIG=./snack-config.js # Path to configuration file (recommended)
# Authentication
POOL_ID=your-cognito-user-pool-id
CLIENT_ID=your-cognito-client-id
JWT_SECRET=your-jwt-secret
# Database (if not using config file)
SSM_PATH=your-ssm-base-pathPermission Constants
Define your permission constants for consistent access control:
// Example permission constants
export const ACLS = {
SIGN_IN: 'sign_in',
READ_USERS: 'read_users',
WRITE_USERS: 'write_users',
DELETE_USERS: 'delete_users'
};
export const TAGS = {
SYSTEM_ADMIN: 'system_admin',
USER_MANAGER: 'user_manager',
READ_ONLY: 'read_only'
};Error Handling
All providers include comprehensive error handling:
- Database Errors: Automatically logged and return 500 responses
- Authentication Errors: Return 401 responses for invalid tokens
- Permission Errors: Return 403 responses for insufficient permissions
- Validation Errors: Return 400 responses for malformed requests
Best Practices
- Use RESTDefaults: For most endpoints,
withRESTDefaults()provides everything you need - Granular Permissions: Define specific permissions for different operations
- Error Handling: Always include error handling in your endpoints
- Database Configuration: Set up global database configuration once at startup
- Logging: Use
@timmons-group/snack-utilsloggers for consistent logging
Dependencies
@timmons-group/snack-utils- Logging and response utilities@aws-sdk/client-cognito-identity-provider- AWS Cognito integrationjsonwebtoken- JWT token handlingjwk-to-pem- JWT verification utilities
Exports
// REST providers
import {
provideDatabase,
provideUser,
provideErrorHandling,
provideRESTDefaults
} from '@timmons-group/snack-pack/REST';
// REST endpoint builder
import { RESTEndpointBuilder } from '@timmons-group/snack-pack/REST/EndpointBuilder';
// Authentication configuration
import { AuthConfigurationBuilder } from '@timmons-group/snack-pack/Auth/ConfigurationBuilder';
// Authentication middleware and utilities
import {
getUser,
getUserFromID,
getAnonymousUser,
hasPermission,
hasAnyPermissions,
hasAllPermissions,
decodeToken,
validateToken,
extractFromToken,
getOAuthDriver
} from '@timmons-group/snack-pack/Auth';
// OAuth endpoints
import { handler as oAuthHandler } from '@timmons-group/snack-pack/Auth/Endpoints';Feature Flags
The FeatureFlags module provides a flexible, configurable feature flag system that supports multiple storage backends with caching and environment-based configuration.
Quick Start
import { FeatureFlagConfigurationBuilder, FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Configure feature flags
const config = new FeatureFlagConfigurationBuilder()
.withEnvironmentDefaults() // Auto-configure based on NODE_ENV
.withDefaultFlags([
{ name: 'new-ui', value: false },
{ name: 'enhanced-logging', value: true }
])
.build();
// Check if a feature is enabled
const isEnabled = await FeatureFlags.isEnabled('new-ui', false);Configuration Builder
The FeatureFlagConfigurationBuilder provides a fluent API for configuring your feature flag system:
import { FeatureFlagConfigurationBuilder } from '@timmons-group/snack-pack/FeatureFlags';
const config = new FeatureFlagConfigurationBuilder()
.withEnvironment('production')
.withDynamoDBSource({
tableName: 'feature-flags',
region: 'us-east-1'
})
.withCaching({
enabled: true,
ttl: 300000 // 5 minutes
})
.withDefaultFlags([
{ name: 'feature-a', value: true },
{ name: 'feature-b', value: false }
])
.build();Builder Methods
withEnvironment(environment)
Sets the environment for the feature flag configuration.
const config = new FeatureFlagConfigurationBuilder()
.withEnvironment('staging')
.build();withEnvironmentDefaults()
Auto-configures based on NODE_ENV environment variable:
production: DynamoDB source with caching enableddevelopment/test: In-memory source- Default: In-memory source
const config = new FeatureFlagConfigurationBuilder()
.withEnvironmentDefaults()
.build();withInMemorySource(flags)
Configures an in-memory feature flag source (ideal for development/testing).
const config = new FeatureFlagConfigurationBuilder()
.withInMemorySource([
{ name: 'debug-mode', value: true },
{ name: 'beta-features', value: false }
])
.build();withDynamoDBSource(options)
Configures a DynamoDB feature flag source (ideal for production).
const config = new FeatureFlagConfigurationBuilder()
.withDynamoDBSource({
tableName: 'my-feature-flags',
region: 'us-west-2',
endpoint: 'http://localhost:8000' // Optional for local DynamoDB
})
.build();withCaching(options)
Enables caching for feature flags to improve performance.
const config = new FeatureFlagConfigurationBuilder()
.withCaching({
enabled: true,
ttl: 600000 // 10 minutes
})
.build();withDefaultFlags(flags)
Sets default feature flags that will be available if not found in the primary source.
const config = new FeatureFlagConfigurationBuilder()
.withDefaultFlags([
{ name: 'fallback-feature', value: true }
])
.build();Feature Flag Utilities
The FeatureFlags utility class provides convenient methods for working with feature flags:
isEnabled(flagName, defaultValue)
Check if a feature flag is enabled:
import { FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
const isNewUIEnabled = await FeatureFlags.isEnabled('new-ui', false);
if (isNewUIEnabled) {
// Show new UI
}getAll()
Get all feature flags:
const allFlags = await FeatureFlags.getAll();
console.log('Current flags:', allFlags);getMultiple(flagNames, defaultValue)
Check multiple flags at once:
const flags = await FeatureFlags.getMultiple([
'feature-a',
'feature-b',
'feature-c'
], false);
console.log(flags); // { 'feature-a': true, 'feature-b': false, 'feature-c': true }when(flagName, enabledCallback, disabledCallback, defaultValue)
Conditional execution based on feature flag:
await FeatureFlags.when(
'new-algorithm',
async () => {
console.log('Using new algorithm');
return await newAlgorithm();
},
async () => {
console.log('Using old algorithm');
return await oldAlgorithm();
},
false
);middleware(flagNames)
Express/Lambda middleware to inject feature flags into request context:
import express from 'express';
import { FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
const app = express();
// Add all feature flags to req.featureFlags
app.use(FeatureFlags.middleware());
// Or add specific flags only
app.use(FeatureFlags.middleware(['ui-redesign', 'api-v2']));
app.get('/api/data', (req, res) => {
if (req.featureFlags['api-v2']) {
return res.json(await getDataV2());
}
return res.json(await getDataV1());
});Storage Backends
The feature flag system supports multiple storage backends, each optimized for different use cases:
In-Memory Storage
Perfect for development and testing:
import { setInMemoryFeatureFlag, getInMemoryFeatureFlag } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withInMemorySource([
{ name: 'debug-mode', value: true },
{ name: 'beta-features', value: false }
])
.build();
// Direct usage
setInMemoryFeatureFlag('debug-mode', true);
const isDebugEnabled = getInMemoryFeatureFlag('debug-mode', false);DynamoDB Storage
Recommended for production AWS environments:
import { setDynamoFeatureFlag, getDynamoFeatureFlag } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withDynamoDBSource({
tableName: 'feature-flags-prod',
region: 'us-east-1'
})
.withCaching({ enabled: true, ttl: 300000 })
.build();
// Direct usage
await setDynamoFeatureFlag('feature-rollout', true);
const isRolloutEnabled = await getDynamoFeatureFlag('feature-rollout', false);DynamoDB table structure:
{
"name": "feature-rollout",
"value": true,
"description": "Enable the new feature rollout",
"lastModified": "2024-01-15T10:30:00.000Z"
}Redis Storage
High-performance distributed caching (requires ioredis):
import { RedisFeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withRedisSource({
host: 'localhost',
port: 6379,
password: 'your-password',
keyPrefix: 'app:flags:'
})
.withCaching({ enabled: true, ttl: 60000 })
.build();
// Direct usage
await RedisFeatureFlags.setFeatureFlag('new-algorithm', true);
const isEnabled = await RedisFeatureFlags.getFeatureFlag('new-algorithm', false);Environment Variables Storage
Simple and secure for containerized deployments:
import { setEnvironmentFeatureFlag, getEnvironmentFeatureFlag } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withEnvironmentSource('MYAPP_FEATURE_') // Custom prefix
.build();
// Set environment variables:
// MYAPP_FEATURE_NEW_UI=true
// MYAPP_FEATURE_BETA_MODE=false
// Direct usage
const isNewUIEnabled = getEnvironmentFeatureFlag('new-ui', false);File-based Storage
Good for deployment-time configuration:
import { setFileFeatureFlag, getFileFeatureFlag } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withFileSource('./config/feature-flags.json', true) // Watch for changes
.build();
// feature-flags.json structure:
// [
// { "name": "new-feature", "value": true, "description": "Enable new feature" },
// { "name": "maintenance-mode", "value": false }
// ]
// Direct usage
const isMaintenanceMode = getFileFeatureFlag('maintenance-mode', false);
await setFileFeatureFlag('new-feature', true);AWS Parameter Store Storage
Secure, managed configuration (requires @aws-sdk/client-ssm):
import { ParameterStoreFeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withParameterStoreSource({
region: 'us-east-1',
prefix: '/myapp/feature-flags/',
clientConfig: { maxRetries: 3 }
})
.withCaching({ enabled: true, ttl: 300000 })
.build();
// Direct usage
await ParameterStoreFeatureFlags.setFeatureFlag('rollout-phase-1', true);
const isEnabled = await ParameterStoreFeatureFlags.getFeatureFlag('rollout-phase-1', false);Database Storage
For applications already using a database:
import { DatabaseFeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration (assuming you have a database connection)
const config = new FeatureFlagConfigurationBuilder()
.withDatabaseSource(databaseConnection, 'app_feature_flags')
.withCaching({ enabled: true, ttl: 120000 })
.build();
// Initialize table (run once)
await DatabaseFeatureFlags.initializeTable();
// Direct usage
await DatabaseFeatureFlags.setFeatureFlag('premium-features', true, 'Enable premium tier');
const isPremiumEnabled = await DatabaseFeatureFlags.getFeatureFlag('premium-features', false);
// Extra database features
await DatabaseFeatureFlags.enableFeatureFlag('premium-features', false); // Disable without deleting
const flagDetails = await DatabaseFeatureFlags.getFeatureFlagDetails('premium-features');Database table structure:
CREATE TABLE feature_flags (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
value BOOLEAN NOT NULL,
description TEXT,
enabled BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);HTTP Remote Service Storage
For centralized feature flag management:
import { HttpFeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Configuration
const config = new FeatureFlagConfigurationBuilder()
.withHttpSource('https://feature-flags.mycompany.com/api/v1', {
apiKey: process.env.FEATURE_FLAGS_API_KEY,
timeout: 5000,
headers: {
'X-App-Name': 'my-application'
}
})
.withCaching({ enabled: true, ttl: 180000 })
.build();
// Direct usage
await HttpFeatureFlags.setFeatureFlag('global-rollout', true);
const isEnabled = await HttpFeatureFlags.getFeatureFlag('global-rollout', false);
// Bulk operations
await HttpFeatureFlags.setMultipleFlags([
{ name: 'feature-a', value: true },
{ name: 'feature-b', value: false }
]);Storage Backend Comparison
| Backend | Best For | Performance | Setup Complexity | Dependencies | |---------|----------|-------------|------------------|--------------| | In-Memory | Development, Testing | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | None | | Environment Variables | Containers, Serverless | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | None | | File-based | Deployment-time config | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | None | | DynamoDB | AWS Production | ⭐⭐⭐⭐ | ⭐⭐⭐ | AWS SDK | | Redis | High-traffic, Distributed | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ioredis | | Parameter Store | Enterprise AWS | ⭐⭐⭐ | ⭐⭐ | AWS SDK | | Database | Existing DB apps | ⭐⭐⭐ | ⭐⭐ | Database | | HTTP Service | Centralized management | ⭐⭐⭐ | ⭐ | HTTP client |
Advanced Configuration Examples
Multi-Environment Factory Pattern
import { FeatureFlagConfigurationBuilder } from '@timmons-group/snack-pack/FeatureFlags';
export const createConfigForEnvironment = (environment, options = {}) => {
const builder = new FeatureFlagConfigurationBuilder()
.withEnvironment(environment);
switch (environment) {
case 'development':
case 'test':
return builder
.withInMemorySource(options.developmentFlags || [
{ name: 'debug-ui', value: true },
{ name: 'mock-data', value: true },
{ name: 'performance-logging', value: true }
])
.build();
case 'staging':
if (options.useFile) {
return builder
.withFileSource('./feature-flags-staging.json')
.withCaching({ enabled: true, ttl: 60000 })
.build();
} else {
return builder
.withDynamoDBSource({
tableName: options.tableName || 'feature-flags-staging',
region: options.region
})
.withCaching({ enabled: true, ttl: 60000 })
.build();
}
case 'production':
if (options.useParameterStore) {
return builder
.withParameterStoreSource({
prefix: '/app/feature-flags/',
region: options.region
})
.withCaching({ enabled: true, ttl: 300000 })
.build();
} else if (options.useRedis) {
return builder
.withRedisSource(options.redis || {})
.withCaching({ enabled: true, ttl: 180000 })
.build();
} else {
return builder
.withDynamoDBSource({
tableName: 'feature-flags-production',
region: options.region
})
.withCaching({ enabled: true, ttl: 300000 })
.build();
}
default:
throw new Error(`Unknown environment: ${environment}`);
}
};Smart Initialization with Fallbacks
import { setGlobalFeatureFlagConfig } from '@timmons-group/snack-pack/FeatureFlags';
export const initializeFeatureFlags = async () => {
try {
const environment = process.env.NODE_ENV || 'development';
// Try environment-specific configuration
const config = createConfigForEnvironment(environment, {
region: process.env.AWS_REGION,
tableName: process.env.FEATURE_FLAGS_TABLE,
useParameterStore: process.env.USE_PARAMETER_STORE === 'true',
useRedis: process.env.USE_REDIS === 'true',
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
}
});
setGlobalFeatureFlagConfig(config);
console.log(`Feature flags initialized for ${environment} environment`);
return config;
} catch (error) {
console.error('Failed to initialize feature flags:', error);
// Fallback to safe in-memory configuration
const fallbackConfig = new FeatureFlagConfigurationBuilder()
.withInMemorySource([
{ name: 'fallback-mode', value: true }
])
.build();
setGlobalFeatureFlagConfig(fallbackConfig);
console.warn('Using fallback in-memory feature flags configuration');
return fallbackConfig;
}
};
// Usage in your application
await initializeFeatureFlags();Container/Docker Configuration
// Perfect for containerized applications
const config = new FeatureFlagConfigurationBuilder()
.withEnvironmentSource('APP_FEATURE_')
.withDefaultFlags([
{ name: 'health-checks', value: true },
{ name: 'metrics-enabled', value: true }
])
.build();
// Set via environment variables:
// APP_FEATURE_NEW_API=true
// APP_FEATURE_BETA_UI=falseMicroservices Configuration
// Service-specific feature flags via HTTP
const config = new FeatureFlagConfigurationBuilder()
.withHttpSource('https://flags.company.com/api/v1', {
apiKey: process.env.FLAGS_API_KEY,
headers: {
'X-Service-Name': process.env.SERVICE_NAME,
'X-Service-Version': process.env.SERVICE_VERSION
}
})
.withCaching({ enabled: true, ttl: 120000 })
.build();High-Performance Configuration
// Redis with aggressive caching for high-traffic apps
const config = new FeatureFlagConfigurationBuilder()
.withRedisSource({
host: process.env.REDIS_HOST,
port: 6379,
keyPrefix: 'flags:',
retryDelayOnFailover: 100
})
.withCaching({ enabled: true, ttl: 30000 }) // 30 second local cache
.withDefaultFlags([
{ name: 'circuit-breaker', value: false }
])
.build();Environment-Based Configuration
Set up different configurations for different environments:
// config/feature-flags.js
import { FeatureFlagConfigurationBuilder } from '@timmons-group/snack-pack/FeatureFlags';
const getFeatureFlagConfig = () => {
const builder = new FeatureFlagConfigurationBuilder();
switch (process.env.NODE_ENV) {
case 'production':
return builder
.withEnvironment('production')
.withDynamoDBSource({
tableName: process.env.FEATURE_FLAGS_TABLE || 'feature-flags-prod',
region: process.env.AWS_REGION || 'us-east-1'
})
.withCaching({ enabled: true, ttl: 300000 })
.build();
case 'staging':
return builder
.withEnvironment('staging')
.withDynamoDBSource({
tableName: 'feature-flags-staging',
region: 'us-east-1'
})
.withCaching({ enabled: true, ttl: 60000 })
.withDefaultFlags([
{ name: 'staging-features', value: true }
])
.build();
default: // development/test
return builder
.withEnvironment('development')
.withInMemorySource([
{ name: 'debug-mode', value: true },
{ name: 'mock-services', value: true },
{ name: 'detailed-logging', value: true }
])
.build();
}
};
export default getFeatureFlagConfig();Integration Examples
AWS Lambda Function with Parameter Store
import { FeatureFlagConfigurationBuilder, FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Initialize feature flags with Parameter Store for production
const config = new FeatureFlagConfigurationBuilder()
.withEnvironment(process.env.NODE_ENV || 'development')
.withEnvironmentDefaults({
productionTableName: 'lambda-feature-flags',
region: process.env.AWS_REGION
})
.withDefaultFlags([
{ name: 'error-tracking', value: true }
])
.build();
export const handler = async (event) => {
try {
// Check feature flags
const useNewProcessor = await FeatureFlags.isEnabled('new-processor', false);
const enableDetailedLogging = await FeatureFlags.isEnabled('detailed-logging', false);
if (enableDetailedLogging) {
console.log('Processing event:', JSON.stringify(event, null, 2));
}
let result;
if (useNewProcessor) {
result = await processWithNewAlgorithm(event);
} else {
result = await processWithLegacyAlgorithm(event);
}
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (error) {
const enableErrorReporting = await FeatureFlags.isEnabled('error-tracking', true);
if (enableErrorReporting) {
// Report error to monitoring service
await reportError(error);
}
throw error;
}
};Express.js Application with Redis
import express from 'express';
import { FeatureFlagConfigurationBuilder, FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
const app = express();
// Initialize feature flags with Redis for high performance
const config = new FeatureFlagConfigurationBuilder()
.withEnvironment('production')
.withRedisSource({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
})
.withCaching({ enabled: true, ttl: 30000 }) // 30 second local cache
.withDefaultFlags([
{ name: 'maintenance-mode', value: false },
{ name: 'rate-limiting', value: true }
])
.build();
// Maintenance mode middleware
app.use(async (req, res, next) => {
const isMaintenanceMode = await FeatureFlags.isEnabled('maintenance-mode', false);
if (isMaintenanceMode && !req.path.startsWith('/health')) {
return res.status(503).json({
message: 'Service temporarily unavailable for maintenance'
});
}
next();
});
// Feature flag middleware
app.use(FeatureFlags.middleware(['enhanced-user-data', 'new-api-version', 'beta-features']));
// Rate limiting based on feature flag
app.use(async (req, res, next) => {
const enableRateLimit = await FeatureFlags.isEnabled('rate-limiting', true);
if (enableRateLimit) {
// Apply rate limiting logic
return rateLimitMiddleware(req, res, next);
}
next();
});
app.get('/api/users', async (req, res) => {
const users = req.featureFlags['enhanced-user-data']
? await getUsersWithExtendedData()
: await getBasicUsers();
res.json(users);
});
app.get('/api/v2/data', async (req, res) => {
const useNewAPI = req.featureFlags['new-api-version'];
if (!useNewAPI) {
return res.status(404).json({ message: 'API version not available' });
}
const data = await getV2Data();
res.json(data);
});Microservice with Environment Variables
import { FeatureFlagConfigurationBuilder, FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
// Perfect for Docker/Kubernetes deployments
const config = new FeatureFlagConfigurationBuilder()
.withEnvironmentSource('SERVICE_FEATURE_')
.withDefaultFlags([
{ name: 'health-check', value: true },
{ name: 'metrics-collection', value: true }
])
.build();
// Environment variables:
// SERVICE_FEATURE_CIRCUIT_BREAKER=true
// SERVICE_FEATURE_ASYNC_PROCESSING=false
class OrderService {
async processOrder(order) {
const useCircuitBreaker = await FeatureFlags.isEnabled('circuit-breaker', false);
const useAsyncProcessing = await FeatureFlags.isEnabled('async-processing', false);
if (useCircuitBreaker) {
return await this.processWithCircuitBreaker(order);
}
if (useAsyncProcessing) {
return await this.processOrderAsync(order);
}
return await this.processOrderSync(order);
}
async processWithCircuitBreaker(order) {
// Circuit breaker implementation
}
async processOrderAsync(order) {
// Queue for async processing
}
async processOrderSync(order) {
// Direct synchronous processing
}
}Next.js Application with File-based Config
// config/feature-flags.js
import { FeatureFlagConfigurationBuilder } from '@timmons-group/snack-pack/FeatureFlags';
const isDevelopment = process.env.NODE_ENV === 'development';
const config = new FeatureFlagConfigurationBuilder()
.withEnvironment(process.env.NODE_ENV)
.withFileSource(
isDevelopment
? './config/feature-flags-dev.json'
: './config/feature-flags-prod.json',
isDevelopment // Watch for changes in development
)
.withCaching({ enabled: !isDevelopment, ttl: 60000 })
.build();
export default config;
// pages/api/users.js
import { FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
export default async function handler(req, res) {
const enableNewUserFlow = await FeatureFlags.isEnabled('new-user-flow', false);
const enableUserAnalytics = await FeatureFlags.isEnabled('user-analytics', true);
if (enableNewUserFlow) {
// New user registration flow
const result = await handleNewUserFlow(req.body);
if (enableUserAnalytics) {
await trackUserRegistration(result.userId);
}
return res.json(result);
}
// Legacy user flow
return res.json(await handleLegacyUserFlow(req.body));
}
// components/FeatureFlag.jsx
import { useEffect, useState } from 'react';
import { FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
export function FeatureFlag({ flag, fallback = null, children }) {
const [isEnabled, setIsEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
FeatureFlags.isEnabled(flag, false)
.then(setIsEnabled)
.finally(() => setLoading(false));
}, [flag]);
if (loading) return fallback;
return isEnabled ? children : fallback;
}
// Usage in component
<FeatureFlag flag="new-dashboard" fallback={<LegacyDashboard />}>
<NewDashboard />
</FeatureFlag>Database Integration Example
import { DatabaseFeatureFlags, FeatureFlagConfigurationBuilder } from '@timmons-group/snack-pack/FeatureFlags';
import { getDatabaseConnection } from './database.js';
// Initialize database-backed feature flags
const db = await getDatabaseConnection();
await DatabaseFeatureFlags.initializeTable(); // Run once
const config = new FeatureFlagConfigurationBuilder()
.withDatabaseSource(db, 'app_feature_flags')
.withCaching({ enabled: true, ttl: 120000 })
.build();
// Admin API for managing feature flags
app.post('/admin/feature-flags', async (req, res) => {
const { name, value, description } = req.body;
try {
await DatabaseFeatureFlags.setFeatureFlag(name, value, description);
res.json({ success: true, message: `Feature flag '${name}' updated` });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/admin/feature-flags', async (req, res) => {
try {
const flags = await DatabaseFeatureFlags.allFeatureFlags();
res.json(flags);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.patch('/admin/feature-flags/:name/toggle', async (req, res) => {
const { name } = req.params;
const { enabled } = req.body;
try {
await DatabaseFeatureFlags.enableFeatureFlag(name, enabled);
res.json({ success: true, message: `Feature flag '${name}' ${enabled ? 'enabled' : 'disabled'}` });
} catch (error) {
res.status(500).json({ error: error.message });
}
});React Hook for Feature Flags
// hooks/useFeatureFlag.js
import { useState, useEffect } from 'react';
import { FeatureFlags } from '@timmons-group/snack-pack/FeatureFlags';
export function useFeatureFlag(flagName, defaultValue = false) {
const [isEnabled, setIsEnabled] = useState(defaultValue);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let mounted = true;
FeatureFlags.isEnabled(flagName, defaultValue)
.then(enabled => {
if (mounted) {
setIsEnabled(enabled);
setError(null);
}
})
.catch(err => {
if (mounted) {
setError(err);
setIsEnabled(defaultValue); // Fallback to default
}
})
.finally(() => {
if (mounted) {
setLoading(false);
}
});
return () => { mounted = false; };
}, [flagName, defaultValue]);
return { isEnabled, loading, error };
}
// Usage in component
function UserDashboard() {
const { isEnabled: showNewFeatures, loading } = useFeatureFlag('new-user-features');
const { isEnabled: enableAnalytics } = useFeatureFlag('user-analytics', true);
useEffect(() => {
if (enableAnalytics) {
trackPageView('user-dashboard');
}
}, [enableAnalytics]);
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>Dashboard</h1>
{showNewFeatures && <NewFeaturePanel />}
<StandardDashboard />
</div>
);
}GraphQL Integration
SNACK provides comprehensive GraphQL utilities for Apollo Server integration, including directives for access control, feature flags, and environment-specific functionality.
Quick Start
import { ApolloServer } from '@apollo/server';
import {
createSNACKApolloConfig,
createSNACKApolloContext,
usePermissionFilter
} from '@timmons-group/snack-pack/GraphQL';
import { gql } from 'graphql-tag';
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
# Admin-only field
sensitiveData: String @acl(permission: "admin")
# Development environment only
debugInfo: String @development
# Feature flag controlled
betaFeature: String @featureFlag(name: "user-beta-features")
}
type Query {
me: User
adminUsers: [User!]! @acl(permission: "admin")
}
`;
const resolvers = {
Query: {
me: (parent, args, context) => context.user,
adminUsers: usePermissionFilter(
'admin',
async () => await getAllUsers()
)
}
};
// Create server with SNACK integration
const server = new ApolloServer(
createSNACKApolloConfig({
typeDefs,
resolvers,
snackOptions: {
includeUser: true,
includeFeatureFlags: true
}
})
);Available Directives
@acl - Access Control
Restrict fields and operations based on user permissions:
type Query {
# Requires specific permission
sensitiveData: String @acl(permission: "admin")
# Requires any of multiple permissions
moderationTools: [String!]! @acl(permissions: ["moderator", "admin"])
# Requires all permissions
superAdminData: String @acl(permissions: ["admin", "super"], requireAll: true)
# Group-based access
internalAPI: String @acl(groups: ["internal-team"])
}@featureFlag - Feature Flags
Control field availability based on feature flags:
type User {
# Basic feature flag check
experimentalField: String @featureFlag(name: "experimental-features")
# Require specific flag value
premiumFeature: String @featureFlag(name: "premium-tier", value: true)
# Multiple flag dependencies
advancedFeature: String @featureFlag(
name: "advanced-features"
dependencies: ["base-features", "user-tier-premium"]
)
}@development - Environment Control
Hide fields in production environments:
type Query {
# Only available in development
debugEndpoint: String @development
# Development and staging only
testingTools: [String!]! @development(environments: ["development", "staging"])
}
type User {
# Debug information for development
internalState: UserDebugInfo @development
}
type UserDebugInfo @development {
createdAt: String!
lastQuery: String
cacheStats: CacheStats
}Context Integration
The SNACK context automatically provides user information and feature flags:
import { createSNACKApolloContext } from '@timmons-group/snack-pack/GraphQL';
const context = createSNACKApolloContext({
includeUser: true,
includeFeatureFlags: true,
// Custom context merger
customContext: async ({ req }) => ({
requestId: req.headers['x-request-id'],
clientVersion: req.headers['x-client-version']
})
});
// In resolvers, access SNACK context:
const resolvers = {
Query: {
profile: async (parent, args, context) => {
// User from JWT token
console.log(context.user.id, context.user.permissions);
// Feature flags for user
const flags = context.featureFlags || [];
const newUI = flags.find(f => f.name === 'new-ui')?.value;
// Custom context
console.log(context.requestId);
return await getProfile(context.user.id, { newUI });
}
}
};Permission Helpers
Use permission helper functions for complex authorization logic:
import {
usePermissionFilter,
useRoleFilter,
useOwnerFilter,
useCompositeFilter
} from '@timmons-group/snack-pack/GraphQL';
const resolvers = {
Query: {
// Simple permission check
adminData: usePermissionFilter(
'admin',
async () => await getAdminData()
),
// Multiple roles
moderationTools: useRoleFilter(
['moderator', 'admin', 'super-admin'],
async () => await getModerationTools()
),
// Owner-based access
userDocuments: useOwnerFilter(
'userId', // field containing owner ID
async (parent, args) => await getUserDocs(args.userId)
),
// Complex permission logic
sensitiveOperation: useCompositeFilter({
permissions: { all: ['read:sensitive', 'access:internal'] },
roles: { any: ['admin', 'super-admin'] },
groups: { any: ['internal-team'] },
custom: async (user, args) => {
return user.department === 'security' && user.clearanceLevel >= 3;
}
}, async () => await getSensitiveData())
}
};Express Integration
For Express-based Apollo servers:
import express from 'express';
import { expressMiddleware } from '@apollo/server/express4';
import { createSNACKApolloContext } from '@timmons-group/snack-pack/GraphQL';
const app = express();
const server = new ApolloServer({
typeDefs,
resolvers,
schemaTransforms: createSNACKSchemaTransforms()
});
await server.start();
app.use(
'/graphql',
cors(),
express.json(),
expressMiddleware(server, {
context: createSNACKApolloContext({
includeUser: true,
includeFeatureFlags: true
})
})
);Lambda/Serverless Integration
For serverless deployments:
import { ApolloServer } from '@apollo/server';
import { handlers } from '@apollo/server-lambda';
import { createSNACKApolloConfig } from '@timmons-group/snack-pack/GraphQL';
const server = new ApolloServer(
createSNACKApolloConfig({
typeDefs,
resolvers
})
);
export const graphqlHandler = handlers.createAPIGatewayProxyEventRequestHandler({
server
});Advanced Schema Composition
For modular GraphQL applications:
import { createSNACKSchemaTransforms } from '@timmons-group/snack-pack/GraphQL';
// Apply SNACK transforms to existing schema
const schema = buildSchema({
typeDefs: [directiveDefs, ...yourTypeDefs],
resolvers: yourResolvers
});
const transformedSchema = createSNACKSchemaTransforms({
enableDevelopment: process.env.NODE_ENV === 'development',
enableACL: true,
enableFeatureFlags: true
})(schema);See GraphQL examples for comprehensive integration patterns and use cases.
Roadmap
- [x] REST API providers
- [x] Endpoint builder pattern
- [x] OAuth authentication flow
- [x] Permission system
- [x] Feature flags system
- [x] GraphQL support
- [x] Error handling
- [ ] WebSocket support
- [ ] Rate limiting
- [ ] API versioning
- [ ] Request validation middleware
- [ ] Response caching
- [ ] Metrics and monitoring
