@api ๋ฐ์ฝ๋ ์ดํฐ๋ Model ๋๋ Frame ํด๋์ค์ ๋ฉ์๋๋ฅผ HTTP API ์๋ํฌ์ธํธ๋ก ๋
ธ์ถํฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
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 ();
httpMethod
HTTP ๋ฉ์๋๋ฅผ ์ง์ ํฉ๋๋ค.
type HTTPMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" ;
๊ธฐ๋ณธ๊ฐ: "GET"
@ api ({ httpMethod: "POST" })
async create ( data : CreateParams ) {
// POST ์์ฒญ์ผ๋ก ๋
ธ์ถ๋ฉ๋๋ค
}
@ api ({ httpMethod: "DELETE" })
async remove ( id : number ) {
// DELETE ์์ฒญ์ผ๋ก ๋
ธ์ถ๋ฉ๋๋ค
}
API ์๋ํฌ์ธํธ ๊ฒฝ๋ก๋ฅผ ์ง์ ํฉ๋๋ค.
๊ธฐ๋ณธ๊ฐ: /{modelName}/{methodName} (camelCase)
@ api ({ path: "/api/v1/users/profile" })
async getProfile () {
// /api/v1/users/profile๋ก ์ ๊ทผ
}
@ api ()
async findById ( id : number ) {
// ๊ธฐ๋ณธ ๊ฒฝ๋ก: /user/findById
}
๊ฒฝ๋ก๋ ์๋์ผ๋ก camelCase๋ก ๋ณํ๋ฉ๋๋ค. UserModel.findById โ /user/findById
contentType
์๋ต์ Content-Type์ ์ง์ ํฉ๋๋ค.
๊ธฐ๋ณธ๊ฐ: "application/json"
type ContentType =
| "text/plain"
| "text/html"
| "text/xml"
| "application/json"
| "application/octet-stream" ;
@ api ({ contentType: "text/plain" })
async exportCsv () {
return "id,name,email \n 1,Alice,[email protected] " ;
}
@ api ({ contentType: "text/html" })
async renderTemplate () {
return "<html><body>Hello</body></html>" ;
}
clients
์์ฑํ ํด๋ผ์ด์ธํธ ํ์
์ ์ง์ ํฉ๋๋ค.
๊ธฐ๋ณธ๊ฐ: ["axios"]
type ServiceClient =
| "axios" // Axios ํด๋ผ์ด์ธํธ
| "axios-multipart" // Multipart form-data
| "tanstack-query" // TanStack Query (์ฝ๊ธฐ)
| "tanstack-mutation" // TanStack Mutation (์ฐ๊ธฐ)
| "tanstack-mutation-multipart" // TanStack Mutation (ํ์ผ ์
๋ก๋)
| "window-fetch" ; // Native Fetch API
@ api ({
httpMethod: "GET" ,
clients: [ "axios" , "tanstack-query" ]
})
async list () {
// axios์ TanStack Query ํด๋ผ์ด์ธํธ ์์ฑ
}
@ api ({
httpMethod: "POST" ,
clients: [ "axios" , "tanstack-mutation" ]
})
async create ( data : CreateParams ) {
// axios์ TanStack Mutation ํด๋ผ์ด์ธํธ ์์ฑ
}
API ์ ๊ทผ ๊ถํ์ ์ง์ ํฉ๋๋ค.
type GuardKey = "query" | "admin" | "user" ;
@ api ({ guards: [ "admin" ] })
async deleteUser ( id : number ) {
// ๊ด๋ฆฌ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
}
@ api ({ guards: [ "user" ] })
async getProfile () {
// ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
}
@ api ({ guards: [ "query" , "admin" ] })
async search ( query : string ) {
// query ๋๋ admin ๊ถํ ํ์
}
description
API ์ค๋ช
์ ์ถ๊ฐํฉ๋๋ค. ์์ฑ๋ ํ์
๊ณผ ๋ฌธ์์ ํฌํจ๋ฉ๋๋ค.
@ api ({
description: "์ฌ์ฉ์ ID๋ก ํ๋กํ์ ์กฐํํฉ๋๋ค."
})
async findById ( id : number ) {
// ...
}
resourceName
์์ฑ๋๋ ์๋น์ค ํ์ผ์ ๋ฆฌ์์ค ์ด๋ฆ์ ์ง์ ํฉ๋๋ค.
@ api ({ resourceName: "Users" })
async list () {
// UsersService.ts ํ์ผ์ ํฌํจ๋ฉ๋๋ค
}
timeout
API ์์ฒญ์ ํ์์์์ ๋ฐ๋ฆฌ์ด ๋จ์๋ก ์ง์ ํฉ๋๋ค.
@ api ({ timeout: 30000 }) // 30์ด
async heavyOperation () {
// ์ค๋ ๊ฑธ๋ฆฌ๋ ์์
}
cacheControl
์๋ต์ Cache-Control ํค๋๋ฅผ ์ค์ ํฉ๋๋ค.
@ api ({
cacheControl: {
maxAge: "10m" , // 10๋ถ ์บ์ฑ
sMaxAge: "1h" , // CDN์์ 1์๊ฐ ์บ์ฑ
public: true
}
})
async getPublicData () {
// Cache-Control: public, max-age=600, s-maxage=3600
}
CacheControlConfig ํ์
:
type CacheControlConfig = {
maxAge ?: string ; // ๋ธ๋ผ์ฐ์ ์บ์ ์๊ฐ
sMaxAge ?: string ; // CDN/ํ๋ก์ ์บ์ ์๊ฐ
public ?: boolean ; // public/private
noCache ?: boolean ; // no-cache
noStore ?: boolean ; // no-store
mustRevalidate ?: boolean ;
};
์๊ฐ ํ๊ธฐ: "10s", "5m", "1h", "1d" ํ์ ์ง์
compress
์๋ต ์์ถ ์ค์ ์ ์ง์ ํฉ๋๋ค.
@ api ({
compress: {
threshold: 1024 , // 1KB ์ด์๋ง ์์ถ
level: 6 // ์์ถ ๋ ๋ฒจ (0-9)
}
})
async getLargeData () {
// ํฐ ๋ฐ์ดํฐ ๋ฐํ
}
@ api ({ compress: false }) // ์์ถ ๋นํ์ฑํ
async getSmallData () {
// ์์ ๋ฐ์ดํฐ๋ ์์ถํ์ง ์์
}
์ ์ฒด ์ต์
์์
@ api ({
httpMethod: "POST" ,
path: "/api/v1/users/search" ,
contentType: "application/json" ,
clients: [ "axios" , "tanstack-query" ],
guards: [ "user" ],
description: "์ฌ์ฉ์ ๊ฒ์ API" ,
resourceName: "Users" ,
timeout: 5000 ,
cacheControl: {
maxAge: "5m" ,
public: true
},
compress: {
threshold: 1024 ,
level: 6
}
})
async search ( params : SearchParams ) {
// ๊ตฌํ
}
๊ฒฝ๋ก ์์ฑ ๊ท์น
Model ํด๋์ค
class UserModelClass extends BaseModelClass {
@ api ()
async findById ( id : number ) {}
// ๊ฒฝ๋ก: /user/findById
}
class PostModelClass extends BaseModelClass {
@ api ()
async getComments () {}
// ๊ฒฝ๋ก: /post/getComments
}
๊ท์น:
ํด๋์ค ์ด๋ฆ์์ โModelClassโ ์ ๊ฑฐ
๋๋จธ์ง๋ฅผ camelCase๋ก ๋ณํ
/{modelName}/{methodName} ํ์
Frame ํด๋์ค
class AuthFrameClass extends BaseFrameClass {
@ api ()
async login () {}
// ๊ฒฝ๋ก: /auth/login
}
๊ท์น:
ํด๋์ค ์ด๋ฆ์์ โFrameClassโ ์ ๊ฑฐ
๋๋จธ์ง๋ฅผ camelCase๋ก ๋ณํ
/{frameName}/{methodName} ํ์
๋ค๋ฅธ ๋ฐ์ฝ๋ ์ดํฐ์ ํจ๊ป ์ฌ์ฉ
@transactional
@ api ({ httpMethod: "POST" })
@ transactional ()
async save ( data : UserSaveParams ) {
const wdb = this . getDB ( "w" );
// ํธ๋์ญ์
๋ด์์ ์คํ
return this . upsert ( wdb , data );
}
@api๋ฅผ ๋จผ์ , @transactional์ ๋์ค์ ์์ฑํ์ธ์.
@ api ({ httpMethod: "GET" })
@ cache ({ ttl: "10m" })
async findById ( id : number ) {
// 10๋ถ๊ฐ ๊ฒฐ๊ณผ ์บ์ฑ
const rdb = this . getPuri ( "r" );
return rdb . table ( "users" ). where ( "id" , id ). first ();
}
@upload
@ api ({ httpMethod: "POST" })
@ upload ({ mode: "single" })
async uploadAvatar () {
const { file } = Sonamu . getUploadContext ();
// ํ์ผ ์ฒ๋ฆฌ
}
์ ์ฝ์ฌํญ
1. @stream๊ณผ ์ค๋ณต ์ฌ์ฉ ๋ถ๊ฐ
// โ ์๋ฌ ๋ฐ์
@ api ()
@ stream ({ type: "sse" , events: EventSchema })
async subscribe () {}
์๋ฌ ๋ฉ์์ง:
@api decorator can only be used once on UserModel.subscribe.
You can use only one of @api or @stream decorator on the same method.
2. ๊ฐ์ ๋ฉ์๋์ ์ฌ๋ฌ ๋ฒ ์ฌ์ฉ ์
์ฌ๋ฌ ๋ฒ ์ฌ์ฉํ๋ฉด ๋ง์ง๋ง ๊ฒ์ด ์ฐ์ ๋๋ฉฐ, ์ถฉ๋ํ๋ ์ต์
์ด ์์ผ๋ฉด ์๋ฌ ๋ฐ์:
// โ ์๋ฌ - path ์ถฉ๋
@ api ({ path: "/users/list" })
@ api ({ path: "/users/all" })
async list () {}
์๋ฌ ๋ฉ์์ง:
@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. Model/Frame ํด๋์ค์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ
// โ ์ผ๋ฐ ํด๋์ค์์๋ ์ฌ์ฉ ๋ถ๊ฐ
class UtilClass {
@ api ()
async helper () {}
// ์๋ฌ: modelName is required
}
// โ
BaseModelClass ์์ ํ์
class UserModelClass extends BaseModelClass {
@ api ()
async findById ( id : number ) {}
}
์์ฑ๋๋ ์ฝ๋
@api ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์์ด ์๋ ์์ฑ๋ฉ๋๋ค:
1. API ๋ผ์ฐํธ ๋ฑ๋ก
// Fastify ๋ผ์ฐํธ ์๋ ๋ฑ๋ก
fastify . get ( "/user/findById" , async ( request , reply ) => {
// ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ ๋ฐ ์ฒ๋ฆฌ
const result = await UserModel . findById ( subset , id );
return result ;
});
2. ํ์
์ ์ ์์ฑ
// api/src/application/services/UserService.types.ts
export type UserFindByIdParams = {
subset : UserSubsetKey ;
id : number ;
};
export type UserFindByIdResult = UserSubsetMapping [ UserSubsetKey ];
3. ํด๋ผ์ด์ธํธ ์ฝ๋ ์์ฑ
Axios:
// web/src/services/UserService.ts
export const UserService = {
async findById ( params : UserFindByIdParams ) {
return axios . get < UserFindByIdResult >( "/user/findById" , { params });
}
};
TanStack Query:
export const useUserFindById = ( params : UserFindByIdParams ) => {
return useQuery ({
queryKey: [ "user" , "findById" , params ],
queryFn : () => UserService . findById ( params )
});
};
@api ๋ฐ์ฝ๋ ์ดํฐ๋ ์๋์ผ๋ก ๋ก๊ทธ๋ฅผ ๋จ๊น๋๋ค:
@ api ({ httpMethod: "GET" })
async findById ( id : number ) {
// ์๋ ๋ก๊ทธ:
// [DEBUG] api: GET UserModel.findById
}
๋ก๊ทธ๋ LogTape๋ฅผ ํตํด ๊ธฐ๋ก๋๋ฉฐ, ์นดํ
๊ณ ๋ฆฌ๋ [model:user] ๋๋ [frame:auth] ํ์์
๋๋ค.
์์ ๋ชจ์
๊ธฐ๋ณธ CRUD
์บ์ฑ
๊ถํ ์ ์ด
์ปค์คํ
๊ฒฝ๋ก
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 () });
}
}
class ProductModelClass extends BaseModelClass {
@ api ({ httpMethod: "GET" })
@ cache ({ ttl: "1h" , tags: [ "products" ] })
async featured () {
// 1์๊ฐ ๋์ ์บ์ฑ
const rdb = this . getPuri ( "r" );
return rdb . table ( "products" )
. where ( "featured" , true )
. limit ( 10 );
}
@ api ({ httpMethod: "GET" })
@ cache ({ ttl: "5m" })
async findById ( id : number ) {
// 5๋ถ ๋์ ์บ์ฑ
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 });
// ์บ์ ๋ฌดํจํ
await Sonamu . cache . deleteByTags ([ "products" ]);
return result ;
}
}
class AdminFrameClass extends BaseFrameClass {
@ api ({
httpMethod: "GET" ,
guards: [ "admin" ]
})
async dashboard () {
// ๊ด๋ฆฌ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
return {
users: await UserModel . count (),
orders: await OrderModel . count ()
};
}
@ api ({
httpMethod: "POST" ,
guards: [ "admin" ]
})
@ transactional ()
async banUser ( userId : number ) {
// ๊ด๋ฆฌ์๋ง ์คํ ๊ฐ๋ฅ
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 () {
// ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
const { user } = Sonamu . getContext ();
return UserModel . findById ( user . id );
}
}
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 ) {
// Stripe ์นํ
์ฒ๋ฆฌ
}
}
๋ค์ ๋จ๊ณ