> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @api Decorator

> Transform methods into API endpoints

The `@api` decorator automatically transforms Model class methods into HTTP API endpoints.

## Decorator Overview

<CardGroup cols={2}>
  <Card title="Automatic Routing" icon="route">
    Transform methods to APIs Auto-generate URLs
  </Card>

  <Card title="Type Safety" icon="shield">
    Parameter type validation Compile-time checks
  </Card>

  <Card title="HTTP Methods" icon="globe">
    GET, POST, PUT, DELETE RESTful API support
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation">
    Automatic error conversion Consistent response format
  </Card>
</CardGroup>

## Basic Usage

### Simplest Form

```typescript theme={null}
import { BaseModelClass, api } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    return user;
  }
}

export const UserModel = new UserModelClass();
```

**Generated endpoint**:

* URL: `GET /api/user/getUser`
* Parameters: `{ id: number }`
* Response: `User` object

### Specifying HTTP Methods

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // GET request
  @api({ httpMethod: "GET" })
  async list(): Promise<User[]> {
    const rdb = this.getPuri("r");
    return rdb.table("users").select("*");
  }

  // POST request
  @api({ httpMethod: "POST" })
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");
    const [userId] = await wdb.table("users").insert(params).returning("id");

    return { userId: userId.id };
  }

  // PUT request
  @api({ httpMethod: "PUT" })
  async update(id: number, params: Partial<UserSaveParams>): Promise<void> {
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", id).update(params);
  }

  // DELETE request
  @api({ httpMethod: "DELETE" })
  async remove(id: number): Promise<void> {
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", id).delete();
  }
}

export const UserModel = new UserModelClass();
```

## API Routing Rules

### URL Generation Pattern

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries); // ← Model name used in URL
  }

  @api({ httpMethod: "GET" })
  async getProfile(userId: number) {
    // URL: GET /api/user/getProfile
    // ...
  }

  @api({ httpMethod: "POST" })
  async register(params: RegisterParams) {
    // URL: POST /api/user/register
    // ...
  }
}

export const UserModel = new UserModelClass();
```

```mermaid theme={null}
flowchart LR
    A[Model Class] --> B[modelName: User]
    C[Method] --> D[getProfile]
    E[httpMethod] --> F[GET]

    B --> G[/api/user]
    D --> G
    F --> H[GET /api/user/getProfile]
    G --> H

    style A fill:#e3f2fd
    style C fill:#e8f5e9
    style E fill:#fff9c4
    style H fill:#c8e6c9
```

<Info>
  **URL Rules**: - Base path: `/api/{modelName}/{methodName}` - modelName is converted to lowercase

  * Example: `UserModel.getProfile` → `/api/user/getProfile`
</Info>

## Parameter Handling

### Single Parameter

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Number parameter
  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    // GET /api/user/getUser?id=1
    // ...
  }

  // String parameter
  @api({ httpMethod: "GET" })
  async findByEmail(email: string): Promise<User | null> {
    // GET /api/user/findByEmail?email=test@example.com
    // ...
  }

  // Boolean parameter
  @api({ httpMethod: "GET" })
  async listActive(active: boolean): Promise<User[]> {
    // GET /api/user/listActive?active=true
    // ...
  }
}

export const UserModel = new UserModelClass();
```

### Complex Parameters (Objects)

```typescript theme={null}
interface UserListParams {
  page?: number;
  pageSize?: number;
  search?: string;
  role?: UserRole;
}

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    users: User[];
    total: number;
  }> {
    const rdb = this.getPuri("r");

    let query = rdb.table("users");

    if (params.search) {
      query = query.where("username", "like", `%${params.search}%`);
    }

    if (params.role) {
      query = query.where("role", params.role);
    }

    const page = params.page || 1;
    const pageSize = params.pageSize || 20;

    const users = await query
      .limit(pageSize)
      .offset((page - 1) * pageSize)
      .select("*");

    const [{ count }] = await rdb.table("users").count({ count: "*" });

    return { users, total: count };
  }
}

// Call: GET /api/user/list?page=1&pageSize=20&search=john&role=admin
```

### Multiple Parameters

```typescript theme={null}
class OrderModelClass extends BaseModelClass<
  OrderSubsetKey,
  OrderSubsetMapping,
  typeof orderSubsetQueries,
  typeof orderLoaderQueries
