> ## 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

> API Endpoint Creation Decorator

The `@api` decorator exposes methods of Model or Frame classes as HTTP API endpoints.

## Basic Usage

```typescript theme={null}
import { BaseModelClass, api } from "sonamu";

class UserModelClass extends BaseModelClass<UserSubsetKey, UserSubsetMapping, UserSubsetQueries> {
  @api({ httpMethod: "GET" })
  async findById(subset: UserSubsetKey, id: number) {
    const rdb = this.getPuri("r");
    return rdb.table("users").where("id", id).first();
  }

  @api({ httpMethod: "POST" })
  async save(data: UserSaveParams) {
    const wdb = this.getDB("w");
    return this.upsert(wdb, data);
  }
}

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

## Options

### httpMethod

Specifies the HTTP method.

```typescript theme={null}
type HTTPMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
```

**Default:** `"GET"`

```typescript theme={null}
@api({ httpMethod: "POST" })
async create(data: CreateParams) {
  // Exposed as POST request
}

@api({ httpMethod: "DELETE" })
async remove(id: number) {
  // Exposed as DELETE request
}
```

### path

Specifies the API endpoint path.

**Default:** `/{modelName}/{methodName}` (camelCase)

```typescript theme={null}
@api({ path: "/api/v1/users/profile" })
async getProfile() {
  // Accessible at /api/v1/users/profile
}

@api()
async findById(id: number) {
  // Default path: /user/findById
}
```

<Info>Paths are automatically converted to camelCase. `UserModel.findById` → `/user/findById`</Info>

### contentType

Specifies the Content-Type of the response.

**Default:** `"application/json"`

```typescript theme={null}
type ContentType =
  | "text/plain"
  | "text/html"
  | "text/xml"
  | "application/json"
  | "application/octet-stream";
```

```typescript theme={null}
// CSV file download
@api({ contentType: "text/plain" })
async exportCsv() {
  return "id,name,email\n1,Alice,alice@example.com";
}

// HTML rendering
@api({ contentType: "text/html" })
async renderTemplate() {
  return "<html><body>Hello</body></html>";
}

// Binary file download (images, PDFs, etc.)
@api({ contentType: "application/octet-stream" })
async downloadFile(fileId: number, ctx: Context) {
  // Fetch file
  const file = await this.getPuri("r")
    .table("files")
    .where("id", fileId)
    .first();

  if (!file) {
    throw new NotFoundError("File not found");
  }

  // Read file from storage
  const disk = Sonamu.storage.use();
  const buffer = await disk.get(file.path);

  // Set download filename with Content-Disposition header
  ctx.reply.header(
    "Content-Disposition",
    `attachment; filename="${encodeURIComponent(file.original_name)}"`
  );

  return buffer;
}
```

**contentType Use Cases**:

| Content-Type               | Use Case                                | Return Type              |
| -------------------------- | --------------------------------------- | ------------------------ |
| `text/plain`               | CSV, TXT file download                  | `string`                 |
| `text/html`                | HTML rendering (SSR)                    | `string`                 |
| `text/xml`                 | XML data response                       | `string`                 |
| `application/json`         | JSON data (default)                     | `object`                 |
| `application/octet-stream` | Binary files (images, PDFs, ZIPs, etc.) | `Buffer` or `Uint8Array` |

<Info>
  **Notes for application/octet-stream**: - Must return `Buffer` or `Uint8Array` - Use
  `Content-Disposition` header to specify download filename - Use `encodeURIComponent()` for
  filenames containing non-ASCII characters - Consider streaming for large files
</Info>

### clients

Specifies the client types to generate.

**Default:** `["axios"]`

```typescript theme={null}
type ServiceClient =
  | "axios" // Axios client
  | "axios-multipart" // Multipart form-data
  | "tanstack-query" // TanStack Query (read)
  | "tanstack-mutation" // TanStack Mutation (write)
  | "tanstack-mutation-multipart" // TanStack Mutation (file upload)
  | "window-fetch"; // Native Fetch API
