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
| Feature | Model | Frame |
|---|
| Entity Connection | ✅ Required | ❌ Not needed |
| DB Access | CRUD focused | Optional |
| Use Cases | Data management | Utilities, proxies |
| Complexity | High | Low |
| UpsertBuilder | ✅ Uses | ❌ Not needed |
| Subset | ✅ Uses | ❌ Not needed |
When to Use Frame
✅ Use Frame When
-
Health Check API
// GET /api/health/check
// Only check server status without Entity
-
Utility API
// POST /api/utils/hash
// Simple functions like string hashing
-
External API Proxy
// GET /api/weather/current
// Wrap external weather API as internal API
-
Aggregated Statistics
// GET /api/stats/dashboard
// Aggregate statistics from multiple tables
-
Auth/Permission Check
// POST /api/auth/verify-token
// Only perform JWT token verification
❌ Use Model When
-
Entity CRUD Operations
// Create/Read/Update/Delete for User, Product, Order, etc.
-
Related Data Processing
// User → Profile, Order → OrderItems
-
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:
-
Frequently querying/modifying DB data
- Started as simple queries but now needs CRUD
-
Type safety becomes important
- Complex data structures emerge
- Need Entity type auto-generation
-
Need to process related data
- Processing relationships between multiple tables
- JOIN, foreign keys, etc.
-
Business logic becomes complex
- Beyond simple utilities
- Transaction processing needed
Next Steps