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
}

@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:
OptionDescriptionUse Case
mode: "single"Single fileProfile image
mode: "multiple"Multiple filesGallery upload

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

@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