Model is a core layer of Sonamu that handles business logic and data access. Based on Entity definitions, it provides database queries, API endpoints, and type-safe data processing.
Role of Model
Data Access Database CRUD operations Type-safe queries through Puri query builder
Business Logic Implementing domain rules Data validation, transformation, calculation
API Endpoints Automatic HTTP API generation REST API provided via @api decorator
Type Safety Compile-time type checking Auto-generated types based on Entity
Position in Sonamu Architecture
Layer Structure :
Client - Frontend (React, Vue, etc.)
API Layer - HTTP endpoints (@api decorator)
Model Layer - Business logic ⭐
Database - PostgreSQL
Model is the business logic layer between API Layer and Database. All data access and business rules are handled in Model.
Model Class Structure
Basic Structure
import { BaseModelClass , api } from "sonamu" ;
import type { UserSubsetKey , UserSubsetMapping } from "../sonamu.generated" ;
import { userLoaderQueries , userSubsetQueries } from "../sonamu.generated.sso" ;
class UserModelClass extends BaseModelClass <
UserSubsetKey , // "A" | "P" | "SS"
UserSubsetMapping , // Type for each subset
typeof userSubsetQueries , // Subset query functions
typeof userLoaderQueries // Loader query functions
> {
constructor () {
super ( "User" , userSubsetQueries , userLoaderQueries );
}
@ api ({ httpMethod: "GET" })
async findById ( id : number ) : Promise < UserSubsetMapping [ "A" ]> {
// Business logic implementation
}
}
export const UserModel = new UserModelClass ();
Generic Type Parameters
Parameter Description Example TSubsetKeyUnion type of Subset keys "A" | "P" | "SS"TSubsetMappingResult type mapping per Subset { A: UserA, P: UserP }TSubsetQueriesSubset query function type Auto-generated TLoaderQueriesLoader query type Auto-generated
These generic types are automatically generated in sonamu.generated.ts and sonamu.generated.sso.ts, so you don’t need to write them manually.
Model vs Entity vs Types
Let’s clarify commonly confused concepts in Sonamu:
Category Entity Types Model File {entity}.entity.json{entity}.types.ts{entity}.model.tsRole Data structure definition Type definition Business logic Creation Manual (Sonamu UI) Auto-generated Manual (Scaffold) Content Tables, columns, relations TypeScript types, Zod Methods, API, queries Change frequency Low Auto-regenerated High
Examples :
user.entity.json (Entity)
user.types.ts (Types - auto-generated)
user.model.ts (Model)
{
"id" : "User" ,
"table" : "users" ,
"props" : [
{ "name" : "id" , "type" : "integer" },
{ "name" : "email" , "type" : "string" }
]
}
Features Provided by Model
1. Type-Safe Data Access
// ✅ Type safe: type inference based on subset
const user = await UserModel . findById ( "A" , 1 );
// user type: UserSubsetMapping["A"]
// ✅ Compile error: invalid subset
const user = await UserModel . findById ( "INVALID" , 1 );
// ^^^^^^^^^
// Error: Argument of type '"INVALID"' is not assignable
2. Subset-Based Queries
async findMany < T extends UserSubsetKey > (
subset : T ,
params : UserListParams
): Promise < ListResult < UserSubsetMapping [ T ] >> {
const { qb } = this.getSubsetQueries(subset);
// Automatically SELECT only fields matching the subset
qb.where( "id" , params.id);
return this.executeSubsetQuery({ subset , qb , params });
}
Subsets reduce network traffic and improve performance by selecting only the fields needed for API responses.
3. Automatic API Generation
@ api ({ httpMethod: "GET" , clients: [ "axios" , "tanstack-query" ] })
async findById ( id : number ): Promise < User > {
// Method implementation
}
What gets auto-generated :
HTTP endpoint: GET /user/findById?id=1
TypeScript client code
TanStack Query hooks
API documentation
4. Transaction Management
@ transactional ()
async updateUserAndProfile (
userId : number ,
userData : UserData ,
profileData : ProfileData
): Promise < void > {
// All operations execute in a single transaction
await this.save( [userData]);
await ProfileModel.save([profileData]);
// Auto commit or rollback
}
async save ( params : UserSaveParams []): Promise < number [] > {
// Business rule validation
for ( const param of params ) {
if ( param . age && param . age < 18 ) {
throw new BadRequestException ( "Users under 18 cannot register" );
}
}
// Data transformation
const hashedParams = params . map ( p => ({
... p ,
password: bcrypt . hashSync ( p . password , 10 )
}));
// Save
return this.getPuri( "w" ).ubUpsert( "users" , hashedParams);
}
Model Writing Patterns
CRUD Methods
A typical Model provides the following CRUD methods:
class UserModelClass extends BaseModelClass {
// Create
@ api ({ httpMethod: "POST" })
async save ( params : UserSaveParams []) : Promise < number []> {
// ...
}
// Read
@ api ({ httpMethod: "GET" })
async findById ( id : number ) : Promise < User > {
// ...
}
@ api ({ httpMethod: "GET" })
async findMany ( params : UserListParams ) : Promise < ListResult < User >> {
// ...
}
// Update
@ api ({ httpMethod: "PUT" })
async update ( id : number , params : UserUpdateParams ) : Promise < User > {
// ...
}
// Delete
@ api ({ httpMethod: "DELETE" })
async del ( ids : number []) : Promise < number > {
// ...
}
}
Domain-Specific Methods
You can add methods specialized for business domains:
class UserModelClass extends BaseModelClass {
@ api ({ httpMethod: "POST" })
async login ( params : LoginParams ) : Promise <{ user : User }> {
// Login logic
}
@ api ({ httpMethod: "POST" })
async logout () : Promise <{ message : string }> {
// Logout logic
}
@ api ({ httpMethod: "GET" })
async me () : Promise < User | null > {
// Current user info
}
@ api ({ httpMethod: "POST" })
async changePassword ( params : ChangePasswordParams ) : Promise < void > {
// Password change
}
}
BaseModel Methods
BaseModelClass provides the following utility methods:
Method Description Return Type getDB(preset)Get database connection KnexgetPuri(preset)Get Puri query builder PuriWrappergetSubsetQueries(subset)Get Subset query builder { qb, onSubset }executeSubsetQuery(params)Execute Subset query ListResultcreateEnhancers(enhancers)Enhancer creation helper EnhancerMap
Advantages of Model
TypeScript types are auto-generated based on Entity definitions, allowing errors to be caught at compile time. // ✅ Type check
const user : UserSubsetMapping [ "A" ] = await UserModel . findById ( "A" , 1 );
// ❌ Compile error
user . nonExistentField ;
Write common logic as Model methods and reuse across multiple APIs. // Model method
async findActiveUsers (): Promise < User [] > {
return this.puri().where( "is_active" , true).many();
}
// Reuse in multiple APIs
@ api ()
async api1 () {
return this . findActiveUsers ();
}
@ api ()
async api2 () {
const users = await this . findActiveUsers ();
// Additional logic...
}
Models can be tested independently. describe ( "UserModel" , () => {
it ( "should find user by id" , async () => {
const user = await UserModel . findById ( "A" , 1 );
expect ( user . id ). toBe ( 1 );
});
});
Clearly separates business logic (Model) from HTTP handling (Controller/API).
Model: Data processing, business rules
API: HTTP request/response, authentication, authorization
Next Steps
After understanding the basic concepts of Model, learn the following topics: