plugins/ai-registry/common/workflow-bundle/skills/api-conventions/SKILL.md
RESTful API 설계 컨벤션. URL 설계, HTTP 메서드, 상태 코드, Response 형식, 에러 코드를 정의합니다. Use when designing API endpoints, implementing controllers, or reviewing API implementations.
npx skillsauth add onejaejae/skills api-conventionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
API 설계 시 참조하는 컨벤션입니다.
/api/{resource} # 복수형 명사 (버전 없음)
/api/{resource}/{id} # 단일 리소스
| 동작 | URL | HTTP 메서드 |
|------|-----|------------|
| 목록 조회 | /api/researches | GET |
| 단일 조회 | /api/researches/{research_id} | GET |
| 생성 | /api/researches | POST |
| 수정 | /api/researches/{research_id} | PATCH |
| 삭제 | /api/researches/{research_id} | DELETE |
| 메서드 | 용도 | 상태코드 | |--------|------|----------| | GET | 조회 | 200 | | POST | 생성 | 201 | | PATCH | 수정 | 200 | | DELETE | 삭제 | 200 또는 204 |
| 코드 | 의미 | |------|------| | 200 | OK (조회/수정 성공) | | 201 | Created (생성 성공) | | 400 | Bad Request (유효성 검사 실패) | | 401 | Unauthorized (인증 필요) | | 403 | Forbidden (권한 없음) | | 404 | Not Found (리소스 없음) | | 409 | Conflict (중복) | | 500 | Internal Server Error |
# 스키마 정의 (src/schemas/common.py)
class Meta(BaseModel):
pass
class CommonResponse(BaseModel, Generic[T, M]):
data: T
meta: M
{
"data": { ... },
"meta": {}
}
# 스키마 정의 (src/schemas/error.py)
class ErrorSchema(BaseModel):
status_code: int
message: str
error_code: str
class ErrorResponse(BaseModel):
error: ErrorSchema
{
"error": {
"status_code": 404,
"message": "Research with id 1 not found",
"error_code": "RESEARCH_NOT_FOUND"
}
}
{DOMAIN}_{ERROR_TYPE}
예시:
AUTH_INVALID_CREDENTIALS - 인증 실패AUTH_TOKEN_EXPIRED - 토큰 만료RESEARCH_NOT_FOUND - 연구 없음VALIDATION_ERROR - 입력값 검증 실패| HTTP 상태 | 예외 클래스 | 사용 예시 | |-----------|-------------|-----------| | 400 | ValidationCommonException | 입력값 검증 실패 | | 401 | UnauthorizedCommonException | 인증 실패 | | 403 | PermissionCommonException | 권한 없음 | | 404 | NotFoundCommonException | 리소스 없음 | | 503 | ServiceUnavailableCommonException | 외부 서비스 장애 |
# src/exceptions/research.py
from src.exceptions.common import NotFoundCommonException
class ResearchNotFoundException(NotFoundCommonException):
pass
# src/controllers/research.py
from typing import Annotated
from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Body, Depends, Path, status
from sqlalchemy.ext.asyncio import AsyncSession
from src.containers import Container
from src.database import database
from src.schemas.common import CommonResponse, Meta
from src.schemas.error import COMMON_ERROR_RESPONSES
from src.schemas.research import ResearchCreate, ResearchResponse
from src.services.research_service import ResearchService
from src.utils.auth import CurrentUserId, RequireAuth
research_router = APIRouter(prefix="/api/researches", tags=["Research"])
@research_router.get(
"/{research_id}",
summary="연구 조회 API",
description="특정 연구 정보를 조회합니다.",
status_code=status.HTTP_200_OK,
responses=COMMON_ERROR_RESPONSES,
)
@inject
async def get_research(
_: RequireAuth,
research_id: Annotated[
int, Path(title="연구 ID", description="조회할 연구의 고유 식별자")
],
session: Annotated[AsyncSession, Depends(database.get_async_session)],
service: Annotated[ResearchService, Depends(Provide[Container.research_service])],
) -> CommonResponse[ResearchResponse, Meta]:
result = await service.get(research_id, session=session)
return CommonResponse(data=result, meta=Meta())
@research_router.post(
"",
summary="연구 생성 API",
status_code=status.HTTP_201_CREATED,
responses=COMMON_ERROR_RESPONSES,
)
@inject
async def create_research(
user_id: CurrentUserId,
request: Annotated[ResearchCreate, Body()],
session: Annotated[AsyncSession, Depends(database.get_async_session)],
service: Annotated[ResearchService, Depends(Provide[Container.research_service])],
) -> CommonResponse[ResearchResponse, Meta]:
result = await service.create(
title=request.title,
project_id=request.project_id,
created_by_id=user_id,
session=session,
)
return CommonResponse(data=result, meta=Meta())
# src/services/research_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from src.repositories.research_repository import ResearchRepository
from src.schemas.research import ResearchResponse
class ResearchService:
def __init__(
self,
*,
research_repository: ResearchRepository,
):
self.research_repository = research_repository
async def get(self, research_id: int, *, session: AsyncSession) -> ResearchResponse:
research = await self.research_repository.get_or_throw(
research_id, session=session
)
return ResearchResponse.model_validate(research)
# src/schemas/research.py
from pydantic import BaseModel, ConfigDict, Field
from src.schemas.common import TimestampInfo
# Request
class ResearchCreate(BaseModel):
title: str = Field(..., description="연구 제목")
project_id: int | None = Field(None, description="프로젝트 ID")
class ResearchUpdate(BaseModel):
title: str = Field(..., description="연구 제목")
# Response
class ResearchResponse(TimestampInfo):
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="연구 ID")
title: str = Field(..., description="연구 제목")
project_id: int | None = Field(None, description="프로젝트 ID")
created_by_id: int | None = Field(None, description="생성자 ID")
# src/repositories/research_repository.py
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.exceptions.research import ResearchNotFoundException
from src.models.research import Research
class ResearchRepository:
async def get(self, research_id: int, *, session: AsyncSession) -> Research | None:
stmt = select(Research).where(
Research.id == research_id,
Research.deleted_at.is_(None), # Soft Delete 필터
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_or_throw(
self,
research_id: int,
*,
session: AsyncSession,
) -> Research:
research = await self.get(research_id, session=session)
if research is None:
raise ResearchNotFoundException(f"Research with id {research_id} not found")
return research
# 성공
CommonResponse(data=result, meta=Meta())
# 에러 (자동 처리)
raise ResearchNotFoundException(f"Research with id {id} not found")
# 인증만 필요 (사용자 ID 불필요)
_: RequireAuth
# 사용자 ID 필요
user_id: CurrentUserId
/api/researches)CommonResponse[T, Meta] 인가?responses=COMMON_ERROR_RESPONSES 포함되었는가?@inject 데코레이터가 있는가?Annotated[Type, Depends(...)] 형식인가?testing
CLAUDE.md 기반 환경 안전 체크. 작업 시작 전에 프로젝트의 안전 규칙, 컨벤션, 환경 설정을 자동 검증하여 CLEAR/WARNING/BLOCKED 상태를 보고한다. /check가 "변경 후 검증"이라면, /pre-flight는 "작업 전 환경 검증"이다. Use PROACTIVELY before starting work, especially after switching branches, pulling changes, or resuming a session. Also use when explicitly asked: "/pre-flight", "프리플라이트", "환경 체크", "작업 전 점검", "안전 체크", "environment check", "pre-flight check", "시작해도 돼?", "환경 괜찮아?", "safety check", "DB 확인", "설정 확인", "config check".
tools
PR 리뷰 워크플로우와 체크리스트를 제공하는 스킬. "PR 리뷰해줘", "코드 리뷰 해줘", "이 PR 봐줘", "review this PR" 등 PR 리뷰 요청 시 사용. GitHub/GitLab PR URL 또는 로컬 브랜치 diff를 기반으로 체계적이고 일관된 리뷰를 수행. 코드 품질, 안정성/보안, 성능, 테스트, 문서화 관점에서 건설적인 피드백 제공.
documentation
PR review comments를 체계적으로 처리하는 skill. Use when: (1) PR에 동료의 리뷰가 달렸을 때, (2) 여러 리뷰를 한 번에 처리하고 싶을 때, (3) 수정 후 commit 링크가 포함된 reply를 자동으로 추가하고 싶을 때
tools
PR diff를 받아 코드 리뷰 자동 요약을 생성하는 스킬. 핵심 변경점을 3줄로 요약하고, 변경 파일별로 what changed / why it matters / risk level을 정리. Use when: "PR 요약", "diff 요약", "PR 변경점 정리", "코드 변경 요약", "summarize PR", "PR summary", "diff summary", "what changed in this PR", "변경점 요약해줘", "PR 핵심 정리", "리뷰 요약"