๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
API ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •์˜ํ•˜๋ฉด ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ ๋ช…ํ™•ํ•œ API ๋ฌธ์„œ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐœ์š”

ํƒ€์ž… ์•ˆ์ „์„ฑ

์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ์ž๋™์™„์„ฑ ์ง€์›

๋ช…ํ™•ํ•œ ๊ณ„์•ฝ

API ๋ช…์„ธ ์ž๋™ ์ƒ์„ฑํด๋ผ์ด์–ธํŠธ ๊ฐ€์ด๋“œ

๊ฒ€์ฆ

์ž๋™ ํƒ€์ž… ๊ฒ€์ฆ์—๋Ÿฌ ์กฐ๊ธฐ ๋ฐœ๊ฒฌ

๋ฌธ์„œํ™”

์ฃผ์„์œผ๋กœ ์„ค๋ช… ์ถ”๊ฐ€์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด

๊ธฐ๋ณธ ํƒ€์ž…

์›์‹œ ํƒ€์ž…

class UserModel extends BaseModelClass {
  // ์ˆซ์ž
  @api({ httpMethod: "GET" })
  async getUser(id: number): Promise<User> {
    // GET /api/user/getUser?id=1
    // ...
  }
  
  // ๋ฌธ์ž์—ด
  @api({ httpMethod: "GET" })
  async findByEmail(email: string): Promise<User | null> {
    // GET /api/user/[email protected]
    // ...
  }
  
  // ๋ถˆ๋ฆฌ์–ธ
  @api({ httpMethod: "GET" })
  async listActive(active: boolean): Promise<User[]> {
    // GET /api/user/listActive?active=true
    // ...
  }
  
  // ๋‚ ์งœ
  @api({ httpMethod: "GET" })
  async listByDate(fromDate: Date, toDate: Date): Promise<User[]> {
    // GET /api/user/listByDate?fromDate=2025-01-01&toDate=2025-12-31
    // ...
  }
}

๋ฐฐ์—ด ํƒ€์ž…

class UserModel extends BaseModelClass {
  // ์ˆซ์ž ๋ฐฐ์—ด
  @api({ httpMethod: "GET" })
  async getMultiple(ids: number[]): Promise<User[]> {
    // GET /api/user/getMultiple?ids=1,2,3
    const rdb = this.getPuri("r");
    return rdb.table("users").whereIn("id", ids).select("*");
  }
  
  // ๋ฌธ์ž์—ด ๋ฐฐ์—ด
  @api({ httpMethod: "GET" })
  async findByRoles(roles: UserRole[]): Promise<User[]> {
    // GET /api/user/findByRoles?roles=admin,manager
    const rdb = this.getPuri("r");
    return rdb.table("users").whereIn("role", roles).select("*");
  }
}

์ธํ„ฐํŽ˜์ด์Šค ์ •์˜

๋‹จ์ˆœ ์ธํ„ฐํŽ˜์ด์Šค

interface CreateUserParams {
  email: string;
  username: string;
  password: string;
  role: "admin" | "manager" | "normal";
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");
    
    // params.email, params.username ๋“ฑ ์ž๋™์™„์„ฑ ๊ฐ€๋Šฅ
    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": "[email protected]",
//   "username": "testuser",
//   "password": "hashedpass",
//   "role": "normal"
// }

์˜ต์…”๋„ ํ•„๋“œ

interface UserListParams {
  // ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ
  page: number;
  pageSize: number;
  
  // ์˜ต์…”๋„ ํŒŒ๋ผ๋ฏธํ„ฐ
  search?: string;
  role?: UserRole;
  isActive?: boolean;
  sortBy?: "created_at" | "username" | "email";
  sortOrder?: "asc" | "desc";
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    users: User[];
    total: number;
  }> {
    const rdb = this.getPuri("r");
    
    let query = rdb.table("users");
    
    // ์˜ต์…”๋„ ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒ˜๋ฆฌ
    if (params.search) {
      query = query.where("username", "like", `%${params.search}%`);
    }
    
    if (params.role) {
      query = query.where("role", params.role);
    }
    
    if (params.isActive !== undefined) {
      query = query.where("is_active", params.isActive);
    }
    
    // ์ •๋ ฌ (๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต)
    const sortBy = params.sortBy || "created_at";
    const sortOrder = params.sortOrder || "desc";
    query = query.orderBy(sortBy, sortOrder);
    
    // ํŽ˜์ด์ง€๋„ค์ด์…˜
    const users = await query
      .limit(params.pageSize)
      .offset((params.page - 1) * params.pageSize)
      .select("*");
    
    const [{ count }] = await rdb.table("users").count({ count: "*" });
    
    return { users, total: count };
  }
}

์ค‘์ฒฉ ๊ฐ์ฒด

interface RegisterParams {
  // User ์ •๋ณด
  email: string;
  username: string;
  password: string;
  
