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):
- backend
/v3/api-docsJSON fetch (BACKEND_OPENAPI_URLenv, defaulthttp://localhost:8080/v3/api-docs) packages/api-client/openapi.json갱신 +servers키 제거 (외부 도구가 prod URL 직접 호출 차단)orval --config orval.config.ts실행 →src/generated/endpoints/<tag>/*+src/generated/schemas/*- 후처리:
endpoints/사전순 스캔 →src/generated/index.ts재작성 (root barrel — orvaltags-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.ts의apiFetch— BFF 경유 (admin 의/api/backend/*프록시) - schemas mode:
tags-split—src/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
doneidempotent — 두 번 돌려도 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(수기) vsname?: 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.tslinkmusic-frontend-space/packages/api-client/scripts/sync.shlinkmusic-frontend-space/packages/api-client/src/mutator.tslinkmusic-frontend-space/apps/admin/src/app/api/backend/[...path]/route.tslinkmusic-msa-space-was/build.gradle.kts(springdoc 의존성)