메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
@api λ°μ½”λ ˆμ΄ν„°λŠ” Model λ©”μ„œλ“œλ₯Ό HTTP API μ—”λ“œν¬μΈνŠΈλ‘œ μžλ™ λ³€ν™˜ν•©λ‹ˆλ‹€. λ©”μ„œλ“œμ— λ°μ½”λ ˆμ΄ν„°λ₯Ό μΆ”κ°€ν•˜λ©΄ λΌμš°νŒ…, νƒ€μž… 검증, ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œκ°€ μžλ™ μƒμ„±λ©λ‹ˆλ‹€.

κΈ°λ³Έ μ‚¬μš©λ²•

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();
  }
}
μƒμ„±λ˜λŠ” 것듀:
  • HTTP μ—”λ“œν¬μΈνŠΈ: GET /user/findById?id=1
  • TypeScript ν΄λΌμ΄μ–ΈνŠΈ ν•¨μˆ˜
  • TanStack Query hooks (선택 μ‹œ)
  • API λ¬Έμ„œ

λ°μ½”λ ˆμ΄ν„° μ˜΅μ…˜

전체 μ˜΅μ…˜

@api({
  httpMethod: "GET",                    // HTTP λ©”μ„œλ“œ
  contentType: "application/json",      // Content-Type
  clients: ["axios", "tanstack-query"], // 생성할 ν΄λΌμ΄μ–ΈνŠΈ
  path: "/custom/path",                 // μ»€μŠ€ν…€ 경둜
  resourceName: "Users",                // λ¦¬μ†ŒμŠ€ 이름
  guards: ["admin", "user"],            // 인증/κΆŒν•œ κ°€λ“œ
  description: "μ‚¬μš©μž 쑰회 API",       // API μ„€λͺ…
  timeout: 5000,                        // νƒ€μž„μ•„μ›ƒ (ms)
  cacheControl: { maxAge: 60 },         // μΊμ‹œ μ„€μ •
  compress: false,                      // 응닡 μ••μΆ• λΉ„ν™œμ„±ν™”
})

httpMethod

HTTP λ©”μ„œλ“œλ₯Ό μ§€μ •ν•©λ‹ˆλ‹€.
λ©”μ„œλ“œμš©λ„μ˜ˆμ‹œ
GET데이터 쑰회findById, findMany
POST데이터 생성/μˆ˜μ •save, login
PUT데이터 μ—…λ°μ΄νŠΈupdate
DELETE데이터 μ‚­μ œdel, remove
PATCHλΆ€λΆ„ μ—…λ°μ΄νŠΈupdateProfile
@api({ httpMethod: "GET" })
async findById(id: number): Promise<User> {
  // ...
}
// μ—”λ“œν¬μΈνŠΈ: GET /user/findById?id=1
κΈ°λ³Έκ°’: httpMethodλ₯Ό μƒλž΅ν•˜λ©΄ GET이 κΈ°λ³Έκ°’μž…λ‹ˆλ‹€.
@api()  // httpMethod: "GET"
async findById(id: number) { }

clients

생성할 ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œ νƒ€μž…μ„ μ§€μ •ν•©λ‹ˆλ‹€.
Clientμ„€λͺ…μ‚¬μš© μ˜ˆμ‹œ
axiosAxios 기반 ν•¨μˆ˜μΌλ°˜ API 호좜
axios-multipart파일 μ—…λ‘œλ“œμš© Axios이미지 μ—…λ‘œλ“œ
tanstack-queryQuery hook데이터 쑰회
tanstack-mutationMutation hook데이터 λ³€κ²½
window-fetchFetch APIλΈŒλΌμš°μ € λ„€μ΄ν‹°λΈŒ
@api({
  httpMethod: "GET",
  clients: ["axios", "tanstack-query"],
})
async findById(id: number): Promise<User> {
  // ...
}
μƒμ„±λ˜λŠ” ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œ:
// services/user.service.ts
export async function findUserById(id: number): Promise<User> {
  const { data } = await axios.get("/user/findById", { params: { id } });
  return data;
}
κΈ°λ³Έκ°’: clientsλ₯Ό μƒλž΅ν•˜λ©΄ ["axios"]κ°€ κΈ°λ³Έκ°’μž…λ‹ˆλ‹€.

path

μ»€μŠ€ν…€ API 경둜λ₯Ό μ§€μ •ν•©λ‹ˆλ‹€.
// 기본 경둜
@api({ httpMethod: "GET" })
async findById(id: number) { }
// 경둜: /user/findById

// μ»€μŠ€ν…€ 경둜
@api({ httpMethod: "GET", path: "/api/v1/users/:id" })
async findById(id: number) { }
// 경둜: /api/v1/users/:id
경둜 νŒŒλΌλ―Έν„°:
@api({ httpMethod: "GET", path: "/posts/:postId/comments/:commentId" })
async findComment(postId: number, commentId: number): Promise<Comment> {
  // ...
}
// 호좜: GET /posts/123/comments/456
경둜λ₯Ό μƒλž΅ν•˜λ©΄ /{model}/{method} ν˜•μ‹μœΌλ‘œ μžλ™ μƒμ„±λ©λ‹ˆλ‹€.
  • Model: UserModel β†’ user
  • Method: findById β†’ findById
  • κ²°κ³Ό: /user/findById

