Build the foundation for a feature flag service. Create a Git repo, set up Node.js, design the database schema for flags, contexts, and evaluations, and scaffold the API. By the end, you'll have a working skeleton ready for flag logic.
← Back to Module 01 overviewflags, flag_rules, flag_evaluations, audit_logs.mkdir feature-flag-service cd feature-flag-service git init git config user.email "you@example.com" git config user.name "Your Name" git branch -M main
git status shows "On branch main".npm init -y npm install express postgres pino uuid dotenv
Install dev dependencies:
npm install --save-dev typescript @types/node @types/express eslint prettier ts-node nodemon
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Create .eslintrc.json:
{
"env": { "node": true, "es2020": true },
"extends": "eslint:recommended",
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
"rules": { "no-console": "off", "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] }
}
Create .prettierrc.json:
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2
}
npx tsc --version shows TypeScript version. npm run lint passes.mkdir -p src/{routes,db,types,middleware,utils}
mkdir -p docs
Create src/db/pool.ts:
import { Pool } from 'postgres';
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'feature_flag_service',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres'
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
export default pool;
Create src/db/schema.sql:
-- Flags: Feature flag definitions
CREATE TABLE IF NOT EXISTS flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
status TEXT DEFAULT 'off', -- off, on, percentage, rules
percentage_enabled INT DEFAULT 0, -- 0-100 for percentage-based rollout
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Flag Rules: Targeting rules for a flag (e.g., user in segment, beta tester)
CREATE TABLE IF NOT EXISTS flag_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flag_id UUID NOT NULL REFERENCES flags(id) ON DELETE CASCADE,
priority INT NOT NULL, -- Lower number = higher priority
rule_type TEXT NOT NULL, -- 'segment', 'user_attribute', 'percentage'
rule_value JSONB NOT NULL, -- {"segment": "beta_testers"} or {"attribute": "country", "operator": "eq", "value": "US"}
enabled BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Flag Evaluations: Log of flag checks for auditing
CREATE TABLE IF NOT EXISTS flag_evaluations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flag_id UUID NOT NULL REFERENCES flags(id) ON DELETE CASCADE,
context_id TEXT NOT NULL, -- User ID, Session ID, etc.
result BOOLEAN NOT NULL, -- true = flag enabled, false = disabled
evaluated_rule_id UUID, -- Which rule matched (if any)
timestamp TIMESTAMP DEFAULT NOW()
);
-- Audit Logs: Who changed what
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flag_id UUID REFERENCES flags(id) ON DELETE SET NULL,
action TEXT NOT NULL, -- 'created', 'updated', 'deleted', 'toggled'
details JSONB,
changed_by TEXT, -- User email or service name
timestamp TIMESTAMP DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_flags_name ON flags(name);
CREATE INDEX idx_flag_rules_flag_id ON flag_rules(flag_id, priority);
CREATE INDEX idx_evaluations_flag_id ON flag_evaluations(flag_id);
CREATE INDEX idx_evaluations_context ON flag_evaluations(context_id);
CREATE INDEX idx_audit_logs_flag_id ON audit_logs(flag_id);
CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp);
Create src/db/migrations.ts:
import fs from 'fs';
import path from 'path';
import pool from './pool';
export async function runMigrations() {
try {
const schemaPath = path.join(__dirname, 'schema.sql');
const schema = fs.readFileSync(schemaPath, 'utf-8');
const client = await pool.connect();
try {
await client.query(schema);
console.log('✓ Database migrations completed');
} finally {
client.release();
}
} catch (err) {
console.error('Migration failed:', err);
process.exit(1);
}
}
export default runMigrations;
Create src/index.ts:
import express, { Request, Response } from 'express';
import runMigrations from './db/migrations';
import flagRoutes from './routes/flags';
const app = express();
const PORT = parseInt(process.env.PORT || '3000');
// Middleware
app.use(express.json());
// Health check
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Routes
app.use('/flags', flagRoutes);
// 404 handler
app.use((_req: Request, res: Response) => {
res.status(404).json({ error: 'Not found' });
});
// Error handler
app.use((err: any, _req: Request, res: Response) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
async function start() {
try {
await runMigrations();
app.listen(PORT, () => {
console.log(`✓ Server running on http://localhost:${PORT}`);
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
start();