๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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/findByEmail?email=test@example.com
    // ...
  }

  // ๋ถˆ๋ฆฌ์–ธ
  @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": "test@example.com",
//   "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);
    }

    // ...
  }
}

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

๋ฐ˜ํ™˜ ํƒ€์ž…

์‘๋‹ต ํƒ€์ž… ์ •์˜ํ•˜๊ธฐ

@api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

HTTP ๋ฉ”์„œ๋“œ

GET, POST, PUT, DELETE

์—๋Ÿฌ ์ฒ˜๋ฆฌ

API ์—๋Ÿฌ ํ•ธ๋“ค๋ง