resourceName

API λ¦¬μ†ŒμŠ€ 이름을 μ§€μ •ν•©λ‹ˆλ‹€. TanStack Query의 queryKey에 μ‚¬μš©λ©λ‹ˆλ‹€.
@api({
  httpMethod: "GET",
  resourceName: "Users",  // λ³΅μˆ˜ν˜•
  clients: ["tanstack-query"],
})
async findMany(): Promise<User[]> {
  // ...
}

// μƒμ„±λ˜λŠ” Query Hook
export function useUsers() {
  return useQuery({
    queryKey: ["Users", "findMany"],  // resourceName μ‚¬μš©
    queryFn: () => findManyUsers(),
  });
}
넀이밍 κ°€μ΄λ“œ:
API νƒ€μž…resourceNameμ˜ˆμ‹œ
단일 μ‘°νšŒλ‹¨μˆ˜ν˜•User
λͺ©λ‘ μ‘°νšŒλ³΅μˆ˜ν˜•Users
생성/μˆ˜μ •λ‹¨μˆ˜ν˜•User
μ‚­μ œλ³΅μˆ˜ν˜•Users

guards

인증 및 κΆŒν•œ 검사λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.
// 인증만 ν•„μš”
@api({ guards: ["user"] })
async getMyProfile(): Promise<User> {
  // λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλ§Œ μ ‘κ·Ό κ°€λŠ₯
}

// κ΄€λ¦¬μž κΆŒν•œ ν•„μš”
@api({ guards: ["admin"] })
async deleteUser(id: number): Promise<void> {
  // κ΄€λ¦¬μžλ§Œ μ ‘κ·Ό κ°€λŠ₯
}

// μ—¬λŸ¬ κ°€λ“œ μ‘°ν•©
@api({ guards: ["user", "admin"] })
async someAdminAction(): Promise<void> {
  // user AND admin λͺ¨λ‘ λ§Œμ‘±ν•΄μ•Ό 함
}
Guard μ’…λ₯˜:
Guardμ„€λͺ…확인 λ‚΄μš©
user둜그인 ν•„μš”context.user 쑴재 μ—¬λΆ€
adminκ΄€λ¦¬μž κΆŒν•œcontext.user.role === "admin"
queryμ»€μŠ€ν…€ κ²€μ‚¬μ‚¬μš©μž μ •μ˜ 둜직
Guard λ‘œμ§μ€ sonamu.config.ts의 guardHandlerμ—μ„œ μ •μ˜ν•©λ‹ˆλ‹€.
sonamu.config.ts
export default {
  guardHandler: (guard, request, api) => {
    if (guard === "user" && !request.user) {
      throw new UnauthorizedException("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€");
    }
    if (guard === "admin" && request.user?.role !== "admin") {
      throw new ForbiddenException("κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€");
    }
  },
};

contentType

μ‘λ‹΅μ˜ Content-Type을 μ§€μ •ν•©λ‹ˆλ‹€.
// JSON (κΈ°λ³Έκ°’)
@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 (파일 λ‹€μš΄λ‘œλ“œ)
@api({ contentType: "application/octet-stream" })
async downloadFile(): Promise<Buffer> {
  return fileBuffer;
}

timeout

API νƒ€μž„μ•„μ›ƒμ„ λ°€λ¦¬μ΄ˆ λ‹¨μœ„λ‘œ μ§€μ •ν•©λ‹ˆλ‹€.
@api({ 
  httpMethod: "GET",
  timeout: 5000,  // 5초
})
async longRunningQuery(): Promise<Result> {
  // 5초 이상 걸리면 νƒ€μž„μ•„μ›ƒ
}
νƒ€μž„μ•„μ›ƒμ€ ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ μ„€μ •μž…λ‹ˆλ‹€. μ„œλ²„μ—μ„œλŠ” 계속 싀행될 수 μžˆμœΌλ―€λ‘œ, μ„œλ²„ μΈ‘ νƒ€μž„μ•„μ›ƒλ„ λ³„λ„λ‘œ μ„€μ •ν•΄μ•Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

cacheControl

HTTP Cache-Control 헀더λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.
// 1λΆ„ 캐싱
@api({
  cacheControl: {
    maxAge: 60,  // seconds
  },
})
async getStaticData(): Promise<Data> {
  // ...
}