```

```typescript theme={null}
@api({
  httpMethod: "GET",
  clients: ["axios", "tanstack-query"]
})
async list() {
  // Generates axios and TanStack Query clients
}

@api({
  httpMethod: "POST",
  clients: ["axios", "tanstack-mutation"]
})
async create(data: CreateParams) {
  // Generates axios and TanStack Mutation clients
}
```

### guards

Specifies API access permissions.

```typescript theme={null}
type GuardKey = "query" | "admin" | "user";
```

```typescript theme={null}
@api({ guards: ["admin"] })
async deleteUser(id: number) {
  // Admin only
}

@api({ guards: ["user"] })
async getProfile() {
  // Authenticated users only
}

@api({ guards: ["query", "admin"] })
async search(query: string) {
  // Requires query or admin permission
}
```

### description

Adds API description. Included in generated types and documentation.

```typescript theme={null}
@api({
  description: "Fetches user profile by ID."
})
async findById(id: number) {
  // ...
}
```

### resourceName

Specifies the resource name for generated service files.

```typescript theme={null}
@api({ resourceName: "Users" })
async list() {
  // Included in UsersService.ts file
}
```

### timeout

Specifies the API request timeout in milliseconds.

```typescript theme={null}
@api({ timeout: 30000 })  // 30 seconds
async heavyOperation() {
  // Long-running task
}
```

### cacheControl

Sets the Cache-Control header for the response.

```typescript theme={null}
@api({
  cacheControl: {
    maxAge: "10m",        // Cache for 10 minutes
    sMaxAge: "1h",        // Cache on CDN for 1 hour
    public: true
  }
})
async getPublicData() {
  // Cache-Control: public, max-age=600, s-maxage=3600
}
```

**CacheControlConfig Type:**

```typescript theme={null}
type CacheControlConfig = {
  maxAge?: string; // Browser cache duration
  sMaxAge?: string; // CDN/proxy cache duration
  public?: boolean; // public/private
  noCache?: boolean; // no-cache
  noStore?: boolean; // no-store
  mustRevalidate?: boolean;
};
```

<Tip>Time notation supports: `"10s"`, `"5m"`, `"1h"`, `"1d"` formats</Tip>

### compress

Specifies response compression settings.

```typescript theme={null}
@api({
  compress: {
    threshold: 1024,      // Compress only if >= 1KB
    level: 6              // Compression level (0-9)
  }
})
async getLargeData() {
  // Returns large data
}

@api({ compress: false })  // Disable compression
async getSmallData() {
  // Small data not compressed
}
```

## Complete Options Example

```typescript theme={null}
@api({
  httpMethod: "POST",
  path: "/api/v1/users/search",
  contentType: "application/json",
  clients: ["axios", "tanstack-query"],
  guards: ["user"],
  description: "User search API",
  resourceName: "Users",
  timeout: 5000,
  cacheControl: {
    maxAge: "5m",
    public: true
  },
  compress: {
    threshold: 1024,
    level: 6
  }
})
async search(params: SearchParams) {
  // Implementation
}
```

## Path Generation Rules

### Model Classes

```typescript theme={null}
class UserModelClass extends BaseModelClass {
  @api()
  async findById(id: number) {}
  // Path: /user/findById
}

