메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
HTTP λ©”μ„œλ“œλŠ” API의 μ˜λ„λ₯Ό λͺ…ν™•ν•˜κ²Œ ν‘œν˜„ν•˜κ³  RESTful 원칙을 λ”°λ₯΄λ„둝 ν•©λ‹ˆλ‹€.

HTTP λ©”μ„œλ“œ κ°œμš”

GET

데이터 쑰회 μ•ˆμ „ν•˜κ³  λ©±λ“±μ„±

POST

데이터 생성 λΉ„λ©±λ“±μ„±

PUT

데이터 μˆ˜μ • λ©±λ“±μ„±

DELETE

데이터 μ‚­μ œ λ©±λ“±μ„±

GET - 쑰회

데이터λ₯Ό μ‘°νšŒν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€. μ„œλ²„ μƒνƒœλ₯Ό λ³€κ²½ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

단일 λ¦¬μ†ŒμŠ€ 쑰회

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    return user;
  }
}

// 호좜: GET /api/user/get?id=1

λͺ©λ‘ 쑰회

interface UserListParams {
  page?: number;
  pageSize?: number;
  search?: string;
  role?: UserRole;
  sortBy?: "created_at" | "username";
  sortOrder?: "asc" | "desc";
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    users: User[];
    total: number;
    page: number;
    pageSize: number;
  }> {
    const rdb = this.getPuri("r");

    // κΈ°λ³Έκ°’ μ„€μ •
    const page = params.page || 1;
    const pageSize = params.pageSize || 20;
    const sortBy = params.sortBy || "created_at";
    const sortOrder = params.sortOrder || "desc";

    // 쿼리 λΉŒλ“œ
    let query = rdb.table("users");

    // 검색 쑰건
    if (params.search) {
      query = query.where((qb) => {
        qb.where("username", "like", `%${params.search}%`).orWhere(
          "email",
          "like",
          `%${params.search}%`,
        );
      });
    }

    // μ—­ν•  ν•„ν„°
    if (params.role) {
      query = query.where("role", params.role);
    }

    // μ •λ ¬
    query = query.orderBy(sortBy, sortOrder);

    // νŽ˜μ΄μ§€λ„€μ΄μ…˜
    const users = await query
      .limit(pageSize)
      .offset((page - 1) * pageSize)
      .select("*");

    // 전체 개수
    const [{ count }] = await rdb.table("users").count({ count: "*" });

    return {
      users,
      total: count,
      page,
      pageSize,
    };
  }
}

// 호좜: GET /api/user/list?page=1&pageSize=20&search=john&role=admin&sortBy=username&sortOrder=asc

관계 데이터 쑰회

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async getWithProfile(id: number): Promise<{
    user: User;
    profile: Profile | null;
    posts: Post[];
  }> {
    const rdb = this.getPuri("r");

    // User 쑰회
    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    // Profile 쑰회 (1:1)
    const profile = await rdb.table("profiles").where("user_id", id).first();

    // Posts 쑰회 (1:N)
    const posts = await rdb
      .table("posts")
      .where("user_id", id)
      .orderBy("created_at", "desc")
      .select("*");

    return {
      user,
      profile: profile || null,
      posts,
    };
  }
}
GET λ©”μ„œλ“œ νŠΉμ§•: - μ•ˆμ „ν•¨ (Safe): μ„œλ²„ μƒνƒœλ₯Ό λ³€κ²½ν•˜μ§€ μ•ŠμŒ - λ©±λ“±μ„± (Idempotent): 같은 μš”μ²­μ„ μ—¬λŸ¬ 번 해도 κ²°κ³Ό 동일 - μΊμ‹œ κ°€λŠ₯ (Cacheable): λΈŒλΌμš°μ €/ν”„λ‘μ‹œμ—μ„œ μΊμ‹œ κ°€λŠ₯ - νŒŒλΌλ―Έν„°λŠ” URL 쿼리 슀트링으둜 전달

POST - 생성

μƒˆλ‘œμš΄ λ¦¬μ†ŒμŠ€λ₯Ό 생성할 λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

λ‹¨μˆœ 생성