// 캐싱 λΉ„ν™œμ„±ν™”
@api({
  cacheControl: {
    maxAge: 0,
    noCache: true,
  },
})
async getDynamicData(): Promise<Data> {
  // ...
}
CacheControl μ˜΅μ…˜:
μ˜΅μ…˜νƒ€μž…μ„€λͺ…μ˜ˆμ‹œ
maxAgenumberμ΅œλŒ€ μΊμ‹œ μ‹œκ°„ (초)60
noCachebooleanμΊμ‹œ μ‚¬μš© μ•ˆν•¨true
noStorebooleanμ €μž₯ μ•ˆν•¨true
privatebooleanμ‚¬μš©μžλ³„ μΊμ‹œtrue
publicboolean곡용 μΊμ‹œtrue

compress

응닡 μ••μΆ• 섀정을 μ œμ–΄ν•©λ‹ˆλ‹€.
// μ••μΆ• ν™œμ„±ν™” (κΈ°λ³Έκ°’)
@api({ compress: true })
async getData(): Promise<LargeData> {
  // 응닡이 gzip으둜 압좕됨
}

// μ••μΆ• λΉ„ν™œμ„±ν™”
@api({ compress: false })
async streamData(): Promise<StreamData> {
  // μ••μΆ• 없이 전솑 (μŠ€νŠΈλ¦¬λ°μ— 적합)
}

μ—¬λŸ¬ λ°μ½”λ ˆμ΄ν„° μ‘°ν•©

@api + @transactional

νŠΈλžœμž­μ…˜ λ‚΄μ—μ„œ APIλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
@api({ httpMethod: "POST" })
@transactional()
async updateUserAndProfile(
  userId: number,
  userData: UserData,
  profileData: ProfileData
): Promise<void> {
  await this.save([userData]);
  await ProfileModel.save([profileData]);
  // μžλ™ 컀밋 λ˜λŠ” λ‘€λ°±
}

@upload (독립 μ‚¬μš©)

파일 μ—…λ‘œλ“œ APIλ₯Ό λ§Œλ“­λ‹ˆλ‹€. @uploadλŠ” @api 없이 λ…λ¦½μ μœΌλ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.
@upload()
async uploadAvatar(): Promise<{ url: string }> {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0]; // 첫 번째 파일 μ‚¬μš©

  if (!file) {
    throw new BadRequestException("파일이 μ—†μŠ΅λ‹ˆλ‹€");
  }

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

  return { url };
}
Upload μ˜΅μ…˜:
μ˜΅μ…˜μ„€λͺ…μ‚¬μš© μ˜ˆμ‹œ
limits.fileSizeμ΅œλŒ€ 파일 크기 (bytes)10 * 1024 * 1024 (10MB)
limits.filesμ΅œλŒ€ 파일 개수5
단일/닀쀑 파일 μ—¬λΆ€λŠ” νŒŒλΌλ―Έν„° νƒ€μž…(UploadedFile vs UploadedFile[])으둜 κ²°μ •λ©λ‹ˆλ‹€.

API 경둜 κ·œμΉ™

κΈ°λ³Έ κ²½λ‘œλŠ” λ‹€μŒ κ·œμΉ™μœΌλ‘œ μƒμ„±λ©λ‹ˆλ‹€:
/{modelName}/{methodName}
λ³€ν™˜ κ·œμΉ™:
  • Model 이름: PascalCase β†’ camelCase
    • UserModel β†’ user
    • BlogPostModel β†’ blogPost
  • Method 이름: κ·ΈλŒ€λ‘œ μ‚¬μš©
    • findById β†’ findById
μ˜ˆμ‹œ:
ModelMethod경둜
UserModelfindById/user/findById
BlogPostModelfindMany/blogPost/findMany
CommentModelsave/comment/save

μ‹€μ „ 예제

κΈ°λ³Έ 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;
}

인증 API

SonamuλŠ” 인증을 better-auth의 HTTP μ—”λ“œν¬μΈνŠΈλ‘œ μ²˜λ¦¬ν•©λ‹ˆλ‹€. 둜그인(/api/auth/sign-in/email), λ‘œκ·Έμ•„μ›ƒ(/api/auth/sign-out) 등은 Sonamu @api λ°μ½”λ ˆμ΄ν„°κ°€ μ•„λ‹Œ better-authκ°€ 직접 μ œκ³΅ν•˜λŠ” μ—”λ“œν¬μΈνŠΈλ₯Ό μ‚¬μš©ν•˜μ„Έμš”.
λ‘œκ·ΈμΈν•œ μ‚¬μš©μž 정보λ₯Ό λ°˜ν™˜ν•˜λŠ” me() APIλŠ” 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);
}

λ‹€μŒ 단계

Business Logic

λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 μž‘μ„± νŒ¨ν„΄ 배우기

Stream Decorator

@stream으둜 μ‹€μ‹œκ°„ 이벀트 μ „μ†‘ν•˜κΈ°

Upload Decorator

@upload둜 파일 μ—…λ‘œλ“œ μ²˜λ¦¬ν•˜κΈ°

Guards

인증과 κΆŒν•œ 검사 κ΅¬ν˜„ν•˜κΈ°