콘텐츠로 이동

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

  • Backendswag (Echo annotation → OpenAPI)
  • Spec locationdocs/api/openapi.json (generated, committed)
  • Frontendopenapi-typescript
  • Generated TSapps/web/src/types/api.gen.ts (generated, committed)
  • Integration — TanStack Query 의 queryFn 이 생성 타입 사용

Commit generated files

생성된 openapi.jsonapi.gen.tsgit 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 등) 이 생기면 병행 검토

References