@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, findManyPOST데이터 생성/수정 save, loginPUT데이터 업데이트 updateDELETE데이터 삭제 del, removePATCH부분 업데이트 updateProfile
GET - 조회
POST - 생성/수정
DELETE - 삭제
@ 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 - Query
변경 API - Mutation
파일 업로드
@ api ({
httpMethod: "GET" ,
clients: [ "axios" , "tanstack-query" ],
})
async findById ( id : number ): Promise < User > {
// ...
}
생성되는 클라이언트 코드 :
axios
tanstack-query
tanstack-mutation
// 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에서 정의합니다. 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 최대 캐시 시간 (초) 60noCacheboolean 캐시 사용 안함 truenoStoreboolean 저장 안함 trueprivateboolean 사용자별 캐시 truepublicboolean 공용 캐시 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
UserModel → user
BlogPostModel → blogPost
Method 이름: 그대로 사용
예시 :
Model Method 경로 UserModelfindById/user/findByIdBlogPostModelfindMany/blogPost/findManyCommentModelsave/comment/save
실전 예제
기본 CRUD API
조회 API
목록 API
생성/수정 API
삭제 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);
}
다음 단계