| name | api-contract-design |
| description | Design APIs using schema-first approach with OpenAPI/Swagger. Use when creating new APIs, documenting existing ones, or when frontend/backend teams need to work in parallel. Covers OpenAPI spec, validation, and code generation. |
| allowed-tools | Read, Glob, Grep, Edit, Write, Bash |
| license | MIT |
| metadata | {"author":"antigravity-team","version":"1.0"} |
API Contract Design
OpenAPI(Swagger) ๊ธฐ๋ฐ ์คํค๋ง ์ฐ์ API ์ค๊ณ ์คํฌ์
๋๋ค.
Core Principle
"์ฝ๋๋ณด๋ค ๊ณ์ฝ(Contract)์ด ๋จผ์ ๋ค."
"ํ๋ก ํธ์๋์ ๋ฐฑ์๋๊ฐ ๋์์ ๊ฐ๋ฐํ ์ ์๊ฒ API๋ฅผ ๋จผ์ ์ ์ํ๋ค."
Schema-First vs Code-First
| ์ ๊ทผ๋ฒ | ์ฅ์ | ๋จ์ |
|---|
| Schema-First (๊ถ์ฅ) | ๋ณ๋ ฌ ๊ฐ๋ฐ ๊ฐ๋ฅ, ๋ช
ํํ ๊ณ์ฝ | ์ด๊ธฐ ์ค๊ณ ์๊ฐ ํ์ |
| Code-First | ๋น ๋ฅธ ์์ | ๋ฌธ์์ ์ฝ๋ ๋ถ์ผ์น ์ํ |
OpenAPI ๊ธฐ๋ณธ ๊ตฌ์กฐ
openapi.yaml
openapi: 3.1.0
info:
title: My API
version: 1.0.0
description: API for My Application
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:3000/api
description: Development
paths:
/users:
get:
summary: Get all users
operationId: getUsers
tags:
- Users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a new user
operationId: createUser
tags:
- Users
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: Email already exists
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/users/{userId}:
get:
summary: Get user by ID
operationId: getUserById
tags:
- Users
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
User:
type: object
required:
- id
- email
- name
- createdAt
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
avatarUrl:
type: string
format: uri
nullable: true
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateUserRequest:
type: object
required:
- email
- name
- password
properties:
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
password:
type: string
minLength: 8
UserListResponse:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
Pagination:
type: object
required:
- page
- limit
- total
- totalPages
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer
Error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
details:
type: object
responses:
BadRequest:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []
ํด๋ ๊ตฌ์กฐ
api/
โโโ openapi.yaml # ๋ฉ์ธ ์คํ
โโโ paths/ # ์๋ํฌ์ธํธ๋ณ ๋ถ๋ฆฌ
โ โโโ users.yaml
โ โโโ posts.yaml
โ โโโ auth.yaml
โโโ schemas/ # ์คํค๋ง ๋ถ๋ฆฌ
โ โโโ user.yaml
โ โโโ post.yaml
โ โโโ common.yaml
โโโ generated/ # ์๋ ์์ฑ ์ฝ๋
โโโ types.ts
โโโ client.ts
๋ถ๋ฆฌ๋ ์คํ (paths/users.yaml)
/users:
get:
$ref: '../operations/users/getUsers.yaml'
post:
$ref: '../operations/users/createUser.yaml'
๋ฉ์ธ ์คํ์์ ์ฐธ์กฐ
paths:
/users:
$ref: './paths/users.yaml#/~1users'
TypeScript ํ์
์์ฑ
openapi-typescript
npm install -D openapi-typescript
npx openapi-typescript ./api/openapi.yaml -o ./src/types/api.ts
์์ฑ๋ ํ์
์ฌ์ฉ
import type { paths, components } from './types/api';
type User = components['schemas']['User'];
type CreateUserRequest = components['schemas']['CreateUserRequest'];
type GetUsersResponse = paths['/users']['get']['responses']['200']['content']['application/json'];
API ํด๋ผ์ด์ธํธ ์์ฑ
openapi-fetch (๊ถ์ฅ)
npm install openapi-fetch
import createClient from 'openapi-fetch';
import type { paths } from './types/api';
export const api = createClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_URL,
});
const { data, error } = await api.GET('/users', {
params: {
query: { page: 1, limit: 20 },
},
});
const { data: user } = await api.POST('/users', {
body: {
email: 'user@example.com',
name: 'John',
password: 'password123',
},
});
Orval (์ฝ๋ ์์ฑ)
npm install -D orval
export default {
api: {
input: './api/openapi.yaml',
output: {
mode: 'tags-split',
target: './src/api',
schemas: './src/api/schemas',
client: 'react-query',
},
},
};
์์ฒญ ๊ฒ์ฆ
Zod + OpenAPI
import { z } from 'zod';
export const CreateUserRequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
});
export async function POST(request: Request) {
const body = await request.json();
const result = CreateUserRequestSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ code: 'VALIDATION_ERROR', message: result.error.message },
{ status: 400 }
);
}
const user = await createUser(result.);
.(user, { : });
}
API ๋ฌธ์ UI
Swagger UI
npm install swagger-ui-react
'use client';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
export default function ApiDocs() {
return <SwaggerUI url="/api/openapi.yaml" />;
}
Scalar (๋ชจ๋ ๋์)
npm install @scalar/nextjs-api-reference
import { ApiReference } from '@scalar/nextjs-api-reference';
export default function ApiDocs() {
return (
<ApiReference
configuration={{
spec: {
url: '/api/openapi.yaml',
},
}}
/>
);
}
๋ฒ์ ๊ด๋ฆฌ
URL ๋ฒ์ ๊ด๋ฆฌ
servers:
- url: https://api.example.com/v1
- url: https://api.example.com/v2
ํค๋ ๋ฒ์ ๊ด๋ฆฌ
parameters:
- name: API-Version
in: header
schema:
type: string
enum: ['2024-01-01', '2024-06-01']
Workflow
Schema-First ๊ฐ๋ฐ ํ๋ฆ
1. API ์คํ ์์ฑ (openapi.yaml)
โ
2. ํ ๋ฆฌ๋ทฐ (PR)
โ
3. ํ์
์์ฑ (openapi-typescript)
โ
4. ๋ณ๋ ฌ ๊ฐ๋ฐ
- Frontend: Mock ์๋ฒ๋ก ๊ฐ๋ฐ
- Backend: ์คํ ๊ธฐ๋ฐ ๊ตฌํ
โ
5. ํตํฉ ํ
์คํธ
Mock ์๋ฒ
npm install -D @stoplight/prism-cli
npx prism mock ./api/openapi.yaml
Checklist
์คํ ์์ฑ
ํ์
์์ ์ฑ
๋ฌธ์ํ
References