interface CreateUserParams {
  email: string;
  username: string;
  password: string;
  role: UserRole;
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async create(params: CreateUserParams): Promise<{
    userId: number;
  }> {
    const wdb = this.getPuri("w");

    // 쀑볡 체크
    const existing = await wdb.table("users").where("email", params.email).first();

    if (existing) {
      throw new Error("Email already exists");
    }

    // User 생성
    const [user] = await wdb
      .table("users")
      .insert({
        email: params.email,
        username: params.username,
        password: params.password, // μ‹€μ œλ‘œλŠ” ν•΄μ‹œ ν•„μš”
        role: params.role,
      })
      .returning({ id: "id" });

    return { userId: user.id };
  }
}

// 호좜: POST /api/user/create
// Body: { "email": "...", "username": "...", "password": "...", "role": "normal" }

λ³΅μž‘ν•œ 생성 (관계 데이터 포함)

interface RegisterParams {
  email: string;
  username: string;
  password: string;
  profile: {
    bio: string;
    avatarUrl?: string;
  };
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async register(params: RegisterParams): Promise<{
    userId: number;
    profileId: number;
  }> {
    const wdb = this.getPuri("w");

    // User 생성
    const userRef = wdb.ubRegister("users", {
      email: params.email,
      username: params.username,
      password: params.password,
      role: "normal",
    });

    // Profile 생성
    wdb.ubRegister("profiles", {
      user_id: userRef,
      bio: params.profile.bio,
      avatar_url: params.profile.avatarUrl || null,
    });

    // μ €μž₯
    const [userId] = await wdb.ubUpsert("users");
    const [profileId] = await wdb.ubUpsert("profiles");

    return { userId, profileId };
  }
}

배치 생성

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async createBatch(users: CreateUserParams[]): Promise<{
    userIds: number[];
    count: number;
  }> {
    const wdb = this.getPuri("w");

    // λͺ¨λ“  User 등둝
    users.forEach((user) => {
      wdb.ubRegister("users", {
        email: user.email,
        username: user.username,
        password: user.password,
        role: user.role,
      });
    });

    // 일괄 μ €μž₯
    const userIds = await wdb.ubUpsert("users");

    return {
      userIds,
      count: userIds.length,
    };
  }
}

// 호좜: POST /api/user/createBatch
// Body: [
//   { "email": "user1@test.com", "username": "user1", ... },
//   { "email": "user2@test.com", "username": "user2", ... }
// ]
POST λ©”μ„œλ“œ νŠΉμ§•: - λΉ„μ•ˆμ „ν•¨: μ„œλ²„ μƒνƒœλ₯Ό 변경함 - λΉ„λ©±λ“±μ„±: 같은 μš”μ²­μ„ μ—¬λŸ¬ 번 ν•˜λ©΄ λ¦¬μ†ŒμŠ€κ°€ 쀑볡 생성될 수 있음 - μΊμ‹œ λΆˆκ°€λŠ₯ - νŒŒλΌλ―Έν„°λŠ” Body둜 전달 - 일반적으둜 μƒμ„±λœ λ¦¬μ†ŒμŠ€μ˜ IDλ₯Ό λ°˜ν™˜

PUT - μˆ˜μ •

κΈ°μ‘΄ λ¦¬μ†ŒμŠ€λ₯Ό μˆ˜μ •ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

전체 μˆ˜μ •

interface UpdateUserParams {
  email: string;
  username: string;
  role: UserRole;
  bio: string | null;
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "PUT" })
  @transactional()
  async update(id: number, params: UpdateUserParams): Promise<void> {
    const wdb = this.getPuri("w");

    // User 쑴재 확인
    const user = await wdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    // 전체 ν•„λ“œ μ—…λ°μ΄νŠΈ
    await wdb.table("users").where("id", id).update({
      email: params.email,
      username: params.username,
      role: params.role,
      bio: params.bio,
    });
  }
}

// 호좜: PUT /api/user/update?id=1
// Body: { "email": "...", "username": "...", "role": "admin", "bio": "..." }

λΆ€λΆ„ μˆ˜μ • (PATCH μŠ€νƒ€μΌ)