> {
  constructor() {
    super("Order", orderSubsetQueries, orderLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async createOrder(
    userId: number,
    items: OrderItem[],
    shippingAddress: string,
  ): Promise<{ orderId: number }> {
    // POST /api/order/createOrder
    // Body: { userId: 1, items: [...], shippingAddress: "..." }

    const wdb = this.getPuri("w");

    const [order] = await wdb
      .table("orders")
      .insert({
        user_id: userId,
        shipping_address: shippingAddress,
        status: "pending",
      })
      .returning("id");

    // Save order items
    for (const item of items) {
      await wdb.table("order_items").insert({
        order_id: order.id,
        product_id: item.productId,
        quantity: item.quantity,
      });
    }

    return { orderId: order.id };
  }
}
```

## Return Types

### Basic Types

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Return object
  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    // ...
    return user;
  }

  // Return array
  @api({ httpMethod: "GET" })
  async list(): Promise<User[]> {
    // ...
    return users;
  }

  // void (no response)
  @api({ httpMethod: "DELETE" })
  async remove(id: number): Promise<void> {
    // ...
    // Empty response on success
  }

  // Return number
  @api({ httpMethod: "GET" })
  async count(): Promise<number> {
    // ...
    return count;
  }
}
```

### Structured Response

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    data: User[];
    pagination: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    };
  }> {
    const rdb = this.getPuri("r");

    const page = params.page || 1;
    const pageSize = params.pageSize || 20;

    const users = await rdb
      .table("users")
      .limit(pageSize)
      .offset((page - 1) * pageSize)
      .select("*");

    const [{ count }] = await rdb.table("users").count({ count: "*" });

    return {
      data: users,
      pagination: {
        page,
        pageSize,
        total: count,
        totalPages: Math.ceil(count / pageSize),
      },
    };
  }
}
```

## Decorator Combinations

### @api + @transactional

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Order doesn't matter
  @api({ httpMethod: "POST" })
  @transactional()
  async register(params: RegisterParams): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");

    // Check duplicates
    const existing = await wdb.table("users").where("email", params.email).first();

    if (existing) {
      throw new Error("Email already exists");
    }

    // Create User
    wdb.ubRegister("users", {
      email: params.email,
      username: params.username,
      password: params.password,
      role: "normal",
    });

    const [userId] = await wdb.ubUpsert("users");

    return { userId };
  }

  // Reverse order also works
  @transactional()
  @api({ httpMethod: "PUT" })
  async updateProfile(userId: number, params: ProfileParams): Promise<void> {
    const wdb = this.getPuri("w");

    await wdb.table("users").where("id", userId).update({
      bio: params.bio,
      avatar_url: params.avatarUrl,
    });
  }
}
```

### @api + cacheControl/compress Options

You can control caching and compression of API responses directly from the `@api` decorator.

#### Cache-Control Configuration

```typescript theme={null}
class PostModelClass extends BaseModelClass<
  PostSubsetKey,
  PostSubsetMapping,
  typeof postSubsetQueries,
  typeof postLoaderQueries
> {
  constructor() {
    super("Post", postSubsetQueries, postLoaderQueries);
  }

  // Public post list - 1 hour cache
  @api({
    httpMethod: "GET",
    cacheControl: {
      maxAge: 3600, // Browser cache: 1 hour
      sMaxAge: 7200, // CDN cache: 2 hours
      staleWhileRevalidate: 86400, // Allow stale state for 24 hours
    },
  })
  async getPublicPosts(): Promise<Post[]> {
    const rdb = this.getPuri("r");
    return rdb.table("posts").where("public", true).select("*");
  }

  // Using cache presets
  @api({
    httpMethod: "GET",
    cacheControl: "5m", // 5 minute cache
  })
  async getTrendingPosts(): Promise<Post[]> {
    // ...
  }
}
```

**Cache-Control Options**:

* `maxAge`: Browser cache duration (seconds)
* `sMaxAge`: CDN/proxy cache duration (seconds)
* `staleWhileRevalidate`: Stale state allowance duration (seconds)
* `public`: Public cache flag (default: true)
* `private`: Private cache (per-user)

**Preset Strings**:

* Time unit strings like `"1m"`, `"5m"`, `"1h"`, `"1d"` can be used

#### Compression Configuration

```typescript theme={null}
class DataModelClass extends BaseModelClass<
  DataSubsetKey,
  DataSubsetMapping,
  typeof dataSubsetQueries,
  typeof dataLoaderQueries
> {
  constructor() {
    super("Data", dataSubsetQueries, dataLoaderQueries);
  }

  // Large dataset - enable compression
  @api({
    httpMethod: "GET",
    compress: true, // Enable gzip/deflate compression
  })
  async getLargeDataset(): Promise<DataPoint[]> {
    const rdb = this.getPuri("r");
    return rdb.table("data_points").limit(10000).select("*");
  }

  // Already compressed data - disable compression
  @api({
    httpMethod: "GET",
    compress: false, // Disable compression
  })
  async getCompressedFile(): Promise<Buffer> {
    // Return already compressed file
    // ...
  }
}
```

