API Contract Agent. OpenAPI 스펙 정의, Mock Server 생성, Contract-First 개발을 담당합니다. Frontend/Backend 간 API 계약을 관리합니다.
56
45%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/api-contract/SKILL.mdContract-First 개발을 위한 API 계약(OpenAPI 스펙) 정의 및 관리를 담당합니다.
┌─────────────────────────────────────────────────────────────────┐
│ Contract-First 개발 흐름 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. API 계약 정의 (OpenAPI 3.0) │
│ │ │
│ ▼ │
│ 2. 계약 검증 및 합의 (Frontend ↔ Backend) │
│ │ │
│ ├──────────────┬──────────────┐ │
│ ▼ ▼ ▼ │
│ 3-A. Mock Server 3-B. Frontend 3-C. Backend │
│ 생성 개발 시작 개발 시작 │
│ (Mock 사용) (스펙 준수) │
│ │ │ │ │
│ └──────────────┴──────────────┘ │
│ │ │
│ ▼ │
│ 4. Contract Test로 검증 │
│ │
└─────────────────────────────────────────────────────────────────┘openapi: 3.0.3
info:
title: Feature API
description: 기능 설명
version: 1.0.0
servers:
- url: /api/v1
description: API Server
paths:
/resource:
get:
summary: 리소스 목록 조회
operationId: getResources
tags:
- Resource
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: 성공
content:
application/json:
schema:
$ref: '#/components/schemas/ResourceListResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
post:
summary: 리소스 생성
operationId: createResource
tags:
- Resource
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResourceDto'
responses:
'201':
description: 생성됨
content:
application/json:
schema:
$ref: '#/components/schemas/Resource'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
components:
schemas:
Resource:
type: object
required:
- id
- name
- createdAt
properties:
id:
type: string
format: uuid
name:
type: string
minLength: 1
maxLength: 100
description:
type: string
nullable: true
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateResourceDto:
type: object
required:
- name
properties:
name:
type: string
minLength: 1
maxLength: 100
description:
type: string
nullable: true
ResourceListResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Resource'
pagination:
$ref: '#/components/schemas/Pagination'
Pagination:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer
Error:
type: object
properties:
code:
type: string
message:
type: string
details:
type: array
items:
type: object
responses:
Unauthorized:
description: 인증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: UNAUTHORIZED
message: 인증이 필요합니다
ValidationError:
description: 유효성 검증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: VALIDATION_ERROR
message: 입력값이 올바르지 않습니다
InternalError:
description: 서버 오류
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: INTERNAL_ERROR
message: 서버 오류가 발생했습니다
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []# 설치
npm install -g @stoplight/prism-cli
# Mock Server 실행
prism mock api-contract.yaml -p 4010
# 동적 응답 생성
prism mock api-contract.yaml -p 4010 --dynamic// src/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
// GET /api/v1/resource
rest.get('/api/v1/resource', (req, res, ctx) => {
const page = Number(req.url.searchParams.get('page')) || 1;
const limit = Number(req.url.searchParams.get('limit')) || 20;
return res(
ctx.status(200),
ctx.json({
data: [
{
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'Sample Resource',
description: 'Description',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pagination: {
page,
limit,
total: 1,
totalPages: 1,
},
})
);
}),
// POST /api/v1/resource
rest.post('/api/v1/resource', async (req, res, ctx) => {
const body = await req.json();
if (!body.name) {
return res(
ctx.status(400),
ctx.json({
code: 'VALIDATION_ERROR',
message: 'name은 필수입니다',
})
);
}
return res(
ctx.status(201),
ctx.json({
id: '550e8400-e29b-41d4-a716-446655440001',
name: body.name,
description: body.description || null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
);
}),
];// consumer.pact.spec.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'Frontend',
provider: 'Backend',
});
describe('Resource API Contract', () => {
it('should get resources', async () => {
await provider
.given('resources exist')
.uponReceiving('a request for resources')
.withRequest({
method: 'GET',
path: '/api/v1/resource',
query: { page: '1', limit: '20' },
})
.willRespondWith({
status: 200,
body: MatchersV3.like({
data: MatchersV3.eachLike({
id: MatchersV3.uuid(),
name: MatchersV3.string('Sample'),
createdAt: MatchersV3.datetime(),
}),
pagination: {
page: 1,
limit: 20,
total: MatchersV3.integer(),
},
}),
});
await provider.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/api/v1/resource?page=1&limit=20`);
const data = await response.json();
expect(data.data).toBeDefined();
expect(data.pagination).toBeDefined();
});
});
});// provider.pact.spec.ts
import { Verifier } from '@pact-foundation/pact';
describe('Pact Verification', () => {
it('should validate contract', async () => {
const verifier = new Verifier({
providerBaseUrl: 'http://localhost:3000',
pactUrls: ['./pacts/frontend-backend.json'],
stateHandlers: {
'resources exist': async () => {
// 테스트 데이터 준비
},
},
});
await verifier.verifyProvider();
});
});# OpenAPI 스펙 검증
npx @redocly/cli lint api-contract.yaml
# 스펙 변경 감지 (Breaking Changes)
npx @redocly/cli diff old-spec.yaml new-spec.yaml
# 문서 생성
npx @redocly/cli build-docs api-contract.yaml# TypeScript 타입 생성
npx openapi-typescript api-contract.yaml -o src/types/api.d.ts
# API 클라이언트 생성
npx openapi-generator-cli generate -i api-contract.yaml -g typescript-axios -o src/apidocs/features/<기능명>/requirements/api-contract.yamlsrc/mocks/handlers/<기능명>.tspacts/src/types/api.d.ts9242c58
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.