Communitygithub.com

API 및 인터페이스 디자인

안정적인 API 및 인터페이스 디자인을 안내합니다. API, 모듈 경계 또는 모든 공개 인터페이스를 설계할 때 사용하세요. REST 또는 GraphQL 엔드포인트 생성, 모듈 간 타입 계약 정의, 또는 프론트엔드와 백엔드 간 경계를 설정할 때 사용하세요.

API 및 인터페이스 디자인란 무엇인가요?

이 스킬은 Hyrum의 법칙, One-Version Rule, 계약 우선 설계, 일관된 오류 의미론 등 핵심 원칙을 바탕으로 안정적이고 잘 문서화된 API 및 인터페이스를 설계하는 방법을 제공합니다. REST API, GraphQL 스키마, 모듈 경계, 컴포넌트 props 등 다양한 공개 인터페이스에 적용 가능하며, Claude Code, Cursor, Codex와 같은 AI 에이전트 플랫폼에서 활용할 수 있습니다. 또한 멱등성 키 설계 및 구현에 대한 실용적인 지침을 포함하여 분산 시스템의 복잡성을 다루며, 일반적인 합리화와 레드 플래그를 통해 개발자가 함정을 피하고 안정적인 API를 설계할 수 있도록 돕습니다. 주요 기능으로는 타입스크립트 인터페이스 패턴, REST API 패턴, 경계에서의 검증, 추가를 통한 확장 선호 원칙 등이 포함됩니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/addyosmani/agent-skills/tree/HEAD/skills/api-and-interface-design

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

API 및 인터페이스 디자인은(는) 무엇을 하나요?

개요

오용하기 어려운 안정적이고 잘 문서화된 인터페이스를 설계하세요. 좋은 인터페이스는 올바른 작업을 쉽게 만들고 잘못된 작업을 어렵게 만듭니다. 이는 REST API, GraphQL 스키마, 모듈 경계, 컴포넌트 props 등 코드가 서로 통신하는 모든 표면에 적용됩니다.

사용 시기

  • 새 API 엔드포인트 디자인
  • 팀 간 모듈 경계 또는 계약 정의
  • 컴포넌트 prop 인터페이스 생성
  • API 모양을 결정하는 데이터베이스 스키마 설정
  • 기존 공개 인터페이스 변경

핵심 원칙

Hyrum의 법칙

충분한 수의 API 사용자가 있는 경우, 시스템의 모든 관찰 가능한 동작은 계약에서 약속한 내용과 관계없이 누군가에 의해 의존됩니다.

즉, 모든 공개 동작(문서화되지 않은 특성, 오류 메시지 텍스트, 타이밍, 순서 포함)은 사용자가 의존하기 시작하면 사실상의 계약이 됩니다. 설계 시 고려 사항:

  • 노출하는 것에 대해 의도적으로 결정하세요. 모든 관찰 가능한 동작은 잠재적인 약속입니다.
  • 구현 세부 사항을 유출하지 마세요. 사용자가 관찰할 수 있다면, 그것에 의존하게 됩니다.
  • 설계 시점에 폐지 계획을 세우세요. 사용자가 의존하는 것을 안전하게 제거하는 방법은 deprecation-and-migration을 참조하세요.
  • 테스트만으로는 충분하지 않습니다. 완벽한 계약 테스트가 있어도 Hyrum의 법칙에 따르면 "안전한" 변경이 문서화되지 않은 동작에 의존하는 실제 사용자에게 문제를 일으킬 수 있습니다.

One-Version Rule

소비자가 동일한 종속성이나 API의 여러 버전 중에서 선택하도록 강요하지 마세요. 다른 소비자가 동일한 것의 다른 버전을 필요로 할 때 다이아몬드 종속성 문제가 발생합니다. 한 번에 하나의 버전만 존재하는 세상을 위해 설계하세요. 포크하지 말고 확장하세요.

1. 계약 우선

인터페이스를 구현하기 전에 정의하세요. 계약이 명세서이며 구현은 그 다음입니다.

// 계약을 먼저 정의
interface TaskAPI {
  // 작업을 생성하고 서버 생성 필드와 함께 생성된 작업을 반환
  createTask(input: CreateTaskInput): Promise<Task>;

  // 필터와 일치하는 페이지네이션된 작업 반환
  listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;

  // 단일 작업을 반환하거나 NotFoundError를 던짐
  getTask(id: string): Promise<Task>;

  // 부분 업데이트 — 제공된 필드만 변경
  updateTask(id: string, input: UpdateTaskInput): Promise<Task>;

