ADR-0030: OpenAPI Type Synchronization¶
Umbra 의 Go 백엔드(Echo)에서 swag 으로 OpenAPI spec 을 생성하고, openapi-typescript 로 프론트엔드 TS 타입을 자동 생성한다. 생성된 파일은 git commit 되어 CI 에서 drift detection 에 사용된다.
Status¶
Accepted
- Decided at — 2026-04-13
- Decided by — Pablo
Context¶
프론트엔드(TS)와 백엔드(Go)가 분리된 구조에서 API 계약(contract)의 타입 안전성을 유지해야 한다. 옵션:
- tRPC-like — 같은 언어에서 타입 공유 (Umbra 는 언어 다름 → 불가)
- OpenAPI 기반 생성 — 백엔드에서 spec 생성, 프론트엔드가 타입 import
- GraphQL — schema 기반 타입 생성
- 수동 유지 — 양쪽에 type 수동 작성 (drift 위험)
Decision¶
OpenAPI 3.x spec 기반 타입 동기화 를 채택한다.
Pipeline¶
Echo handlers (with swag annotations)
│
▼
swag CLI (go generate)
│
▼
docs/api/openapi.json (commit)
│
▼
openapi-typescript (bun generate)
│
▼
apps/web/src/types/api.gen.ts (commit)
│
▼
Frontend code imports
Tools¶
- Backend — swag (Echo annotation → OpenAPI)
- Spec location —
docs/api/openapi.json(generated, committed) - Frontend — openapi-typescript
- Generated TS —
apps/web/src/types/api.gen.ts(generated, committed) - Integration — TanStack Query 의
queryFn이 생성 타입 사용
Commit generated files¶
생성된 openapi.json 과 api.gen.ts 는 git commit 한다. 이유:
- PR 에서 API 변경이 diff 로 즉시 보임 (리뷰 가능)
- CI 가 "generated 파일이 latest 와 일치하는가" drift 감지
- 로컬 개발 시 매번 생성 없이도 빌드 가능
CI drift detection¶
on pull_request:
- run: go generate ./...
- run: bun run openapi:gen
- run: git diff --exit-code docs/api/openapi.json apps/web/src/types/api.gen.ts
# diff 있으면 CI 실패 → "generated 파일 commit 안 함" 경고
선택 근거:
- 다언어 타입 공유 — Go 와 TS 간 타입 안전성을 OpenAPI 를 intermediate representation 으로 확보
- 업계 표준 — OpenAPI 는 REST API spec 의 de-facto 표준
- 도구 성숙 — swag, openapi-typescript 모두 활발히 유지
- 수동 drift 방지 — CI 가 자동 검증
Consequences¶
Positive¶
- API 타입 변경이 백엔드 → 자동 → 프론트엔드 반영
- PR 리뷰에서 API 변경이 명시적으로 보임
- 프론트엔드에서 잘못된 API 호출은 컴파일 타임에 감지
- OpenAPI spec 은 외부 공개 시에도 바로 사용 가능
Negative¶
- swag annotation 을 handler 마다 작성해야 함
- generated 파일 2개 관리 (commit 필요)
- swag 의 annotation 문법 학습 필요
- Go struct 와 OpenAPI schema 매핑의 한계 (generic, union 등)
Neutral¶
- Backend PR 에 frontend 타입 변경이 자동 포함 → 풀스택 PR 이 자연
- GraphQL 대비 query 유연성은 떨어지나 REST 의 단순성 확보
Swag annotation pattern¶
// @Summary Start subscription
// @Description Start a Pro subscription for a guild
// @Tags subscriptions
// @Accept json
// @Produce json
// @Param request body dto.StartSubscriptionRequest true "Subscription details"
// @Success 200 {object} dto.SubscriptionResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 402 {object} dto.ErrorResponse "Payment required"
// @Router /api/v1/subscriptions [post]
func (h *SubscriptionHandler) Start(c echo.Context) error {
// ...
}
DTO struct 는 JSON tag + swag tag 로 스키마 기술.
type StartSubscriptionRequest struct {
GuildID string `json:"guild_id" example:"01J0XX..."`
PlanCode string `json:"plan_code" example:"PRO" enums:"PRO,ENTERPRISE"`
}
Frontend usage¶
import type { paths } from '@/types/api.gen'
type StartResp = paths['/api/v1/subscriptions']['post']['responses']['200']['content']['application/json']
// TanStack Query
const mutation = useMutation<StartResp, Error, StartSubscriptionRequest>({
mutationFn: (body) => apiFetch('/api/v1/subscriptions', { method: 'POST', body }),
})
타입이 자동 추론되어 필드명 오타, 누락 필드가 컴파일 타임에 잡힘.
Alternatives considered¶
Alternative 1: GraphQL (gqlgen + graphql-codegen)¶
Pros
- Query 유연성
- 강력한 타입 시스템
Cons
- REST 대비 복잡도 ↑ (resolver, N+1 문제 관리)
- Toss 웹훅 같은 REST 엔드포인트는 REST 유지 필요 → 혼합 구조
- 학습 부담
Why rejected — Umbra 의 API 규모에 GraphQL 은 과함. REST + OpenAPI 로 충분.
Alternative 2: tRPC-like (공유 타입)¶
Pros
- 가장 타입 안전
Cons
- 언어가 달라 직접 적용 불가 (Go 백엔드 + TS 프론트)
- Go 의 tRPC 대안 미성숙
Why rejected — 구조적으로 불가능.
Alternative 3: 수동 타입 작성¶
Pros
- 도구 부재로 심플
Cons
- Drift 위험 (백엔드 변경 시 프론트 type 수동 업데이트)
- 팀 규모 확장 시 실수 증가
Why rejected — 결제 SaaS 의 타입 안전성 요구에 부적합.
Alternative 4: huma (Go OpenAPI-first)¶
Pros
- OpenAPI spec 을 first-class 로 취급
- Echo 대비 타입 안전성 ↑
Cons
- Echo 가 이미 채택 (ADR-0003)
- 프레임워크 재선정 부담
Why rejected — Echo 유지. swag annotation 으로 충분.
Alternative 5: 프론트엔드에서 Zod schema 중복 정의¶
Pros
- 런타임 검증 가능
Cons
- Go struct 와 Zod schema 수동 동기화
- drift 위험
Why rejected — openapi-typescript 가 타입만 제공하고 런타임 검증은 별도라면, Zod 는 런타임 검증용도로만 선택적 사용. 타입 소스 of truth 는 OpenAPI spec.
Compliance¶
- Echo handler 는 swag annotation 필수 (CI 가 annotation 누락 감지)
- DTO 는
apps/api/internal/dto/또는engine/{domain}/port/dto/에 위치 - generated 파일(
openapi.json,api.gen.ts) 은 수동 편집 금지 (CI 검증) - API breaking change 시 spec version bump 고려 (
/api/v2/) - 프론트엔드에서 타입을
paths['...']패턴으로 사용, 직접 재정의 금지
Revisit triggers¶
- swag 의 유지보수 정체나 OpenAPI 3.1 호환성 문제 → huma 등 대안 검토
- API 가 외부 개발자에 공개되면 spec 품질 기준 강화
- GraphQL 이 필요해지는 특정 영역(대시보드 데이터 aggregation 등) 이 생기면 병행 검토