class UserModel extends BaseModelClass {
  @api({ httpMethod: "PUT" })
  @transactional()
  async updatePartial(id: number, params: Partial<UpdateUserParams>): Promise<void> {
    const wdb = this.getPuri("w");

    // User 쑴재 확인
    const user = await wdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    // μ „λ‹¬λœ ν•„λ“œλ§Œ μ—…λ°μ΄νŠΈ
    const updateData: any = {};

    if (params.email !== undefined) updateData.email = params.email;
    if (params.username !== undefined) updateData.username = params.username;
    if (params.role !== undefined) updateData.role = params.role;
    if (params.bio !== undefined) updateData.bio = params.bio;

    if (Object.keys(updateData).length > 0) {
      await wdb.table("users").where("id", id).update(updateData);
    }
  }
}

// 호좜: PUT /api/user/updatePartial?id=1
// Body: { "bio": "Updated bio" }  // λ‹€λ₯Έ ν•„λ“œλŠ” λ³€κ²½ μ•ˆ 됨

μƒνƒœ λ³€κ²½

class OrderModel extends BaseModelClass {
  @api({ httpMethod: "PUT" })
  @transactional()
  async updateStatus(
    orderId: number,
    status: "pending" | "paid" | "shipped" | "delivered" | "cancelled",
  ): Promise<void> {
    const wdb = this.getPuri("w");

    const order = await wdb.table("orders").where("id", orderId).first();

    if (!order) {
      throw new Error("Order not found");
    }

    // μƒνƒœ μ „ν™˜ 검증
    if (!this.isValidStatusTransition(order.status, status)) {
      throw new Error(`Cannot change status from ${order.status} to ${status}`);
    }

    await wdb.table("orders").where("id", orderId).update({ status });
  }

  private isValidStatusTransition(from: string, to: string): boolean {
    const transitions: Record<string, string[]> = {
      pending: ["paid", "cancelled"],
      paid: ["shipped", "cancelled"],
      shipped: ["delivered"],
      delivered: [],
      cancelled: [],
    };

    return transitions[from]?.includes(to) || false;
  }
}
PUT λ©”μ„œλ“œ νŠΉμ§•: - λΉ„μ•ˆμ „ν•¨: μ„œλ²„ μƒνƒœλ₯Ό 변경함 - λ©±λ“±μ„±: 같은 μš”μ²­μ„ μ—¬λŸ¬ 번 해도 κ²°κ³Ό 동일 - 일반적으둜 λ¦¬μ†ŒμŠ€ 전체λ₯Ό ꡐ체 - νŒŒλΌλ―Έν„°λŠ” Body둜 전달 - 성곡 μ‹œ 빈 응닡 λ˜λŠ” μ—…λ°μ΄νŠΈλœ λ¦¬μ†ŒμŠ€ λ°˜ν™˜

DELETE - μ‚­μ œ

λ¦¬μ†ŒμŠ€λ₯Ό μ‚­μ œν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

단일 μ‚­μ œ

class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  @transactional()
  async remove(id: number): Promise<void> {
    const wdb = this.getPuri("w");

    // User 쑴재 확인
    const user = await wdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    // μ‚­μ œ
    await wdb.table("users").where("id", id).delete();
  }
}

// 호좜: DELETE /api/user/remove?id=1

μ†Œν”„νŠΈ μ‚­μ œ

class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  @transactional()
  async softDelete(id: number): Promise<void> {
    const wdb = this.getPuri("w");

    const user = await wdb.table("users").where("id", id).whereNull("deleted_at").first();

    if (!user) {
      throw new Error("User not found");
    }

    // deleted_at μ„€μ • (μ‹€μ œ μ‚­μ œ μ•ˆ 함)
    await wdb.table("users").where("id", id).update({
      deleted_at: new Date(),
    });
  }

  // μ†Œν”„νŠΈ μ‚­μ œ 볡ꡬ
  @api({ httpMethod: "PUT" })
  @transactional()
  async restore(id: number): Promise<void> {
    const wdb = this.getPuri("w");

    const user = await wdb.table("users").where("id", id).whereNotNull("deleted_at").first();

    if (!user) {
      throw new Error("User not found or not deleted");
    }

    await wdb.table("users").where("id", id).update({
      deleted_at: null,
    });
  }
}

