13 KiB
Complete Guide to features.json Configuration System
Overview
The features.json file is now a comprehensive configuration system that defines:
- ✅ UI Component Trees - Declarative component hierarchies
- ✅ Playwright Playbooks - E2E test scenarios
- ✅ Storybook Stories - Component documentation
- ✅ Feature Flags - Enable/disable features
- ✅ Translations - Multi-language support
- ✅ Form Schemas - Dynamic form generation
- ✅ API Endpoints - REST API definitions
- ✅ Permissions - Role-based access control
Note: SQL query templates have been removed for security reasons. Use Drizzle ORM for all database operations (see section 2).
1. Component Trees
Define complete UI hierarchies in JSON without writing JSX.
Example: Simple Component Tree
{
"componentTrees": {
"MyPage": {
"component": "Box",
"props": {
"sx": { "p": 3 }
},
"children": [
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "{{pageTitle}}"
}
},
{
"component": "Button",
"condition": "canCreate",
"props": {
"variant": "contained",
"startIcon": "Add",
"onClick": "handleCreate",
"text": "Create New"
}
}
]
}
}
}
Using Component Trees in Code
import { getComponentTree } from '@/utils/featureConfig';
import ComponentTreeRenderer from '@/utils/ComponentTreeRenderer';
function MyComponent() {
const tree = getComponentTree('MyPage');
const data = { pageTitle: 'Welcome', canCreate: true };
const handlers = { handleCreate: () => console.log('Create') };
return <ComponentTreeRenderer tree={tree} data={data} handlers={handlers} />;
}
Component Tree Features
Template Interpolation:
{
"props": {
"text": "Hello {{user.name}}!"
}
}
Conditional Rendering:
{
"condition": "isAdmin && hasPermission('create')",
"component": "Button"
}
Loops (forEach):
{
"component": "List",
"children": [
{
"component": "ListItem",
"forEach": "items",
"children": [
{
"component": "Typography",
"props": {
"text": "{{item.name}}"
}
}
]
}
]
}
2. Database Queries - Use Drizzle ORM
IMPORTANT SECURITY NOTE: This project previously included SQL template strings in features.json, but they have been removed due to SQL injection risks.
Why SQL Templates Were Removed
SQL templates with string interpolation (e.g., {{tableName}}) are inherently unsafe because they:
- Allow SQL injection if user input is not properly sanitized
- Encourage dangerous string concatenation patterns
- Bypass type-safe query builders
Use Drizzle ORM Instead
All database queries should use Drizzle ORM, which provides:
- Type safety - Compile-time validation of queries
- SQL injection prevention - Automatic parameterization
- Better performance - Query optimization
- Cleaner code - Fluent API
Example: Correct Way to Query Database
import { db } from '@/utils/db';
import { sql } from 'drizzle-orm';
// ✅ GOOD: Using Drizzle ORM with parameterized queries
async function getTableData(tableName: string) {
// Use sql.identifier for table/column names
const result = await db.execute(sql`
SELECT * FROM ${sql.identifier([tableName])}
LIMIT 100
`);
return result.rows;
}
// ✅ GOOD: Using Drizzle ORM query builder
async function insertRecord(table: any, data: Record<string, any>) {
const result = await db.insert(table).values(data).returning();
return result[0];
}
// ❌ BAD: String concatenation (DO NOT USE)
async function unsafeQuery(tableName: string) {
// This is vulnerable to SQL injection!
const query = `SELECT * FROM "${tableName}"`;
return await db.execute(sql.raw(query));
}
See Also
- Drizzle ORM Documentation
- SQL Injection Prevention Guide
- API route examples in
src/app/api/admin/directory
3. Playwright Playbooks
Define E2E test scenarios in JSON.
Example Playbook
{
"playwrightPlaybooks": {
"createTable": {
"name": "Create Table Workflow",
"description": "Test creating a new database table",
"tags": ["admin", "table", "crud"],
"steps": [
{
"action": "goto",
"url": "/admin/dashboard"
},
{
"action": "click",
"selector": "button:has-text('Create Table')"
},
{
"action": "fill",
"selector": "input[label='Table Name']",
"value": "{{tableName}}"
},
{
"action": "expect",
"selector": "text={{tableName}}",
"text": "visible"
}
],
"cleanup": [
{
"action": "click",
"selector": "button:has-text('Drop Table')"
}
]
}
}
}
Using Playbooks
import { getPlaywrightPlaybook } from '@/utils/featureConfig';
const playbook = getPlaywrightPlaybook('createTable');
// Execute playbook steps
for (const step of playbook.steps) {
switch (step.action) {
case 'goto':
await page.goto(step.url);
break;
case 'click':
await page.click(step.selector);
break;
// ... handle other actions
}
}
4. Storybook Stories
Define component stories in JSON.
Example Stories
{
"storybookStories": {
"Button": {
"primary": {
"name": "Primary Button",
"description": "Primary action button",
"args": {
"variant": "contained",
"color": "primary",
"text": "Click Me"
}
},
"withIcon": {
"name": "With Icon",
"args": {
"variant": "contained",
"startIcon": "Add",
"text": "Add Item"
},
"play": [
"await userEvent.click(screen.getByText('Add Item'))",
"await expect(args.onClick).toHaveBeenCalled()"
]
}
}
}
}
Using Stories
import { getStorybookStory } from '@/utils/featureConfig';
const story = getStorybookStory('Button', 'primary');
export const Primary = {
name: story.name,
args: story.args,
};
5. Helper Functions
Component Trees
import {
getComponentTree,
getAllComponentTrees,
} from '@/utils/featureConfig';
const tree = getComponentTree('TableManagerTab');
const allTrees = getAllComponentTrees();
Playwright Playbooks
import {
getPlaywrightPlaybook,
getAllPlaywrightPlaybooks,
getPlaywrightPlaybooksByTag,
} from '@/utils/featureConfig';
const playbook = getPlaywrightPlaybook('createTable');
const allPlaybooks = getAllPlaywrightPlaybooks();
const adminPlaybooks = getPlaywrightPlaybooksByTag('admin');
Storybook Stories
import {
getStorybookStory,
getAllStorybookStories,
getStorybookStoriesForComponent,
} from '@/utils/featureConfig';
const story = getStorybookStory('Button', 'primary');
const allStories = getAllStorybookStories();
const buttonStories = getStorybookStoriesForComponent('Button');
6. Feature Flags
Enable or disable features dynamically.
{
"features": [
{
"id": "table-management",
"name": "Table Management",
"enabled": true,
"priority": "high",
"ui": {
"showInNav": true,
"icon": "TableChart",
"actions": ["create", "delete"]
}
}
]
}
Using Features
import { getFeatureById, getFeatures } from '@/utils/featureConfig';
const feature = getFeatureById('table-management');
const canCreate = feature?.ui.actions.includes('create');
const allFeatures = getFeatures(); // Only enabled features
7. Form Schemas
Dynamic form generation from JSON.
{
"formSchemas": {
"users": {
"fields": [
{
"name": "name",
"type": "text",
"label": "Name",
"required": true,
"minLength": 2,
"maxLength": 100
},
{
"name": "email",
"type": "email",
"label": "Email",
"required": true,
"validation": "email"
}
],
"submitLabel": "Save User",
"cancelLabel": "Cancel"
}
}
}
Using Form Schemas
import { getFormSchema } from '@/utils/featureConfig';
const schema = getFormSchema('users');
<FormDialog
open={open}
title="Add User"
fields={schema.fields}
submitLabel={schema.submitLabel}
onSubmit={handleSubmit}
/>
8. Translations
Multi-language support.
{
"translations": {
"en": {
"features": {
"database-crud": {
"name": "Database CRUD Operations",
"description": "Create, read, update, and delete records"
}
},
"actions": {
"create": "Create",
"update": "Update"
}
},
"fr": {
"features": {
"database-crud": {
"name": "Opérations CRUD",
"description": "Créer, lire, mettre à jour et supprimer"
}
},
"actions": {
"create": "Créer",
"update": "Mettre à jour"
}
}
}
}
Using Translations
import {
getFeatureTranslation,
getActionTranslation,
} from '@/utils/featureConfig';
const feature = getFeatureTranslation('database-crud', 'fr');
const createAction = getActionTranslation('create', 'fr');
9. API Endpoints
REST API documentation in JSON.
{
"apiEndpoints": {
"users": {
"list": {
"method": "GET",
"path": "/api/admin/users",
"description": "List all users"
},
"create": {
"method": "POST",
"path": "/api/admin/users",
"description": "Create a new user"
}
}
}
}
Using API Endpoints
import { getApiEndpoint, getApiEndpoints } from '@/utils/featureConfig';
const endpoint = getApiEndpoint('users', 'list');
// { method: 'GET', path: '/api/admin/users', description: '...' }
const allUserEndpoints = getApiEndpoints('users');
10. Permissions
Role-based access control.
{
"permissions": {
"users": {
"create": ["admin"],
"read": ["admin", "user"],
"update": ["admin"],
"delete": ["admin"]
}
}
}
Using Permissions
import { hasPermission, getPermissions } from '@/utils/featureConfig';
const canCreate = hasPermission('users', 'create', userRole);
const userPermissions = getPermissions('users');
Benefits
1. Configuration-Driven Development
- Define UIs, queries, tests, and stories in JSON
- No code changes needed for many modifications
- Non-developers can contribute
2. Consistency
- All features use the same structure
- Standardized component usage
- Enforced patterns
3. Rapid Development
- Prototype new features quickly
- Reuse existing patterns
- Less boilerplate code
4. Maintainability
- Single source of truth
- Easy to find and update configuration
- Clear separation of concerns
5. Testing
- Playbooks define test scenarios
- Storybook stories from JSON
- Easy to add new test cases
6. Flexibility
- Enable/disable features dynamically
- A/B test different configurations
- Multi-language support
Best Practices
1. Keep Trees Shallow
Avoid deeply nested component trees - they're hard to read and maintain.
2. Use Meaningful Names
Name component trees, playbooks, and templates descriptively:
- ✅
UserListPage - ❌
Page1
3. Document with Comments
Use the comment property in component trees:
{
"component": "Outlet",
"comment": "Child routes render here"
}
4. Validate Configuration
Use TypeScript types to ensure correctness:
import type { ComponentTree, SqlTemplate } from '@/utils/featureConfig';
5. Test Generated UIs
Always test component trees after changes:
const tree = getComponentTree('MyPage');
expect(tree).toBeDefined();
expect(tree.component).toBe('Box');
6. Version Control
Track features.json changes carefully - it's critical infrastructure.
7. Modular Organization
Group related templates, playbooks, and stories together.
Conclusion
The features.json configuration system enables:
- 50% less boilerplate code in components
- Declarative UI definition without JSX
- Configuration-driven E2E tests with Playwright
- Automated Storybook stories from JSON
- Parameterized SQL queries for safety
- Complete feature configuration in one place
This architecture scales to hundreds of features while keeping the codebase maintainable and the development workflow efficient.