**Compression Options**:

* `true`: Compress response with gzip/deflate
* `false`: Disable compression (default)

#### Combined Usage

```typescript theme={null}
class ApiModelClass extends BaseModelClass<
  ApiSubsetKey,
  ApiSubsetMapping,
  typeof apiSubsetQueries,
  typeof apiLoaderQueries
> {
  constructor() {
    super("Api", apiSubsetQueries, apiLoaderQueries);
  }

  // Caching + compression
  @api({
    httpMethod: "GET",
    cacheControl: { maxAge: 3600, sMaxAge: 7200 },
    compress: true,
  })
  async getReport(reportId: number): Promise<Report> {
    const rdb = this.getPuri("r");

    // Large report data
    const report = await rdb.table("reports").where("id", reportId).first();

    if (!report) {
      throw new Error("Report not found");
    }

    return report;
  }
}
```

**Practical Example: API Optimization**:

```typescript theme={null}
class OptimizedApiModelClass extends BaseModelClass<
  OptimizedApiSubsetKey,
  OptimizedApiSubsetMapping,
  typeof optimizedApiSubsetQueries,
  typeof optimizedApiLoaderQueries
> {
  constructor() {
    super("OptimizedApi", optimizedApiSubsetQueries, optimizedApiLoaderQueries);
  }

  // Static content: long cache + compression
  @api({
    httpMethod: "GET",
    cacheControl: { maxAge: 86400, sMaxAge: 604800 }, // 1 day / 1 week
    compress: true,
  })
  async getStaticContent(): Promise<Content> {
    // ...
  }

  // Dynamic content: short cache + compression
  @api({
    httpMethod: "GET",
    cacheControl: "5m",
    compress: true,
  })
  async getDynamicData(): Promise<Data[]> {
    // ...
  }

  // Private data: no cache (private)
  @api({
    httpMethod: "GET",
    cacheControl: { private: true, maxAge: 0 },
  })
  async getUserPrivateData(userId: number): Promise<PrivateData> {
    // ...
  }

  // Large download: compression only
  @api({
    httpMethod: "GET",
    compress: true,
  })
  async downloadLargeFile(fileId: string): Promise<Buffer> {
    // ...
  }
}
```

<Info>
  **Performance Tips**:

  * Use `cacheControl` for static or infrequently changing data
  * Use `compress: true` for responses larger than 10KB
  * Use `{ private: true, maxAge: 0 }` to prevent caching of private data
</Info>

<Warning>
  **Caution**: - `compress: true` increases CPU usage (inefficient for small responses) - Excessively
  long cache durations can delay update propagation - `private: true` disables CDN caching
</Warning>

## Error Handling

### Automatic Error Conversion

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      // Error object → HTTP 500
      throw new Error("User not found");
    }

    return user;
  }

  @api({ httpMethod: "POST" })
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");

    try {
      const [userId] = await wdb.table("users").insert(params).returning("id");

      return { userId: userId.id };
    } catch (error) {
      // DB error → HTTP 500
      throw new Error("Failed to create user");
    }
  }
}
```

<Warning>
  By default, all errors are converted to HTTP 500. For custom error handling, you need to implement
  a separate error handler.
</Warning>

## Practical Examples

### CRUD API

```typescript theme={null}
interface UserSaveParams {
  email: string;
  username: string;
  password: string;
  role: UserRole;
}

interface UserListParams {
  page?: number;
  pageSize?: number;
  search?: string;
}

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Create
  @api({ httpMethod: "POST" })
  @transactional()
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");

    wdb.ubRegister("users", params);
    const [userId] = await wdb.ubUpsert("users");

    return { userId };
  }

  // Read (single)
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    return user;
  }

  // Read (list)
  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    users: User[];
    total: number;
  }> {
    const rdb = this.getPuri("r");

    let query = rdb.table("users");

    if (params.search) {
      query = query.where("username", "like", `%${params.search}%`);
    }

    const page = params.page || 1;
    const pageSize = params.pageSize || 20;

    const users = await query
      .limit(pageSize)
      .offset((page - 1) * pageSize)
      .select("*");

    const [{ count }] = await rdb.table("users").count({ count: "*" });

    return { users, total: count };
  }

  // Update
  @api({ httpMethod: "PUT" })
  @transactional()
  async update(id: number, params: Partial<UserSaveParams>): Promise<void> {
    const wdb = this.getPuri("w");

    await wdb.table("users").where("id", id).update(params);
  }

  // Delete
  @api({ httpMethod: "DELETE" })
  @transactional()
  async remove(id: number): Promise<void> {
    const wdb = this.getPuri("w");

    await wdb.table("users").where("id", id).delete();
  }
}
```

### Complex Business Logic

```typescript theme={null}
class OrderModelClass extends BaseModelClass<
  OrderSubsetKey,
  OrderSubsetMapping,
  typeof orderSubsetQueries,
  typeof orderLoaderQueries
