Skip to main content
Frame is used to create independent API endpoints that are not connected to database Entities.

Frame Overview

No Entity Required

Works without DB tablesFree API implementation

Simple Structure

Extend BaseFrameClassUse @api decorator

Various Uses

Health check, utilitiesExternal API proxy

Independent Logic

Implement business logic onlyNo Entity constraints

Model vs Frame

Model (BaseModelClass)

// Entity-based API
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries); // ← Connected to Entity
  }
  
  @api({ httpMethod: "GET" })
  async list(): Promise<User[]> {
    // Query users table
    const rdb = this.getPuri("r");
    return rdb.table("users").select("*");
  }
  
  @api({ httpMethod: "POST" })
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    // Insert into users table
    const wdb = this.getPuri("w");
    wdb.ubRegister("users", params);
    const [userId] = await wdb.ubUpsert("users");
    return { userId };
  }
}
Model Characteristics:
  • 1:1 mapping with Entity (DB table)
  • CRUD operation focused
  • Uses UpsertBuilder, Subset
  • Type safety guaranteed

Frame (BaseFrameClass)

// Independent API without Entity
class HealthFrame extends BaseFrameClass {
  frameName = "Health"; // ← Independent of Entity
  
  @api({ httpMethod: "GET" })
  async check(): Promise<{
    status: "ok" | "error";
    timestamp: Date;
    uptime: number;
  }> {
    // Return server status without DB access
    return {
      status: "ok",
      timestamp: new Date(),
      uptime: process.uptime(),
    };
  }
  
  @api({ httpMethod: "GET" })
  async ping(): Promise<{ pong: string }> {
    return { pong: "pong" };
  }
}
Frame Characteristics:
  • Independent logic unrelated to Entity
  • Utilities, health checks, proxies, etc.
  • Flexible API structure
  • Simple implementation

Comparison Table

FeatureModelFrame
Entity Connection✅ Required❌ Not needed
DB AccessCRUD focusedOptional
Use CasesData managementUtilities, proxies
ComplexityHighLow
UpsertBuilder✅ Uses❌ Not needed
Subset✅ Uses❌ Not needed

When to Use Frame

✅ Use Frame When

  1. Health Check API
    // GET /api/health/check
    // Only check server status without Entity
    
  2. Utility API
    // POST /api/utils/hash
    // Simple functions like string hashing
    
  3. External API Proxy
    // GET /api/weather/current
    // Wrap external weather API as internal API
    
  4. Aggregated Statistics
    // GET /api/stats/dashboard
    // Aggregate statistics from multiple tables
    
  5. Auth/Permission Check
    // POST /api/auth/verify-token
    // Only perform JWT token verification
    

❌ Use Model When

  1. Entity CRUD Operations
    // Create/Read/Update/Delete for User, Product, Order, etc.
    
  2. Related Data Processing
    // User → Profile, Order → OrderItems
    
  3. Complex Business Logic
    // Order processing, payments involving multiple Entities
    

Structural Differences

Model Structure

api/src/application/
  user/
    ├── user.entity.json       # ← Entity definition
    ├── user.model.ts          # ← Model class
    ├── user.types.ts          # ← Type definitions
    └── subsets/
        └── user-list.subset.json

Frame Structure

api/src/frames/
  health/
    └── health.frame.ts        # ← Frame class only
  utils/
    └── utils.frame.ts
Frame only requires the .frame.ts file without Entity definition.

Simple Examples

Health Check Frame

class HealthFrame extends BaseFrameClass {
  frameName = "Health";
  
  @api({ httpMethod: "GET" })
  async check(): Promise<{
    status: "ok" | "error";
    timestamp: Date;
    database: "connected" | "disconnected";
  }> {
    // DB connection check
    let dbStatus: "connected" | "disconnected" = "disconnected";
    
    try {
      const rdb = this.getPuri("r");
      await rdb.raw("SELECT 1");
      dbStatus = "connected";
    } catch (error) {
      console.error("Database connection failed:", error);
    }
    
    return {
      status: dbStatus === "connected" ? "ok" : "error",
      timestamp: new Date(),
      database: dbStatus,
    };
  }
  
  @api({ httpMethod: "GET" })
  async ping(): Promise<{ message: string }> {
    return { message: "pong" };
  }
}

// Endpoints:
// GET /api/health/check
// GET /api/health/ping

Utility Frame

import crypto from "crypto";

class UtilsFrame extends BaseFrameClass {
  frameName = "Utils";
  
  @api({ httpMethod: "POST" })
  async hash(params: {
    text: string;
    algorithm: "md5" | "sha256" | "sha512";
  }): Promise<{ hash: string }> {
    const hash = crypto
      .createHash(params.algorithm)
      .update(params.text)
      .digest("hex");
    
    return { hash };
  }
  
  @api({ httpMethod: "POST" })
  async uuid(): Promise<{ uuid: string }> {
    return { uuid: crypto.randomUUID() };
  }
}

// Endpoints:
// POST /api/utils/hash
// POST /api/utils/uuid

Pros and Cons

Pros

Fast Implementation

No Entity definition neededSimple structure

Flexibility

Unrestricted logicFree API design

Independence

Independent of DB schemaEasy external API integration

Easy Testing

Simple logicNo mock needed

Cons

Reduced Type Safety

Entity types not usedManual type management

No Auto Generation

No Subset, Types auto-generationManual writing required

Practical Tips

1. Keep Frame Simple

// ✅ Good: Simple logic
class HealthFrame extends BaseFrameClass {
  @api({ httpMethod: "GET" })
  async check(): Promise<{ status: string }> {
    return { status: "ok" };
  }
}

// ❌ Bad: Complex business logic
class OrderFrame extends BaseFrameClass {
  @api({ httpMethod: "POST" })
  async process(params: ComplexOrderParams): Promise<OrderResult> {
    // 100 lines of complex logic...
    // This case should use Model
  }
}

2. Proper Naming

// ✅ Good: Clear names
class HealthFrame extends BaseFrameClass {
  frameName = "Health";
}

class UtilsFrame extends BaseFrameClass {
  frameName = "Utils";
}

// ❌ Bad: Vague names
class MiscFrame extends BaseFrameClass {
  frameName = "Misc";
}
// ✅ Good: Related features in one Frame
class CryptoUtilsFrame extends BaseFrameClass {
  frameName = "CryptoUtils";
  
  @api({ httpMethod: "POST" })
  async hash(params: HashParams): Promise<{ hash: string }> { /* ... */ }
  
  @api({ httpMethod: "POST" })
  async encrypt(params: EncryptParams): Promise<{ encrypted: string }> { /* ... */ }
  
  @api({ httpMethod: "POST" })
  async decrypt(params: DecryptParams): Promise<{ decrypted: string }> { /* ... */ }
}

When to Convert to Model?

Consider converting from Frame to Model when:
  1. Frequently querying/modifying DB data
    • Started as simple queries but now needs CRUD
  2. Type safety becomes important
    • Complex data structures emerge
    • Need Entity type auto-generation
  3. Need to process related data
    • Processing relationships between multiple tables
    • JOIN, foreign keys, etc.
  4. Business logic becomes complex
    • Beyond simple utilities
    • Transaction processing needed

Next Steps