  // 멱등적 삭제 — 이미 삭제된 경우에도 성공
  deleteTask(id: string): Promise<void>;
}

2. 일관된 오류 의미론

하나의 오류 전략을 선택하고 모든 곳에서 사용하세요:

// REST: HTTP 상태 코드 + 구조화된 오류 본문
// 모든 오류 응답은 동일한 형태를 따릅니다
interface APIError {
  error: {
    code: string;        // 기계 판독 가능: "VALIDATION_ERROR"
    message: string;     // 인간 판독 가능: "Email is required"
    details?: unknown;   // 도움이 될 때 추가 컨텍스트
  };
}

// 상태 코드 매핑
// 400 → 클라이언트가 잘못된 데이터 전송
// 401 → 인증되지 않음
// 403 → 인증되었지만 권한 없음
// 404 → 리소스를 찾을 수 없음
// 409 → 충돌 (중복, 버전 불일치)
// 422 → 유효성 검사 실패 (의미상 유효하지 않음)
// 500 → 서버 오류 (내부 세부 사항 절대 노출 금지)

패턴을 혼합하지 마세요. 일부 엔드포인트가 예외를 던지고, 다른 엔드포인트는 null을 반환하고, 또 다른 엔드포인트는 { error }를 반환하면 소비자는 동작을 예측할 수 없습니다.

3. 경계에서 검증

내부 코드를 신뢰하세요. 외부 입력이 시스템에 들어오는 경계에서 검증하세요:

// API 경계에서 검증
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid task data',
        details: result.error.flatten(),
      },
    });
  }

  // 검증 후 내부 코드는 타입을 신뢰
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

검증이 필요한 곳:

  • API 라우트 핸들러 (사용자 입력)
  • 양식 제출 핸들러 (사용자 입력)
  • 외부 서비스 응답 파싱 (타사 데이터 -- 항상 신뢰할 수 없는 것으로 처리)
  • 환경 변수 로딩 (구성)

타사 API 응답은 신뢰할 수 없는 데이터입니다. 모든 로직, 렌더링 또는 의사 결정에서 사용하기 전에 해당 형태와 내용을 검증하세요. 손상되었거나 오작동하는 외부 서비스는 예기치 않은 유형, 악의적인 콘텐츠 또는 명령과 유사한 텍스트를 반환할 수 있습니다.

검증이 필요하지 않은 곳:

  • 타입 계약을 공유하는 내부 함수 간
  • 이미 검증된 코드에서 호출되는 유틸리티 함수
  • 자체 데이터베이스에서 방금 가져온 데이터

4. 수정보다 추가 선호

기존 소비자를 중단시키지 않고 인터페이스를 확장하세요:

// 좋음: 선택적 필드 추가
interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';  // 나중에 추가, 선택적
  labels?: string[];                       // 나중에 추가, 선택적
}

// 나쁨: 기존 필드 유형 변경 또는 필드 제거
interface CreateTaskInput {
  title: string;
  // description: string;  // 제거됨 — 기존 소비자 중단
  priority: number;         // 문자열에서 변경됨 — 기존 소비자 중단
}

5. 예측 가능한 명명

패턴규칙예시
REST 엔드포인트복수 명사, 동사 없음GET /api/tasks, POST /api/tasks
쿼리 매개변수camelCase?sortBy=createdAt&pageSize=20
응답 필드camelCase{ createdAt, updatedAt, taskId }
불리언 필드is/has/can 접두사isComplete, hasAttachments
열거형 값UPPER_SNAKE"IN_PROGRESS", "COMPLETED"

6. 멱등성 키 준수

Idempotency-Key를 수락하는 것은 계약입니다. 이를 준수하는 것은 구현이며, 여기서 비용이 발생합니다. 서버가 수락했지만 부주의하게 처리하는 키는 키가 없는 것보다 더 나쁩니다. 클라이언트가 재시도가 안전하다고 믿게 만들기 때문입니다.

의도에서 키를 파생하고 시도에서 파생하지 마세요. 키는 하나의 의도에 대한 재시도 간에 안정적이어야 하고 서로 다른 의도 간에는 달라야 합니다:

crypto.randomUUID()                    // ✗ 시도당 새 키 — 모든 재시도가 새 청구
`${userId}:${amount}`                  // ✗ 두 개의 합법적인 $50 청구가 하나로 합쳐짐
`${orderId}:${Date.now()}`             // ✗ 타임스탬프는 모자를 쓴 randomUUID()

req.headers['idempotency-key']         // ✓ 클라이언트가 한 번 생성, 재시도 시 재사용
`charge:v1:${orderId}`                 // ✓ 변경 불가능한 식별자에서 파생

