ArchitectureAPI Contract (OpenAPI · orval)

API Contract — OpenAPI 단일 소스 + orval generated

SPEC #006 §2-4 정합. 모든 backend 응답 DTO 는 generated schema 만 사용 (수기 정의 금지).

Overview

Springdoc OpenAPI 가 backend /v3/api-docs 에 OpenAPI v3 spec 을 emit. packages/api-client 가 orval 로 TypeScript 클라이언트 + zod 스키마를 생성. 단일 소스 원칙 — 프론트에서 수기 type 정의 금지.

Sync Flow

pnpm sync-api 스킬

# 워크스페이스 root 에서
pnpm --filter @linkmusic/api-client sync

내부 step (packages/api-client/scripts/sync.sh):

  1. backend /v3/api-docs JSON fetch (BACKEND_OPENAPI_URL env, default http://localhost:8080/v3/api-docs)
  2. packages/api-client/openapi.json 갱신 + servers 키 제거 (외부 도구가 prod URL 직접 호출 차단)
  3. orval --config orval.config.ts 실행 → src/generated/endpoints/<tag>/* + src/generated/schemas/*
  4. 후처리: endpoints/ 사전순 스캔 → src/generated/index.ts 재작성 (root barrel — orval tags-split 가 emit 안 함, idempotent)

orval 설정 (packages/api-client/orval.config.ts)

  • mode: tags-split — endpoint 를 OpenAPI tag 별 폴더로 분리 (endpoints/auth-controller/* · endpoints/hq-admin-controller/* 등)
  • httpClient: fetch — 별도 axios 의존성 X
  • mutator: packages/api-client/src/mutator.tsapiFetch — BFF 경유 (admin 의 /api/backend/* 프록시)
  • schemas mode: tags-splitsrc/generated/schemas/<Name>.ts
  • client: tanstack-query — react-query 훅 자동 생성 (useQuery · useMutation)

Mutator (packages/api-client/src/mutator.ts)

// 의사 코드 — 실제 구현은 packages/api-client/src/mutator.ts
export const apiFetch = async <T>(
  url: string,
  init: RequestInit,
): Promise<{ data: T; status: number; headers: Headers }> => {
  // admin: BFF prefix /api/backend
  const fullUrl = `/api/backend${url}`;
  const res = await fetch(fullUrl, {
    ...init,
    credentials: init.credentials ?? "same-origin",  // 동일 출처 → lm_session cookie 자동 첨부
    headers: { "Content-Type": "application/json", ...init.headers },
  });
  if (!res.ok) {
    throw new BackendCallError(res.status, await res.json());
  }
  const data = (await res.json()) as T;
  return { data, status: res.status, headers: res.headers };
};

tags-split 모드의 generated caller 가 { data, status, headers } shape 를 expect — mutator 시그니처 정합 필수 ([[feedback-10]]).

Barrel 자동화

tags-split 은 root barrel 을 emit 하지 않으므로 scripts/sync.sh 가 후처리에서 만든다:

# scripts/sync.sh 후처리 의사 코드
files=$(ls src/generated/endpoints/*/*.ts | sort)
for f in $files; do
  echo "export * from './$(...path...)';" >> src/generated/index.ts
done

idempotent — 두 번 돌려도 diff=0.

Constraints (재구현 Blueprint)

  • 수기 type 정의 금지lib/backend.ts · lib/use-me.ts 등에서 backend 응답 DTO 의 type alias 를 generated schema 만 import ([[feedback-15]]).
  • null vs undefined 함정 — backend 가 nullable 로 보낼지 omit 으로 보낼지는 backend 결정. 프론트는 OpenAPI 그대로 반영. 예: name: string | null (수기) vs name?: string (OpenAPI) 는 runtime 분기가 다름.
  • generated barrel 은 commit — CI 가 pnpm sync-api && git diff --exit-code 로 drift 검증 (후속).
  • backend OpenAPI 의 enum / pattern / min / max 는 클라이언트 zod 와 일치pnpm sync-api 가 자동 처리. 수기 form validation 도 zod schema 재사용 ([[feedback-8]]).

apps/admin/src/app/api/backend/[...path]/route.ts

generated caller 가 /api/backend/auth/me 로 요청 → 이 BFF route 가 prefix 제거 후 backend (/api/v1/auth/me) 로 프록시. iron-session 에서 access 꺼내 Authorization: Bearer 헤더 첨부. refresh aware + 401 1회 재시도 + 5xx → 502 BACKEND_ERROR (session 유지).

Roadmap

  • CI drift 검증 (sync 후 변경 없음 확인)
  • OpenAPI tag 명 규약 (auth · admin-hq · admin-stores · …)
  • 자동 changelog 생성 (OpenAPI diff)

References

  • SPEC #006 §2-4
  • linkmusic-frontend-space/packages/api-client/orval.config.ts
  • linkmusic-frontend-space/packages/api-client/scripts/sync.sh
  • linkmusic-frontend-space/packages/api-client/src/mutator.ts
  • linkmusic-frontend-space/apps/admin/src/app/api/backend/[...path]/route.ts
  • linkmusic-msa-space-was/build.gradle.kts (springdoc 의존성)