  // Profile ์ •๋ณด (์ค‘์ฒฉ)
  profile: {
    bio: string;
    avatarUrl?: string;
    socialLinks?: {
      twitter?: string;
      github?: string;
      linkedin?: string;
    };
  };
  
  // ์„ค์ • (์ค‘์ฒฉ)
  preferences: {
    theme: "light" | "dark";
    language: "ko" | "en";
    notifications: {
      email: boolean;
      push: boolean;
    };
  };
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async register(params: RegisterParams): Promise<{ userId: 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,
      social_links: JSON.stringify(params.profile.socialLinks || {}),
    });
    
    // Preferences ์ƒ์„ฑ
    wdb.ubRegister("user_preferences", {
      user_id: userRef,
      theme: params.preferences.theme,
      language: params.preferences.language,
      email_notifications: params.preferences.notifications.email,
      push_notifications: params.preferences.notifications.push,
    });
    
    const [userId] = await wdb.ubUpsert("users");
    await wdb.ubUpsert("profiles");
    await wdb.ubUpsert("user_preferences");
    
    return { userId };
  }
}

์œ ๋‹ˆ์˜จ ํƒ€์ž…๊ณผ Enum

Enum ์‚ฌ์šฉ

// Enum ์ •์˜
enum UserRole {
  Admin = "admin",
  Manager = "manager",
  Normal = "normal",
}

enum UserStatus {
  Active = "active",
  Inactive = "inactive",
  Suspended = "suspended",
}

interface UpdateUserParams {
  role?: UserRole;
  status?: UserStatus;
  bio?: string;
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "PUT" })
  @transactional()
  async update(
    id: number,
    params: UpdateUserParams
  ): Promise<void> {
    const wdb = this.getPuri("w");
    
    const updateData: any = {};
    
    if (params.role) {
      // Enum ๊ฐ’ ๊ฒ€์ฆ ์ž๋™
      updateData.role = params.role;
    }
    
    if (params.status) {
      updateData.status = params.status;
    }
    
    if (params.bio !== undefined) {
      updateData.bio = params.bio;
    }
    
    await wdb.table("users").where("id", id).update(updateData);
  }
}

๋ฆฌํ„ฐ๋Ÿด ์œ ๋‹ˆ์˜จ ํƒ€์ž…

interface SearchParams {
  query: string;
  // ๋ฆฌํ„ฐ๋Ÿด ์œ ๋‹ˆ์˜จ ํƒ€์ž…
  searchIn: "username" | "email" | "bio" | "all";
  sortBy: "relevance" | "created_at" | "username";
  order: "asc" | "desc";
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async search(params: SearchParams): Promise<User[]> {
    const rdb = this.getPuri("r");
    
    let query = rdb.table("users");
    
    // searchIn์— ๋”ฐ๋ฅธ ๊ฒ€์ƒ‰ ํ•„๋“œ ๊ฒฐ์ •
    switch (params.searchIn) {
      case "username":
        query = query.where("username", "like", `%${params.query}%`);
        break;
      case "email":
        query = query.where("email", "like", `%${params.query}%`);
        break;
      case "bio":
        query = query.where("bio", "like", `%${params.query}%`);
        break;
      case "all":
        query = query.where((qb) => {
          qb.where("username", "like", `%${params.query}%`)
            .orWhere("email", "like", `%${params.query}%`)
            .orWhere("bio", "like", `%${params.query}%`);
        });
        break;
    }
    
    return query.orderBy(params.sortBy, params.order).select("*");
  }
}

์ œ๋„ค๋ฆญ ํƒ€์ž…

๊ณตํ†ต ๋ฆฌ์ŠคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ

// ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ œ๋„ค๋ฆญ ์ธํ„ฐํŽ˜์ด์Šค
interface ListParams<T> {
  page: number;
  pageSize: number;
  sortBy?: keyof T;
  sortOrder?: "asc" | "desc";
}

// User์šฉ ํ™•์žฅ
interface UserListParams extends ListParams<User> {
  search?: string;
  role?: UserRole;
}

// Product์šฉ ํ™•์žฅ
interface ProductListParams extends ListParams<Product> {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<User[]> {
    // sortBy๋Š” User์˜ ํ‚ค๋งŒ ๊ฐ€๋Šฅ (ํƒ€์ž… ์•ˆ์ „)
    // ...
  }
}

๊ฒ€์ฆ ๋กœ์ง

์ปค์Šคํ…€ ๊ฒ€์ฆ

interface CreateUserParams {
  email: string;
  username: string;
  password: string;
  age?: number;
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    // ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ
    this.validateCreateParams(params);
    
    const wdb = this.getPuri("w");
    