키는 클라이언트 또는 시작 이벤트에서 가져와야 하며, 재시도하는 계층에서 절대 가져오지 마세요.

원자적으로 획득하세요. 확인 후 작업은 경쟁 조건입니다:

// ✗ TOCTOU: 두 개의 동시 재시도가 모두 "본 적 없음"을 읽고, 둘 다 청구
if (!(await db.exists(key))) {
  await chargeCard(amount);
  await db.insert(key);
}

// ✓ 고유 제약 조건이 승자를 결정하게 하세요
 try {
  await db.insert({ key, state: 'in_progress', requestHash });
} catch (e) {
  if (isUniqueViolation(e)) return replayOrReject(key);
  throw;
}
const result = await chargeCard(amount);
await db.update({ key, state: 'succeeded', response: result });

고유 제약 조건이 메커니즘입니다. 한 번의 작업으로 고유성을 강제할 수 없는 저장소는 이 기능을 지원할 수 없습니다.

페이로드를 보호하세요. 동일한 키에 다른 본문이 있는 것은 클라이언트 버그이며, 두 번째 요청에 첫 번째 응답을 제공하는 대신 명확하게 실패해야 합니다:

if (existing.requestHash !== hash(req.body)) {
  return res.status(422).json({ error: 'idempotency key reused with a different payload' });
}

진행 중인 중복에 대한 처리를 결정하세요. 첫 번째 요청이 여전히 실행 중일 때 두 번째 요청이 도착합니다. 재시도 폭풍의 일반적인 경우입니다:

전략응답사용 시기
거부409 Conflict클라이언트가 나중에 재시도 가능, 가장 간단하고 안전함
대기결과를 위해 차단, 제한됨호출자가 동기적으로 필요로 함
보류 중 반환202 + 상태 URL장기 실행 효과

첫 번째 요청이 "멈춘 것처럼 보인다"고 두 번째 호출자를 통과시키지 마세요. 운명을 알 수 없는 중단된 시도는 정확히 중복 비용이 가장 많이 드는 순간입니다.

모든 호출에는 세 가지 결과가 있습니다: 성공, 실패, 그리고 알 수 없음. 시간 초과는 효과가 적용되었는지에 대해 아무것도 알려주지 않습니다. 외부 호출 전에 의도를 기록하여 호출과 응답 사이의 충돌이 나중에 해결해야 할 증거를 남기도록 하세요. 조용히 재시도되는 청구 대신.

디스크 비용이 아닌 가장 긴 재시도 체인에서 보존 기간을 설정하세요. 키는 동일한 의도를 재전달할 수 있는 모든 경로보다 오래 지속되어야 합니다. 여기에는 일주일 후에 재생되는 데드 레터 큐와 공급자 분쟁 기간이 포함됩니다. 7일 DLQ 뒤의 24시간 키 TTL은 발생을 기다리는 중복입니다.

REST API 패턴

리소스 디자인

GET    /api/tasks              → 작업 목록 (필터링을 위한 쿼리 매개변수 포함)
POST   /api/tasks              → 작업 생성
GET    /api/tasks/:id          → 단일 작업 가져오기
PATCH  /api/tasks/:id          → 작업 업데이트 (부분)
DELETE /api/tasks/:id          → 작업 삭제

GET    /api/tasks/:id/comments → 작업에 대한 댓글 목록 (하위 리소스)
POST   /api/tasks/:id/comments → 작업에 댓글 추가

페이지네이션

목록 엔드포인트에 페이지네이션 적용:

// 요청
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc

// 응답
{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalItems": 142,
    "totalPages": 8
  }
}

필터링

필터에 쿼리 매개변수 사용:

GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01

부분 업데이트 (PATCH)

부분 객체 수락 — 제공된 것만 업데이트:

// 제목만 변경, 나머지는 유지
PATCH /api/tasks/123
{ "title": "Updated title" }

타입스크립트 인터페이스 패턴

변형에 판별 유니온 사용

// 좋음: 각 변형이 명시적
 type TaskStatus =
  | { type: 'pending' }
  | { type: 'in_progress'; assignee: string; startedAt: Date }
  | { type: 'completed'; completedAt: Date; completedBy: string }
  | { type: 'cancelled'; reason: string; cancelledAt: Date };

// 소비자는 타입 좁히기 가능
function getStatusLabel(status: TaskStatus): string {
  switch (status.type) {
    case 'pending': return '대기 중';
    case 'in_progress': return `진행 중 (${status.assignee})`;
    case 'completed': return `${status.completedAt}에 완료됨`;
    case 'cancelled': return `취소됨: ${status.reason}`;
  }
}

