--- id: api-openapi-spec title: OpenAPI / Swagger — Schema-first vs Code-first category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [api, openapi, swagger, vibe-coding] tech_stack: { language: "TS / OpenAPI", applicable_to: ["Backend"] } applied_in: [] aliases: [OpenAPI, Swagger, schema-first, code-first, Hono RPC, oRPC, ts-rest] --- # OpenAPI > API contract 표준. **Schema → Type generation, mock server, client SDK, docs**. Schema-first 또는 Code-first. **Hono / ts-rest / oRPC** 가 modern code-first. ## 📖 핵심 개념 - OpenAPI 3.1: 표준 spec. - Schema-first: yaml/json 먼저 → 코드 생성. - Code-first: 코드의 type → spec 자동. - Server / client SDK 자동. ## 💻 코드 패턴 ### Schema-first (yaml) ```yaml openapi: 3.1.0 info: title: Acme API version: 1.0.0 paths: /orders: get: summary: List orders parameters: - { name: limit, in: query, schema: { type: integer, default: 20 } } - { name: cursor, in: query, schema: { type: string } } responses: '200': description: ok content: application/json: schema: $ref: '#/components/schemas/OrderList' post: summary: Create order requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateOrder' } responses: '201': description: created content: application/json: schema: { $ref: '#/components/schemas/Order' } '400': description: validation error content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } components: schemas: Order: type: object required: [id, items, total] properties: id: { type: string, format: uuid } items: type: array items: { $ref: '#/components/schemas/OrderItem' } total: { type: string } status: { type: string, enum: [open, paid, shipped] } Problem: type: object required: [type, title, status] properties: type: { type: string, format: uri } title: { type: string } status: { type: integer } detail: { type: string } ``` ### 코드 생성 ```bash # Server stub openapi-generator-cli generate -i api.yaml -g typescript-node-server -o server/ # Client SDK openapi-generator-cli generate -i api.yaml -g typescript-axios -o client/ # 또는 modern: openapi-typescript npx openapi-typescript api.yaml -o api-types.ts ``` ```ts import type { paths } from './api-types'; type CreateOrderRequest = paths['/orders']['post']['requestBody']['content']['application/json']; type OrderResponse = paths['/orders']['post']['responses']['201']['content']['application/json']; ``` ### Code-first — Hono + zod-openapi ```ts import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; const app = new OpenAPIHono(); const route = createRoute({ method: 'post', path: '/orders', request: { body: { content: { 'application/json': { schema: CreateOrderSchema } }, }, }, responses: { 201: { content: { 'application/json': { schema: OrderSchema } }, description: 'Created', }, }, }); app.openapi(route, async (c) => { const data = c.req.valid('json'); // typed const order = await createOrder(data); return c.json(order, 201); }); // Spec 노출 app.doc('/openapi.json', { openapi: '3.1.0', info: { title: 'API', version: '1.0' } }); ``` ### Code-first — ts-rest ```ts import { initContract } from '@ts-rest/core'; const c = initContract(); export const contract = c.router({ createOrder: { method: 'POST', path: '/orders', body: CreateOrderSchema, responses: { 201: OrderSchema, 400: ProblemSchema }, }, }); // Server (Express / Fastify / Hono) import { initServer } from '@ts-rest/express'; const router = initServer().router(contract, { createOrder: async ({ body }) => ({ status: 201, body: await create(body) }), }); // Client (auto-typed) import { initClient } from '@ts-rest/core'; const api = initClient(contract, { baseUrl: '...' }); const r = await api.createOrder({ body: { items: [...] } }); ``` → Frontend / backend 가 type 공유. ### Mock server (fast prototyping) ```bash # Prism — OpenAPI mock prism mock api.yaml --port 4010 ``` → Backend 만들기 전에 frontend 시작. ### Docs UI ```ts // Swagger UI import swaggerUi from 'swagger-ui-express'; app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec)); // Scalar (modern, beautiful) import { apiReference } from '@scalar/express-api-reference'; app.use('/docs', apiReference({ spec: { url: '/openapi.json' } })); // Stoplight Elements ``` ### Validation (Hono / Express middleware) ```ts // Express import OpenApiValidator from 'express-openapi-validator'; app.use(OpenApiValidator.middleware({ apiSpec: 'api.yaml' })); // 자동 validate body / query / response ``` ### Lint (Spectral) ```bash npx spectral lint api.yaml # 표준 / 일관성 검사 ``` ```yaml # .spectral.yml rules: operation-tag-defined: warn operation-success-response: error no-unresolved-refs: error ``` ### Diff (breaking change) ```bash npx oasdiff diff old.yaml new.yaml --breaking-only # CI 에서 PR 마다 ``` ### Code-first vs Schema-first ``` Schema-first: + Single source of truth + Multi-language (server / client) - Sync 어려움 (yaml 과 코드) Code-first: + TS type 가 진실 + Hot reload - 다른 언어 client = generate 필요 ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | TS only fullstack | ts-rest / oRPC / Hono RPC / tRPC | | 다양한 client 언어 | Schema-first OpenAPI | | Public API | OpenAPI + Scalar docs | | Mock first | Schema-first + Prism | | Strong type 일급 | Code-first | | 빠른 prototype | Code-first | ## ❌ 안티패턴 - **Spec 과 코드 drift**: schema-first 의 위험. CI 에서 검증. - **모든 endpoint 200 만 명시**: 4xx / 5xx 같이. - **Schema 안 example**: 사용자 모름. - **Auth 누락 (security scheme)**: 명시. - **Generated client 직접 변경**: 다음 generate 시 잃음. - **OpenAPI 안 서버 검증**: client 만 — server bypass 가능. - **Versioning 없는 spec 변경**: breaking. ## 🤖 LLM 활용 힌트 - TS = ts-rest / Hono RPC. - 다중 언어 = OpenAPI yaml + generators. - Spectral lint + oasdiff CI. ## 🔗 관련 문서 - [[API_REST_Best_Practices]] - [[API_Versioning_Strategies]] - [[TS_Schema_Validation_Comparison]]