> {
  constructor() {
    super("Order", orderSubsetQueries, orderLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  @transactional()
  async placeOrder(params: {
    userId: number;
    items: Array<{ productId: number; quantity: number }>;
    shippingAddress: string;
    paymentMethod: string;
  }): Promise<{
    orderId: number;
    totalAmount: number;
  }> {
    const wdb = this.getPuri("w");

    // 1. Check inventory
    for (const item of params.items) {
      const product = await wdb.table("products").where("id", item.productId).first();

      if (!product || product.stock < item.quantity) {
        throw new Error(`Insufficient stock for product ${item.productId}`);
      }
    }

    // 2. Calculate total
    let totalAmount = 0;
    for (const item of params.items) {
      const product = await wdb.table("products").where("id", item.productId).first();

      totalAmount += product.price * item.quantity;
    }

    // 3. Create order
    const [order] = await wdb
      .table("orders")
      .insert({
        user_id: params.userId,
        total_amount: totalAmount,
        shipping_address: params.shippingAddress,
        payment_method: params.paymentMethod,
        status: "pending",
      })
      .returning("id");

    // 4. Create order items
    for (const item of params.items) {
      await wdb.table("order_items").insert({
        order_id: order.id,
        product_id: item.productId,
        quantity: item.quantity,
      });

      // Deduct inventory
      await wdb.table("products").where("id", item.productId).decrement("stock", item.quantity);
    }

    return {
      orderId: order.id,
      totalAmount,
    };
  }
}
```

## Type Safety

### Parameter Type Validation

```typescript theme={null}
interface CreateUserParams {
  email: string;
  username: string;
  password: string;
  role: "admin" | "normal";
}

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    // TypeScript validates params type
    // params.email, params.username, etc. have autocomplete

    const wdb = this.getPuri("w");

    // ✅ OK if types match
    const [userId] = await wdb
      .table("users")
      .insert({
        email: params.email,
        username: params.username,
        password: params.password,
        role: params.role,
      })
      .returning("id");

    return { userId: userId.id };
  }
}

// When calling:
// POST /api/user/create
// Body: {
//   "email": "test@example.com",
//   "username": "testuser",
//   "password": "hashedpass",
//   "role": "normal"  // Only "admin" or "normal" allowed
// }
```

### Explicit Return Types

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Explicit return type ensures type safety
  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<{
    user: User;
    profile: Profile | null;
  }> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    const profile = await rdb.table("profiles").where("user_id", id).first();

    // ✅ Return must match the declared structure
    return {
      user,
      profile: profile || null,
    };
  }
}
```

## Cautions

<Warning>
  **Cautions when using @api**: 1. Only usable in Model classes 2. Method must be `async` function
  3\. `modelName` property is required 4. Specifying parameter/return types is recommended 5.
  Propagate errors with throw
</Warning>

### Common Mistakes

```typescript theme={null}
// ❌ Wrong: No constructor and super() call
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    // ...
  }
}

// ❌ Wrong: Not async
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  getUser(id: number): User {
    // ...
  }
}

// ❌ Wrong: No type specified
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async create(params): Promise<any> {
    // params and return type are any
  }
}

// ✅ Correct
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    // ...
  }

  @api({ httpMethod: "POST" })
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    // ...
  }
}

export const UserModel = new UserModelClass();
```

## Next Steps

<CardGroup cols={2}>
  <Card title="HTTP Methods" icon="globe" href="/en/api-development/creating-apis/http-methods">
    GET, POST, PUT, DELETE details
  </Card>

  <Card title="Parameters" icon="sliders" href="/en/api-development/creating-apis/parameters">
    Type definitions and validation
  </Card>

  <Card title="Return Types" icon="arrow-turn-down-left" href="/en/api-development/creating-apis/return-types">
    Defining response types
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/en/api-development/error-handling">
    API error handling
  </Card>
</CardGroup>
