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 유지).

공통 규약(양 앱 apps/admin·apps/space):

  • GET/HEAD 외 메서드는 동일 출처 검사(assertSameOrigin) 통과 필수 — 세션 조회보다 먼저 검사.
  • refresh 실패는 원인별 분기invalid(refresh 401/403) 만 session.destroy() + 401, transient(5xx·네트워크) 는 세션 유지 + 502 BACKEND_ERROR.
  • 경로 세그먼트 검증.·..·빈 세그먼트가 있으면 400 INVALID_PATH. encodeURIComponent. 를 인코딩하지 않아 .. 가 그대로 통과하고 fetch 가 정규화하므로, 경로 기반 판정(스코프· public 화이트리스트)과 실제 target 이 어긋날 수 있다.
  • 스트리밍(SSE)은 이 proxy 를 타지 않는다 — catch-all 은 응답을 arrayBuffer() 로 전량 버퍼링하고 Vercel maxDuration 이 스트리밍 시간을 포함하므로, 실시간 통지는 브라우저 → 백엔드 직접 + 단기 티켓으로 연결한다(#180 · 업로드 티켓 #177 과 같은 패턴). Deployment §실시간 통지 채널 참조.
  • apps/space 만: 화이트리스트 반전으로 세션 스코프를 정한다 — 임퍼소네이션이 유효한 경로 (api/v1/hq/** · api/v1/auth/me)만 getActiveSession({ scope: "impersonation-aware" }), 그 외 전부 scope: "real-login"(임퍼소네이션 cookie 미열람). Auth Flow §3-1 참조.

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 의존성)