κ΄€λ ¨ 데이터와 ν•¨κ»˜ μ‚­μ œ

class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  @transactional()
  async removeWithRelations(id: number): Promise<void> {
    const wdb = this.getPuri("w");

    const user = await wdb.table("users").where("id", id).first();

    if (!user) {
      throw new Error("User not found");
    }

    // 1. Profile μ‚­μ œ
    await wdb.table("profiles").where("user_id", id).delete();

    // 2. Posts μ‚­μ œ
    await wdb.table("posts").where("user_id", id).delete();

    // 3. User μ‚­μ œ
    await wdb.table("users").where("id", id).delete();
  }
}

배치 μ‚­μ œ

class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  @transactional()
  async removeBatch(ids: number[]): Promise<{
    deletedCount: number;
  }> {
    const wdb = this.getPuri("w");

    const deletedCount = await wdb.table("users").whereIn("id", ids).delete();

    return { deletedCount };
  }
}

// 호좜: DELETE /api/user/removeBatch
// Body: { "ids": [1, 2, 3] }
DELETE λ©”μ„œλ“œ νŠΉμ§•: - λΉ„μ•ˆμ „ν•¨: μ„œλ²„ μƒνƒœλ₯Ό 변경함 - λ©±λ“±μ„±: 이미 μ‚­μ œλœ λ¦¬μ†ŒμŠ€λ₯Ό λ‹€μ‹œ μ‚­μ œν•΄λ„ κ²°κ³Ό 동일 - 성곡 μ‹œ 일반적으둜 빈 응닡 (204 No Content) - μ†Œν”„νŠΈ μ‚­μ œ κ³ λ € (deleted_at 컬럼 μ‚¬μš©)

λ©”μ„œλ“œ 선택 κ°€μ΄λ“œ

선택 κΈ°μ€€

μž‘μ—…λ©”μ„œλ“œμ˜ˆμ‹œ
단일 쑰회GET/api/user/get?id=1
λͺ©λ‘ 쑰회GET/api/user/list?page=1
μƒˆλ‘œ 생성POST/api/user/create
전체 μˆ˜μ •PUT/api/user/update?id=1
λΆ€λΆ„ μˆ˜μ •PUT/api/user/updatePartial?id=1
μ‚­μ œDELETE/api/user/remove?id=1

RESTful 원칙

쒋은 예

class UserModel extends BaseModelClass {
  // βœ… GET: 쑰회
  @api({ httpMethod: "GET" })
  async list(): Promise<User[]> {
    /* ... */
  }

  // βœ… POST: 생성
  @api({ httpMethod: "POST" })
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    /* ... */
  }

  // βœ… PUT: μˆ˜μ •
  @api({ httpMethod: "PUT" })
  async update(id: number, params: UpdateUserParams): Promise<void> {
    /* ... */
  }

  // βœ… DELETE: μ‚­μ œ
  @api({ httpMethod: "DELETE" })
  async remove(id: number): Promise<void> {
    /* ... */
  }
}

λ‚˜μœ 예

class UserModel extends BaseModelClass {
  // ❌ GET으둜 데이터 λ³€κ²½
  @api({ httpMethod: "GET" })
  async deleteUser(id: number): Promise<void> {
    // GET은 μ•ˆμ „ν•΄μ•Ό 함 (μƒνƒœ λ³€κ²½ X)
  }

  // ❌ POST둜 쑰회
  @api({ httpMethod: "POST" })
  async getUser(id: number): Promise<User> {
    // POSTλŠ” μƒμ„±μš©
  }

  // ❌ DELETE둜 μˆ˜μ •
  @api({ httpMethod: "DELETE" })
  async updateUser(id: number, params: any): Promise<void> {
    // DELETEλŠ” μ‚­μ œμš©
  }
}

λ‹€μŒ 단계

νŒŒλΌλ―Έν„°

νƒ€μž… μ •μ˜ 및 검증

λ°˜ν™˜ νƒ€μž…

응닡 νƒ€μž… μ •μ˜ν•˜κΈ°

@api λ°μ½”λ ˆμ΄ν„°

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

μ—λŸ¬ 처리

API μ—λŸ¬ 핸듀링