입력/출력 분리

// 입력: 호출자가 제공하는 것
interface CreateTaskInput {
  title: string;
  description?: string;
}

// 출력: 시스템이 반환하는 것 (서버 생성 필드 포함)
interface Task {
  id: string;
  title: string;
  description: string | null;
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
}

ID에 브랜드 타입 사용

type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };

// UserId가 필요한 곳에 TaskId를 실수로 전달하는 것을 방지
function getTask(id: TaskId): Promise<Task> { ... }

일반적인 합리화

합리화현실
"API 문서는 나중에 작성할게요"타입이 문서입니다. 먼저 정의하세요.
"지금은 페이지네이션이 필요 없어요"누군가 100개 이상의 항목을 가질 때 필요해집니다. 처음부터 추가하세요.
"PATCH는 복잡하니까 PUT을 사용하자"PUT은 매번 전체 객체가 필요합니다. PATCH가 클라이언트가 실제로 원하는 것입니다.
"필요할 때 API 버전을 관리할게요"버전 관리 없이 주요 변경 사항은 소비자를 중단시킵니다. 처음부터 확장을 위해 설계하세요.
"아무도 문서화되지 않은 동작을 사용하지 않아요"Hyrum의 법칙: 관찰 가능하면 누군가 의존합니다. 모든 공개 동작을 약속으로 취급하세요.
"두 버전을 유지하면 돼"여러 버전은 유지 비용을 증가시키고 다이아몬드 종속성 문제를 만듭니다. One-Version Rule을 선호하세요.
"내부 API는 계약이 필요 없어"내부 소비자도 여전히 소비자입니다. 계약은 결합을 방지하고 병렬 작업을 가능하게 합니다.
"Idempotency-Key 헤더를 수락하는 것으로 충분해"헤더는 계약이고, 결과에 대해 키를 저장하는 것이 구현입니다. 수락했지만 준수하지 않는 키는 재시도가 안전하다고 클라이언트에게 말하지만 실제로는 그렇지 않습니다.
"우리 큐가 정확히 한 번 전달을 보장해"소비자 충돌 시 어떤 큐도 그렇게 하지 않습니다. 브로커의 ack와 사이드 이펙트는 하나의 트랜잭션에 있지 않습니다. 멱등적 처리를 통해 최소 한 번 전달을 위해 설계하세요.
"중복 요청은 드물어"그것들은 상관관계가 있습니다. 재시도는 종속성이 저하될 때 정확히 급증합니다. 중복이 가장 가능성이 높고 비용이 가장 많이 드는 순간입니다.

레드 플래그

  • 조건에 따라 다른 형태를 반환하는 엔드포인트
  • 엔드포인트 간 일관되지 않은 오류 형식
  • 경계 대신 내부 코드 전반에 흩어진 유효성 검사
  • 기존 필드에 대한 주요 변경 사항 (유형 변경, 제거)
  • 페이지네이션이 없는 목록 엔드포인트
  • REST URL의 동사 (/api/createTask, /api/getUsers)
  • 검증이나 살균 없이 사용되는 타사 API 응답
  • 멱등성 키에 대한 SELECT 다음 INSERT — 경쟁 조건이지 보호가 아닙니다.
  • UUID, 타임스탬프 또는 시도당 재생성되는 다른 것에서 파생된 멱등성 키
  • 동일한 키가 다른 요청 본문으로 수락되고, 첫 번째 응답을 조용히 반환하는 경우
  • 요청을 재전달할 수 있는 가장 긴 경로보다 짧은 키 보존 기간

검증

API 설계 후:

  • 모든 엔드포인트에 타입이 지정된 입력 및 출력 스키마가 있음
  • 오류 응답이 단일 일관된 형식을 따름
  • 검증이 시스템 경계에서만 발생함
  • 목록 엔드포인트가 페이지네이션을 지원함
  • 새 필드가 추가적이고 선택적임 (하위 호환성 유지)
  • 명명이 모든 엔드포인트에서 일관된 규칙을 따름
  • API 문서가 인터페이스 변경으로 최신 상태로 유지됨
  • 멱등성 키가 의도에서 파생되고 원자적으로 저장되며 재생으로부터 보호됨
  • 보존 기간이 가장 긴 재시도 체인을 포괄함

마이그레이션 노트

인터페이스 변경 시 적절한 deprecation 전략을 따르고, 모든 소비자에게 충분한 마이그레이션 기간을 제공하며, 중단 변경 사항을 주 버전 릴리스에서 명확하게 문서화하세요.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

addyosmani/agent-skills

