@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 | ์ค๋ช
| ์ฌ์ฉ ์์ |
|---|
axios | Axios ๊ธฐ๋ฐ ํจ์ | ์ผ๋ฐ API ํธ์ถ |
axios-multipart | ํ์ผ ์
๋ก๋์ฉ Axios | ์ด๋ฏธ์ง ์
๋ก๋ |
tanstack-query | Query hook | ๋ฐ์ดํฐ ์กฐํ |
tanstack-mutation | Mutation hook | ๋ฐ์ดํฐ ๋ณ๊ฒฝ |
window-fetch | Fetch 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"]๊ฐ ๊ธฐ๋ณธ๊ฐ์
๋๋ค.
์ปค์คํ
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 |
์ธ์ฆ ๋ฐ ๊ถํ ๊ฒ์ฌ๋ฅผ ์ค์ ํฉ๋๋ค.
// ์ธ์ฆ๋ง ํ์
@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 ์ต์
:
| ์ต์
| ํ์
| ์ค๋ช
| ์์ |
|---|
maxAge | number | ์ต๋ ์บ์ ์๊ฐ (์ด) | 60 |
noCache | boolean | ์บ์ ์ฌ์ฉ ์ํจ | true |
noStore | boolean | ์ ์ฅ ์ํจ | true |
private | boolean | ์ฌ์ฉ์๋ณ ์บ์ | true |
public | boolean | ๊ณต์ฉ ์บ์ | 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 | ๊ฒฝ๋ก |
|---|
UserModel | findById | /user/findById |
BlogPostModel | findMany | /blogPost/findMany |
CommentModel | save | /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);
}
๋ค์ ๋จ๊ณ