class PostModelClass extends BaseModelClass {
  @api()
  async getComments() {}
  // Path: /post/getComments
}
```

**Rules:**

1. Remove "ModelClass" from class name
2. Convert the rest to camelCase
3. Format: `/{modelName}/{methodName}`

### Frame Classes

```typescript theme={null}
class AuthFrameClass extends BaseFrameClass {
  @api()
  async login() {}
  // Path: /auth/login
}
```

**Rules:**

1. Remove "FrameClass" from class name
2. Convert the rest to camelCase
3. Format: `/{frameName}/{methodName}`

## Using with Other Decorators

### @transactional

```typescript theme={null}
@api({ httpMethod: "POST" })
@transactional()
async save(data: UserSaveParams) {
  const wdb = this.getDB("w");
  // Executed within transaction
  return this.upsert(wdb, data);
}
```

<Warning>Write `@api` first, then `@transactional`.</Warning>

### @cache

```typescript theme={null}
@api({ httpMethod: "GET" })
@cache({ ttl: "10m" })
async findById(id: number) {
  // Result cached for 10 minutes
  const rdb = this.getPuri("r");
  return rdb.table("users").where("id", id).first();
}
```

### @upload

`@upload` is used independently without `@api`.

```typescript theme={null}
@upload()
async uploadAvatar() {
  const { files } = Sonamu.getContext();
  const file = files?.[0]; // Use first file
  // Process file
}
```

## Constraints

### 1. Cannot be combined with other routing decorators (@stream, @websocket, @upload)

`@api`, `@stream`, `@websocket`, and `@upload` cannot be combined on the same method.

```typescript theme={null}
// ❌ Error
@api()
@stream({ type: "sse", events: EventSchema })
async subscribe() {}
```

**Error Message:**

```
@api decorator can only be used once on UserModel.subscribe.
You can use only one of @api, @stream, @websocket, or @upload decorator on the same method.
```

### 2. Using Multiple Times on Same Method

Using multiple times gives **last one priority**, and conflicting options cause errors:

```typescript theme={null}
// ❌ Error - path conflict
@api({ path: "/users/list" })
@api({ path: "/users/all" })
async list() {}
```

**Error Message:**

```
@api decorator on UserModel.list has conflicting path: /users/all.
The decorator is trying to override the existing path(/users/list)
with the new path(/users/all).
```

### 3. Only Available in Model/Frame Classes

```typescript theme={null}
// ❌ Not available in regular classes
class UtilClass {
  @api()
  async helper() {}
  // Error: modelName is required
}

// ✅ Requires BaseModelClass inheritance
class UserModelClass extends BaseModelClass {
  @api()
  async findById(id: number) {}
}
```

## Generated Code

Using the `@api` decorator automatically generates:

### 1. API Route Registration

```typescript theme={null}
// Fastify route automatically registered
fastify.get("/user/findById", async (request, reply) => {
  // Parameter validation and processing
  const result = await UserModel.findById(subset, id);
  return result;
});
```

### 2. Type Definitions

```typescript theme={null}
// api/src/application/services/UserService.types.ts
export type UserFindByIdParams = {
  subset: UserSubsetKey;
  id: number;
};