Prepares production launches. Use when preparing to deploy to production, or when asking what needs to be in place before shipping. Use when you need a pre-launch checklist, when setting up monitoring, when planning a staged rollout, or when you need a rollback strategy.

addyosmani/agent-skills

Builds production-quality, accessible, responsive user-facing UIs. Use when building or modifying interfaces and pages, creating components, implementing layouts, meeting WCAG accessibility requirements, managing state, or when the output needs to look and feel production-quality rather than AI-generated.

addyosmani/browser-testing-with-devtools

Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.

addyosmani/ci-cd-and-automation

Automates CI/CD pipeline setup. Use when setting up or modifying build and deployment pipelines. Use when you need to automate quality gates, configure test runners in CI, or establish deployment strategies.

addyosmani/code-review-and-quality

Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.

addyosmani/code-simplification

Simplifies code for clarity. Use when refactoring code for clarity without changing behavior. Use when code works but is harder to read, maintain, or extend than it should be. Use when reviewing code that has accumulated unnecessary complexity.

addyosmani/constraint-driven-development

Establishes a project's quality bar as a written contract and stops agents quietly lowering it. Interviews the user on which dimensions matter, supplies sane default thresholds when they have no number in mind, records everything in CONSTRAINTS.md, and watches the diff for a weakened bar — new @ts-ignore or eslint-disable suppressions, skipped or deleted tests, assertions stripped out, unimplemented stubs, thresholds edited down. Use when no quality bar is written down, when the user says "set up constraints" or "define our standards", when the user wants dimensions they care about — accessibility, web performance, coverage — set up as enforced constraints, when an agent keeps silencing checks or skipping tests to get to green, when you need a coverage or performance threshold and don't know what number to pick, or when an agent writes more code than anyone will read.

addyosmani/debugging-and-error-recovery

Guides systematic root-cause debugging. Use when tests fail, builds break, behavior doesn't match expectations, or you encounter any unexpected error. Use when you need a systematic approach to finding and fixing the root cause rather than guessing.

addyosmani/deprecation-and-migration

Manages deprecation and migration. Use when removing old systems, APIs, or features. Use when migrating users from one implementation to another. Use when deciding whether to maintain or sunset existing code.

addyosmani/documentation-and-adrs

Records decisions and documentation. Use when making architectural decisions, changing public APIs, shipping features, or when you need to record context that future engineers and agents will need to understand the codebase.

addyosmani/doubt-driven-development

Subjects every non-trivial decision to a fresh-context adversarial review before it stands. Use when correctness matters more than speed, when working in unfamiliar code, when stakes are high (production, security-sensitive logic, irreversible operations), or any time a confident output would be cheaper to verify now than to debug later.

addyosmani/git-workflow-and-versioning

Structures git workflow practices. Use when making any code change. Use when committing, branching, resolving conflicts, or when you need to organize work across multiple parallel streams.

addyosmani/idea-refine

Refines raw ideas into sharp, actionable concepts through structured divergent and convergent thinking. Use when an idea is still vague, when you need to stress-test assumptions before committing to a plan, or when you want to expand options before converging on one. Triggers on "ideate", "refine this idea", or "stress-test my plan".

addyosmani/incremental-implementation

Delivers changes incrementally. Use when implementing any feature or change that touches more than one file. Use when you're about to write a large amount of code at once, or when a task feels too big to land in one step.

addyosmani/interview-me

Extracts what the user actually wants instead of what they think they should want. Achieves this through one-question-at-a-time interview until ~95% confidence about the underlying intent. Use when an ask is underspecified ("build me X" without "for whom" or "why now"), when the user explicitly invokes ("interview me", "grill me", "are we sure?", "stress-test my thinking"), or when you catch yourself silently filling in ambiguous requirements before any plan, spec, or code exists.

addyosmani/observability-and-instrumentation

Instruments code so production behavior is visible and diagnosable. Use when adding logging, metrics, tracing, or alerting. Use when shipping any feature that runs in production and you need evidence it works. Use when production issues are reported but you can't tell what happened from the available data.

addyosmani/performance-optimization

Optimizes application performance. Use when performance requirements exist, when you suspect performance regressions, or when Core Web Vitals or load times need improvement. Use when profiling reveals bottlenecks that need fixing.

addyosmani/planning-and-task-breakdown

Breaks work into ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable tasks. Use when a task feels too large to start, when you need to estimate scope, or when parallel work is possible.

addyosmani/security-and-hardening

Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.

addyosmani/source-driven-development

Grounds every implementation decision in official documentation. Use when you want authoritative, source-cited code free from outdated patterns. Use when building with any framework or library where correctness matters.

관련 스킬