    // ...
  }
  
  private validateCreateParams(params: CreateUserParams): void {
    // ์ด๋ฉ”์ผ ํ˜•์‹ ๊ฒ€์ฆ
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(params.email)) {
      throw new Error("Invalid email format");
    }
    
    // ์‚ฌ์šฉ์ž๋ช… ๊ธธ์ด ๊ฒ€์ฆ
    if (params.username.length < 3 || params.username.length > 20) {
      throw new Error("Username must be between 3 and 20 characters");
    }
    
    // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ๊ฒ€์ฆ
    if (params.password.length < 8) {
      throw new Error("Password must be at least 8 characters");
    }
    
    // ๋‚˜์ด ๊ฒ€์ฆ
    if (params.age !== undefined && (params.age < 18 || params.age > 120)) {
      throw new Error("Age must be between 18 and 120");
    }
  }
}

Zod๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒ€์ฆ

import { z } from "zod";

// Zod ์Šคํ‚ค๋งˆ ์ •์˜
const CreateUserSchema = z.object({
  email: z.string().email("Invalid email format"),
  username: z.string().min(3).max(20),
  password: z.string().min(8),
  age: z.number().min(18).max(120).optional(),
  role: z.enum(["admin", "manager", "normal"]),
});

type CreateUserParams = z.infer<typeof CreateUserSchema>;

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  @transactional()
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    // Zod๋กœ ๊ฒ€์ฆ
    const validated = CreateUserSchema.parse(params);
    
    const wdb = this.getPuri("w");
    
    // validated๋Š” ํƒ€์ž…์ด ๋ณด์žฅ๋จ
    const [user] = await wdb
      .table("users")
      .insert({
        email: validated.email,
        username: validated.username,
        password: validated.password,
        age: validated.age,
        role: validated.role,
      })
      .returning({ id: "id" });
    
    return { userId: user.id };
  }
}

ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฌธ์„œํ™”

JSDoc ์ฃผ์„

/**
 * ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
 */
interface UserListParams {
  /**
   * ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (1๋ถ€ํ„ฐ ์‹œ์ž‘)
   * @example 1
   */
  page: number;
  
  /**
   * ํŽ˜์ด์ง€๋‹น ํ•ญ๋ชฉ ์ˆ˜
   * @example 20
   * @min 1
   * @max 100
   */
  pageSize: number;
  
  /**
   * ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ (username, email์—์„œ ๊ฒ€์ƒ‰)
   * @example "john"
   */
  search?: string;
  
  /**
   * ์‚ฌ์šฉ์ž ์—ญํ•  ํ•„ํ„ฐ
   * @example "admin"
   */
  role?: "admin" | "manager" | "normal";
  
  /**
   * ์ •๋ ฌ ๊ธฐ์ค€ ํ•„๋“œ
   * @default "created_at"
   */
  sortBy?: "created_at" | "username" | "email";
  
  /**
   * ์ •๋ ฌ ์ˆœ์„œ
   * @default "desc"
   */
  sortOrder?: "asc" | "desc";
}

์‹ค์ „ ํŒจํ„ด

ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ

interface PaginationParams {
  page: number;
  pageSize: number;
}

interface UserListParams extends PaginationParams {
  search?: string;
  role?: UserRole;
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    data: User[];
    pagination: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    };
  }> {
    const rdb = this.getPuri("r");
    
    // ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ
    const page = Math.max(1, params.page);
    const pageSize = Math.min(100, Math.max(1, params.pageSize));
    
    // ...
  }
}

ํ•„ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ

interface ProductFilterParams {
  // ์นดํ…Œ๊ณ ๋ฆฌ
  category?: string;
  
  // ๊ฐ€๊ฒฉ ๋ฒ”์œ„
  minPrice?: number;
  maxPrice?: number;
  
  // ์žฌ๊ณ  ์ƒํƒœ
  inStock?: boolean;
  
  // ๋ธŒ๋žœ๋“œ
  brands?: string[];
  
  // ํƒœ๊ทธ
  tags?: string[];
  
  // ๊ฒ€์ƒ‰
  search?: string;
}

class ProductModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async search(
    params: ProductFilterParams & PaginationParams
  ): Promise<Product[]> {
    const rdb = this.getPuri("r");
    
    let query = rdb.table("products");
    
    if (params.category) {
      query = query.where("category", params.category);
    }
    
    if (params.minPrice !== undefined) {
      query = query.where("price", ">=", params.minPrice);
    }
    
    if (params.maxPrice !== undefined) {
      query = query.where("price", "<=", params.maxPrice);
    }
    
    if (params.inStock !== undefined) {
      if (params.inStock) {
        query = query.where("stock", ">", 0);
      } else {
        query = query.where("stock", "=", 0);
      }
    }
    
    if (params.brands && params.brands.length > 0) {
      query = query.whereIn("brand", params.brands);
    }
    
    // ...
  }
}

๋‹ค์Œ ๋‹จ๊ณ„