export type UserFindByIdResult = UserSubsetMapping[UserSubsetKey];
```

### 3. Client Code

**Axios:**

```typescript theme={null}
// web/src/services/UserService.ts
export const UserService = {
  async findById(params: UserFindByIdParams) {
    return axios.get<UserFindByIdResult>("/user/findById", { params });
  },
};
```

**TanStack Query:**

```typescript theme={null}
export const useUserFindById = (params: UserFindByIdParams) => {
  return useQuery({
    queryKey: ["user", "findById", params],
    queryFn: () => UserService.findById(params),
  });
};
```

## Logging

The `@api` decorator automatically logs:

```typescript theme={null}
@api({ httpMethod: "GET" })
async findById(id: number) {
  // Automatic log:
  // [DEBUG] api: GET UserModel.findById
}
```

Logs are recorded through LogTape, with categories in `[model:user]` or `[frame:auth]` format.

## Examples

<Tabs>
  <Tab title="Basic CRUD" icon="database">
    ```typescript theme={null}
    class UserModelClass extends BaseModelClass {
      @api({ httpMethod: "GET" })
      async list(params: UserListParams) {
        const rdb = this.getPuri("r");
        return rdb.table("users")
          .where("deleted_at", null)
          .paginate(params);
      }

      @api({ httpMethod: "GET" })
      async findById(id: number) {
        const rdb = this.getPuri("r");
        return rdb.table("users").where("id", id).first();
      }

      @api({ httpMethod: "POST" })
      @transactional()
      async create(data: UserCreateParams) {
        const wdb = this.getDB("w");
        return this.insert(wdb, data);
      }

      @api({ httpMethod: "PUT" })
      @transactional()
      async update(id: number, data: UserUpdateParams) {
        const wdb = this.getDB("w");
        return this.upsert(wdb, { id, ...data });
      }

      @api({ httpMethod: "DELETE" })
      @transactional()
      async delete(id: number) {
        const wdb = this.getDB("w");
        return wdb.table("users")
          .where("id", id)
          .update({ deleted_at: new Date() });
      }
    }
    ```
  </Tab>

  <Tab title="Caching" icon="bolt">
    ```typescript theme={null}
    class ProductModelClass extends BaseModelClass {
      @api({ httpMethod: "GET" })
      @cache({ ttl: "1h", tags: ["products"] })
      async featured() {
        // Cached for 1 hour
        const rdb = this.getPuri("r");
        return rdb.table("products")
          .where("featured", true)
          .limit(10);
      }

      @api({ httpMethod: "GET" })
      @cache({ ttl: "5m" })
      async findById(id: number) {
        // Cached for 5 minutes
        const rdb = this.getPuri("r");
        return rdb.table("products").where("id", id).first();
      }

      @api({ httpMethod: "POST" })
      @transactional()
      async update(id: number, data: ProductUpdateParams) {
        const wdb = this.getDB("w");
        const result = await this.upsert(wdb, { id, ...data });

        // Invalidate cache
        await Sonamu.cache.deleteByTags(["products"]);

        return result;
      }
    }
    ```
  </Tab>

  <Tab title="Access Control" icon="shield-check">
    ```typescript theme={null}
    class AdminFrameClass extends BaseFrameClass {
      @api({
        httpMethod: "GET",
        guards: ["admin"]
      })
      async dashboard() {
        // Admin only — use findMany with queryMode: "count" for aggregation only
        const [users, orders] = await Promise.all([
          UserModel.findMany("A", { queryMode: "count" }),
          OrderModel.findMany("A", { queryMode: "count" })
        ]);
        return {
          users: users.total ?? 0,
          orders: orders.total ?? 0
        };
      }

      @api({
        httpMethod: "POST",
        guards: ["admin"]
      })
      @transactional()
      async banUser(userId: number) {
        // Admin only
        const wdb = UserModel.getDB("w");
        return wdb.table("users")
          .where("id", userId)
          .update({ banned_at: new Date() });
      }
    }

    class UserFrameClass extends BaseFrameClass {
      @api({
        httpMethod: "GET",
        guards: ["user"]
      })
      async profile() {
        // Authenticated users only
        const { user } = Sonamu.getContext();
        return UserModel.findById(user.id);
      }
    }
    ```
  </Tab>

  <Tab title="Custom Paths" icon="route">
    ```typescript theme={null}
    class ApiFrameClass extends BaseFrameClass {
      @api({
        path: "/api/v1/health",
        httpMethod: "GET"
      })
      async health() {
        return { status: "ok" };
      }

      @api({
        path: "/api/v1/users/:id/profile",
        httpMethod: "GET"
      })
      async userProfile(id: number) {
        return UserModel.findById(id);
      }

      @api({
        path: "/webhooks/stripe",
        httpMethod: "POST",
        contentType: "application/json"
      })
      async stripeWebhook(payload: StripePayload) {
        // Handle Stripe webhook
      }
    }
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="@stream" icon="tower-broadcast" href="/en/api-reference/decorators/stream">
    Create SSE streaming APIs
  </Card>

  <Card title="@transactional" icon="arrows-rotate" href="/en/api-reference/decorators/transactional">
    Use database transactions
  </Card>

  <Card title="@upload" icon="upload" href="/en/api-reference/decorators/upload">
    Create file upload APIs
  </Card>

  <Card title="@cache" icon="database" href="/en/api-reference/decorators/cache">
    Cache method results
  </Card>
</CardGroup>
