Skip to main content
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.
MethodUseExamples
GETData retrievalfindById, findMany
POSTData create/modifysave, login
PUTData updateupdate
DELETEData deletiondel, remove
PATCHPartial updateupdateProfile
@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.
ClientDescriptionUse Case
axiosAxios-based functionGeneral API calls
axios-multipartAxios for file uploadsImage uploads
tanstack-queryQuery hookData retrieval
tanstack-mutationMutation hookData modification
window-fetchFetch APIBrowser native
@api({
  httpMethod: "GET",
  clients: ["axios", "tanstack-query"],
})
async findById(id: number): Promise<User> {
  // ...
}
Generated client code:
// 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: UserModeluser
  • Method: findByIdfindById
  • 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 TyperesourceNameExample
Single retrievalSingularUser
List retrievalPluralUsers
Create/updateSingularUser
DeletePluralUsers

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:
GuardDescriptionChecks
userRequires logincontext.user existence
adminAdmin permissioncontext.user.role === "admin"
queryCustom checkUser-defined logic
Guard logic is defined in guardHandler in sonamu.config.ts.
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:
OptionTypeDescriptionExample
maxAgenumberMax cache time (seconds)60
noCachebooleanDon’t use cachetrue
noStorebooleanDon’t storetrue
privatebooleanPer-user cachetrue
publicbooleanPublic cachetrue

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
}

@upload (Standalone)

Creates a file upload API. @upload is used independently without @api.
@upload()
async uploadAvatar(): Promise<{ url: string }> {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0]; // Use first file

  if (!file) {
    throw new BadRequestException("No file provided");
  }

  // Process file...
  const url = await file.saveToDisk("fs", `avatars/${Date.now()}-${file.filename}`);

  return { url };
}
Upload options:
OptionDescriptionUse Case
limits.fileSizeMaximum file size (bytes)10 * 1024 * 1024 (10MB)
limits.filesMaximum number of files5
Single/multiple file handling is determined by how you access bufferedFiles from the context.

API Path Rules

Default paths are generated with the following rules:
/{modelName}/{methodName}
Conversion rules:
  • Model name: PascalCase → camelCase
    • UserModeluser
    • BlogPostModelblogPost
  • Method name: Used as-is
    • findByIdfindById
Examples:
ModelMethodPath
UserModelfindById/user/findById
BlogPostModelfindMany/blogPost/findMany
CommentModelsave/comment/save

Practical Examples

Basic CRUD 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

Sonamu handles authentication through better-auth’s HTTP endpoints. For login (/api/auth/sign-in/email), logout (/api/auth/sign-out), and other auth operations, use the endpoints provided directly by better-auth rather than Sonamu @api decorated methods.
The me() API that returns the current logged-in user’s information is implemented via context.user.
@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

Business Logic

Learn business logic writing patterns

Stream Decorator

Send real-time events with @stream

Upload Decorator

Handle file uploads with @upload

Guards

Implementing authentication and authorization