메인 콘텐츠로 건너뛰기
@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: UserModeluser
  • Method: findByIdfindById
  • 결과: /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.tsguardHandler에서 정의합니다.
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]);
  // 자동 커밋 또는 롤백
}

@api + @upload

파일 업로드 API를 만듭니다.
@api({
  httpMethod: "POST",
  clients: ["axios-multipart"],
})
@upload({ mode: "single" })
async uploadAvatar(): Promise<{ url: string }> {
  const { file } = Sonamu.getUploadContext();
  
  if (!file) {
    throw new BadRequestException("파일이 없습니다");
  }
  
  // 파일 처리...
  const url = await saveToS3(file);
  
  return { url };
}
Upload 옵션:
옵션설명사용 예시
mode: "single"단일 파일프로필 이미지
mode: "multiple"여러 파일갤러리 업로드

API 경로 규칙

기본 경로는 다음 규칙으로 생성됩니다:
/{modelName}/{methodName}
변환 규칙:
  • Model 이름: PascalCase → camelCase
    • UserModeluser
    • BlogPostModelblogPost
  • Method 이름: 그대로 사용
    • findByIdfindById
예시:
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

@api({
  httpMethod: "POST",
  clients: ["axios", "tanstack-mutation"],
})
async login(params: LoginParams): Promise<{ user: User; token: string }> {
  const { email, password } = params;
  
  // 사용자 조회
  const user = await this.getPuri("r")
    .where("email", email)
    .first();
    
  if (!user) {
    throw new UnauthorizedException("이메일 또는 비밀번호가 일치하지 않습니다");
  }
  
  // 비밀번호 확인
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    throw new UnauthorizedException("이메일 또는 비밀번호가 일치하지 않습니다");
  }
  
  // 세션 생성
  const context = Sonamu.getContext();
  await context.passport.login(user);
  
  // 토큰 생성 (선택)
  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);
}

다음 단계