The @api decorator automatically converts Model methods into HTTP API endpoints. Adding the decorator to a method auto-generates routing, type validation, and client code.
Basic Usage
import { api } from "sonamu" ;
class UserModelClass extends BaseModelClass {
@ api ({ httpMethod: "GET" })
async findById ( id : number ) : Promise < User > {
return this . getPuri ( "r" ). where ( "id" , id ). first ();
}
}
What gets generated :
HTTP endpoint: GET /user/findById?id=1
TypeScript client function
TanStack Query hooks (when selected)
API documentation
Decorator Options
All Options
@ api ({
httpMethod: "GET" , // HTTP method
contentType: "application/json" , // Content-Type
clients: [ "axios" , "tanstack-query" ], // Clients to generate
path: "/custom/path" , // Custom path
resourceName: "Users" , // Resource name
guards: [ "admin" , "user" ], // Auth/permission guards
description: "User retrieval API" , // API description
timeout: 5000 , // Timeout (ms)
cacheControl: { maxAge: 60 }, // Cache settings
compress: false , // Disable response compression
})
httpMethod
Specifies the HTTP method.
Method Use Examples GETData retrieval findById, findManyPOSTData create/modify save, loginPUTData update updateDELETEData deletion del, removePATCHPartial update updateProfile
GET - Retrieval
POST - Create/Modify
DELETE - Deletion
@ api ({ httpMethod: "GET" })
async findById ( id : number ): Promise < User > {
// ...
}
// Endpoint: GET /user/findById?id=1
Default : If httpMethod is omitted, GET is the default.@ api () // httpMethod: "GET"
async findById ( id : number ) { }
clients
Specifies which client code types to generate.
Client Description Use Case axiosAxios-based function General API calls axios-multipartAxios for file uploads Image uploads tanstack-queryQuery hook Data retrieval tanstack-mutationMutation hook Data modification window-fetchFetch API Browser native
Retrieval API - Query
Modification API - Mutation
File Upload
@ api ({
httpMethod: "GET" ,
clients: [ "axios" , "tanstack-query" ],
})
async findById ( id : number ): Promise < User > {
// ...
}
Generated client code :
axios
tanstack-query
tanstack-mutation
// services/user.service.ts
export async function findUserById ( id : number ) : Promise < User > {
const { data } = await axios . get ( "/user/findById" , { params: { id } });
return data ;
}
Default : If clients is omitted, ["axios"] is the default.
path
Specifies a custom API path.
// Default path
@ api ({ httpMethod: "GET" })
async findById ( id : number ) { }
// Path: /user/findById
// Custom path
@ api ({ httpMethod: "GET" , path: "/api/v1/users/:id" })
async findById ( id : number ) { }
// Path: /api/v1/users/:id
Path parameters :
@ api ({ httpMethod: "GET" , path: "/posts/:postId/comments/:commentId" })
async findComment ( postId : number , commentId : number ): Promise < Comment > {
// ...
}
// Call: GET /posts/123/comments/456
If path is omitted, it’s auto-generated in /{model}/{method} format.
Model: UserModel → user
Method: findById → findById
Result: /user/findById
resourceName
Specifies the API resource name. Used in TanStack Query’s queryKey.
@ api ({
httpMethod: "GET" ,
resourceName: "Users" , // Plural
clients: [ "tanstack-query" ],
})
async findMany (): Promise < User [] > {
// ...
}
// Generated Query Hook
export function useUsers () {
return useQuery ({
queryKey: [ "Users" , "findMany" ], // Uses resourceName
queryFn : () => findManyUsers (),
});
}
Naming guide :
API Type resourceName Example Single retrieval Singular UserList retrieval Plural UsersCreate/update Singular UserDelete Plural Users
guards
Sets up authentication and permission checks.
// Authentication only
@ api ({ guards: [ "user" ] })
async getMyProfile (): Promise < User > {
// Only logged-in users can access
}
// Admin permission required
@ api ({ guards: [ "admin" ] })
async deleteUser ( id : number ): Promise < void > {
// Only admins can access
}
// Multiple guards combination
@ api ({ guards: [ "user" , "admin" ] })
async someAdminAction (): Promise < void > {
// Must satisfy both user AND admin
}
Guard types :
Guard Description Checks userRequires login context.user existenceadminAdmin permission context.user.role === "admin"queryCustom check User-defined logic
Guard logic is defined in guardHandler in sonamu.config.ts. export default {
guardHandler : ( guard , request , api ) => {
if ( guard === "user" && ! request . user ) {
throw new UnauthorizedException ( "Login required" );
}
if ( guard === "admin" && request . user ?. role !== "admin" ) {
throw new ForbiddenException ( "Admin permission required" );
}
} ,
} ;
contentType
Specifies the response Content-Type.
// JSON (default)
@ api ({ contentType: "application/json" })
async getUser (): Promise < User > {
return { id : 1 , name : "John" };
}
// HTML
@ api ({ contentType: "text/html" })
async renderProfile (): Promise < string > {
return "<html><body>Profile</body></html>" ;
}
// Plain Text
@api({ contentType: "text/plain" })
async getLog (): Promise < string > {
return "Log content..." ;
}
// Binary (file download)
@api({ contentType: "application/octet-stream" })
async downloadFile (): Promise < Buffer > {
return fileBuffer;
}
timeout
Specifies API timeout in milliseconds.
@ api ({
httpMethod: "GET" ,
timeout: 5000 , // 5 seconds
})
async longRunningQuery (): Promise < r > {
// Times out if takes more than 5 seconds
}
Timeout is a client-side setting. The server may continue executing, so you may need to set server-side timeout separately.
cacheControl
Sets HTTP Cache-Control headers.
// 1 minute caching
@ api ({
cacheControl: {
maxAge: 60 , // seconds
},
})
async getStaticData (): Promise < Data > {
// ...
}
// Disable caching
@ api ({
cacheControl: {
maxAge: 0 ,
noCache: true ,
},
})
async getDynamicData (): Promise < Data > {
// ...
}
CacheControl options :
Option Type Description Example maxAgenumber Max cache time (seconds) 60noCacheboolean Don’t use cache truenoStoreboolean Don’t store trueprivateboolean Per-user cache truepublicboolean Public cache true
compress
Controls response compression settings.
// Enable compression (default)
@ api ({ compress: true })
async getData (): Promise < LargeData > {
// Response is gzip compressed
}
// Disable compression
@ api ({ compress: false })
async streamData (): Promise < StreamData > {
// Send without compression (suitable for streaming)
}
Combining Multiple Decorators
@api + @transactional
Execute API within a transaction.
@ api ({ httpMethod: "POST" })
@ transactional ()
async updateUserAndProfile (
userId : number ,
userData : UserData ,
profileData : ProfileData
): Promise < void > {
await this.save( [userData]);
await ProfileModel.save([profileData]);
// Auto commit or rollback
}
@api + @upload
Create file upload API.
@ api ({
httpMethod: "POST" ,
clients: [ "axios-multipart" ],
})
@ upload ({ mode: "single" })
async uploadAvatar (): Promise < { url : string } > {
const { files } = Sonamu.getContext();
const file = files ?.[ 0 ]; // Use first file
if (! file ) {
throw new BadRequestException ( "No file provided" );
}
// Process file...
const url = await saveToS3 ( file );
return { url };
}
Upload options :
Option Description Use Case mode: "single"Single file Profile image mode: "multiple"Multiple files Gallery upload
API Path Rules
Default paths are generated with the following rules:
/{modelName}/{methodName}
Conversion rules :
Model name: PascalCase → camelCase
UserModel → user
BlogPostModel → blogPost
Method name: Used as-is
Examples :
Model Method Path UserModelfindById/user/findByIdBlogPostModelfindMany/blogPost/findManyCommentModelsave/comment/save
Practical Examples
Basic CRUD API
Retrieval API
List API
Create/Update API
Delete API
@ api ({
httpMethod: "GET" ,
clients: [ "axios" , "tanstack-query" ],
resourceName: "User" ,
})
async findById ( id : number ): Promise < User > {
const user = await this . getPuri ( "r" )
.where( "id" , id)
.first();
if (! user ) {
throw new NotFoundException ( `User not found: ${ id } ` );
}
return user;
}
Authentication API
@ api ({
httpMethod: "POST" ,
clients: [ "axios" , "tanstack-mutation" ],
})
async login ( params : LoginParams ): Promise < { user : User ; token : string } > {
const { email , password } = params;
// Find user
const user = await this . getPuri ( "r" )
.where( "email" , email)
.first();
if (! user ) {
throw new UnauthorizedException ( "Email or password does not match" );
}
// Verify password
const isValid = await bcrypt . compare ( password , user . password );
if (! isValid ) {
throw new UnauthorizedException ( "Email or password does not match" );
}
// Create session
const context = Sonamu . getContext ();
await context.passport.login(user);
// Generate token (optional)
const token = jwt . sign ({ userId: user . id }, SECRET_KEY );
return { user , token };
}
@ api ({
httpMethod: "GET" ,
clients: [ "axios" , "tanstack-query" ],
guards: [ "user" ],
})
async me (): Promise < User | null > {
const context = Sonamu . getContext ();
if (!context.user) {
return null ;
}
return this.findById(context.user.id);
}
Next Steps