Skip to content

feat(api): M4 - API Service with better-sqlite3 and robust type safety#3

Merged
reckziegelwilliam merged 2 commits intomainfrom
feat/m4-api-service
Dec 2, 2025
Merged

feat(api): M4 - API Service with better-sqlite3 and robust type safety#3
reckziegelwilliam merged 2 commits intomainfrom
feat/m4-api-service

Conversation

@reckziegelwilliam
Copy link
Owner

Overview

Implements M4 - API Service using better-sqlite3 instead of Prisma for maximum simplicity, performance, and control while maintaining robust type safety.

What's New

Core Implementation

  • Fastify API server with CORS support
  • better-sqlite3 database layer (simpler than Prisma, ~1MB vs ~3MB)
  • Comprehensive type system with branded types to prevent ID mixing
  • Type-safe database client with prepared statements
  • Migration system for schema versioning
  • API key authentication middleware

API Endpoints

  • GET /v1/config?apiKey=<key> - SDK config fetching (returns FlagConfig)
  • GET /v1/flags?apiKey=<key> - List all flags in environment
  • POST /v1/flags - Create a new flag
  • GET /v1/flags/:id - Get specific flag
  • PUT /v1/flags/:id - Update flag configuration
  • DELETE /v1/flags/:id - Delete flag
  • POST /v1/evaluate - Server-side flag evaluation

Testing

  • 17 integration tests all passing
  • ✅ In-memory SQLite for blazing fast tests
  • ✅ Full coverage of CRUD operations, authentication, and evaluation

Technical Highlights

Type Safety

// Branded types prevent mixing different ID types
type ProjectId = Brand<string, 'ProjectId'>;
type EnvironmentId = Brand<string, 'EnvironmentId'>;
type FlagId = Brand<string, 'FlagId'>;

// Type-safe mappers
function mapFlagRow(row: FlagRow): Flag {
  return {
    id: row.id as FlagId,
    key: row.key,
    config: row.config,
    envId: row.env_id as EnvironmentId,
    createdAt: new Date(row.created_at),
    updatedAt: new Date(row.updated_at),
  };
}

Database Client

// Type-safe queries with prepared statements
class DatabaseClient {
  findFlagByKey(envId: EnvironmentId, key: string): Flag | null {
    const stmt = this.db.prepare('SELECT * FROM flags WHERE env_id = ? AND key = ?');
    const row = stmt.get(envId, key) as FlagRow | undefined;
    return row ? mapFlagRow(row) : null;
  }
}

Why better-sqlite3 over Prisma?

Aspect Prisma better-sqlite3
Bundle Size ~3MB ~1MB
Type Safety ✅ Auto-generated ✅ Explicit types
Code Verbosity Low Slightly Higher
Performance Fast Faster (sync)
Learning Curve Steeper Easier (just SQL)
Migrations Built-in Simple manual
Control Abstracted Full control
Magic High Zero

For Togglekit's simple schema (3 tables) and read-heavy workload, better-sqlite3 provides better control and performance without the overhead of Prisma's code generation.

Database Schema

CREATE TABLE projects (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE environments (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  api_key TEXT NOT NULL UNIQUE,
  project_id TEXT NOT NULL,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (project_id) REFERENCES projects(id)
);

CREATE TABLE flags (
  id TEXT PRIMARY KEY,
  key TEXT NOT NULL,
  config TEXT NOT NULL,  -- JSON string
  env_id TEXT NOT NULL,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (env_id) REFERENCES environments(id)
);

Test Results

PASS test/api.test.ts
  Togglekit API
    Health Check
      ✓ should return 200 OK
    Authentication
      ✓ should reject requests without API key
      ✓ should reject requests with invalid API key
      ✓ should accept requests with valid API key
    GET /v1/config
      ✓ should return empty config when no flags exist
      ✓ should return all flags in FlagConfig format
    POST /v1/flags
      ✓ should create a new flag
      ✓ should reject duplicate flag keys
      ✓ should reject invalid JSON config
    GET /v1/flags
      ✓ should list all flags
    PUT /v1/flags/:id
      ✓ should update flag config
      ✓ should return 404 for non-existent flag
    DELETE /v1/flags/:id
      ✓ should delete a flag
      ✓ should return 404 for non-existent flag
    POST /v1/evaluate
      ✓ should evaluate flag with matching condition
      ✓ should evaluate flag with default value
      ✓ should handle non-existent flag

Test Suites: 1 passed, 1 total
Tests:       17 passed, 17 total

Files Changed

  • apps/api/package.json - Dependencies and scripts
  • apps/api/src/db/types.ts - Comprehensive type definitions
  • apps/api/src/db/client.ts - Type-safe database client
  • apps/api/src/db/migrations.ts - Migration system
  • apps/api/src/middleware/auth.ts - API key authentication
  • apps/api/src/routes/*.ts - All API endpoints
  • apps/api/src/app.ts - Fastify app factory
  • apps/api/src/index.ts - Server entry point
  • apps/api/test/api.test.ts - Integration tests

Ready for Review

  • ✅ All tests passing
  • ✅ TypeScript compiles without errors
  • ✅ No linter warnings
  • ✅ Comprehensive type safety
  • ✅ Ready to merge into main

Next Steps (M5)

After merging, the next milestone will be the Dashboard (Next.js UI) for managing flags visually.

Closes #M4

…pe safety

- Replace Prisma with better-sqlite3 for simpler, faster database access
- Implement comprehensive TypeScript type system with branded types
- Create type-safe database client with prepared statements
- Build migration system for schema versioning
- Implement all API endpoints:
  - GET /v1/config (SDK config fetching)
  - CRUD endpoints for flags (GET/POST/PUT/DELETE /v1/flags)
  - POST /v1/evaluate (server-side evaluation)
- Add API key authentication middleware
- Create comprehensive integration tests (17 tests, all passing)
- Full type safety maintained throughout with no 'any' leaks

Technical highlights:
- Branded types prevent mixing ProjectId, EnvironmentId, FlagId
- Type-safe mappers convert between DB rows and domain models
- Prepared statements for performance
- In-memory SQLite for blazing fast tests
- Zero magic - explicit SQL queries
- ~1MB bundle vs Prisma's ~3MB
- Create standalone ESLint config (root config didn't exist)
- Remove unused ApiKey import from db/client.ts
- Remove unused Environment import from types/api.ts
- All linting now passes
@reckziegelwilliam reckziegelwilliam merged commit 9619341 into main Dec 2, 2025
14 checks passed
@reckziegelwilliam reckziegelwilliam deleted the feat/m4-api-service branch December 2, 2025 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant