
바이브 코딩으로 API 설계할 때, 반드시 챙겨야 할 것들
바이브 코딩으로 API 설계할 때, 반드시 챙겨야 할 것들

"게시글 CRUD 만들어줘" — AI에게 이렇게만 말하면, 동작하는 API를 금방 만들어줍니다. 하지만 그 API가 실제 서비스에서 써도 될 수준인지는 다른 문제입니다.
URL이 뒤죽박죽이면 프론트엔드 개발이 힘들어집니다. 에러 처리가 없으면 사용자가 흰 화면만 보게 됩니다. 검증이 없으면 잘못된 데이터가 DB에 들어갑니다.
이 글은 바이브 코딩으로 API를 만들 때, 프롬프트에 어떤 조건들을 넣어야 하는지, 그리고 왜 그 조건들이 필요한지를 설명합니다.
API란? 30초 정리
**API(Application Programming Interface)**는 프론트엔드와 백엔드가 데이터를 주고받는 약속입니다.
프론트엔드: "게시글 목록 줘" → GET /api/posts
백엔드: "여기 있어, 10개야" ← 200 OK + JSON 데이터
프론트엔드: "새 게시글 저장해줘" → POST /api/posts + {title, content}
백엔드: "저장했어, ID는 42야" ← 201 Created + {id: 42}
비유하자면 레스토랑의 주문서입니다. 손님(프론트)이 주문서(API 요청)를 작성하면, 주방(백엔드)이 요리(데이터)를 만들어 내보냅니다. 주문서 양식이 엉망이면 주방도 혼란스러워집니다.
1. URL 설계 — API의 주소 체계
왜 중요한가?
URL은 API의 주소입니다. 주소가 일관성 없으면:
- 프론트엔드 개발자(또는 AI)가 혼란
- 어떤 URL이 뭘 하는지 추측해야 함
- 유지보수할 때 어디를 수정해야 하는지 모름
좋은 URL vs 나쁜 URL
❌ 나쁜 예시
GET /getPostList (동사 사용)
GET /post/1 (단수형)
POST /createNewPost (동사 중복)
GET /api/v1/getBlogPostsByCategory?cat=tech (길고 복잡)
✅ 좋은 예시
GET /api/posts (목록 조회)
GET /api/posts/1 (단건 조회)
POST /api/posts (생성)
PUT /api/posts/1 (수정)
DELETE /api/posts/1 (삭제)
GET /api/posts?category=tech (필터)
핵심 규칙
| 규칙 | 예시 | 이유 |
|---|---|---|
| 명사형 사용 | /posts (O), /getPost (X) | HTTP 메서드가 동사 역할을 함 |
| 복수형 사용 | /posts (O), /post (X) | 컬렉션을 표현 |
| 계층 구조 | /posts/1/comments | 게시글 1의 댓글들 |
| 소문자 + 하이픈 | /blog-posts (O), /blogPosts (X) | URL 표준 |
| 필터는 쿼리 파라미터 | /posts?status=draft | 경로와 필터 분리 |
프롬프트에 이렇게 넣으세요
API URL 설계 규칙:
- RESTful 명사형 복수 리소스 (예: /api/posts, /api/users)
- 중첩 리소스는 계층 구조 (예: /api/posts/:id/comments)
- 필터, 정렬, 검색은 쿼리 파라미터 (예: /api/posts?category=tech&sort=latest)
- 소문자 + 하이픈 사용
2. HTTP 메서드 — 요청의 종류
왜 중요한가?
HTTP 메서드는 **"이 요청이 뭘 하려는 건지"**를 나타냅니다. 메서드를 잘못 쓰면 의미가 혼란스러워집니다.
5가지 핵심 메서드
| 메서드 | 역할 | 비유 | 예시 |
|---|---|---|---|
| GET | 조회 | "보여줘" | 게시글 목록, 상세 조회 |
| POST | 생성 | "새로 만들어줘" | 게시글 작성, 회원가입 |
| PUT | 전체 수정 | "이걸로 바꿔줘" | 게시글 전체 수정 |
| PATCH | 부분 수정 | "이 부분만 고쳐줘" | 제목만 변경, 상태 변경 |
| DELETE | 삭제 | "지워줘" | 게시글 삭제 |
자주 하는 실수
❌ POST /api/deletePost/1 (POST로 삭제)
❌ GET /api/posts/create (GET으로 생성)
❌ POST /api/posts/1/update (URL에 동사)
✅ DELETE /api/posts/1 (DELETE 메서드 사용)
✅ POST /api/posts (POST로 생성)
✅ PUT /api/posts/1 (PUT으로 수정)
프롬프트에 이렇게 넣으세요
HTTP 메서드 규칙:
- GET: 조회 (목록, 단건)
- POST: 생성
- PUT: 전체 수정
- PATCH: 부분 수정
- DELETE: 삭제
- URL에 동사를 넣지 마 (create, update, delete 등)
3. 응답 형식 통일 — 프론트가 편해야 한다
왜 중요한가?
API마다 응답 형식이 다르면, 프론트엔드에서 매번 다른 방식으로 데이터를 파싱해야 합니다.
❌ 나쁜 예시 — API마다 형식이 다름
GET /api/posts → { posts: [...] }
GET /api/users → { data: { users: [...] } }
GET /api/comments → [...]
✅ 좋은 예시 — 통일된 형식
GET /api/posts → { success: true, data: [...], meta: { total: 50, page: 1 } }
GET /api/users → { success: true, data: [...], meta: { total: 20, page: 1 } }
GET /api/posts/1 → { success: true, data: { id: 1, title: "..." } }
통일된 응답 구조
// 성공
{
"success": true,
"data": { ... },
"meta": {
"total": 50,
"page": 1,
"limit": 10,
"totalPages": 5
}
}
// 에러
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "제목은 필수입니다.",
"details": [
{ "field": "title", "message": "필수 항목입니다" }
]
}
}
프롬프트에 이렇게 넣으세요
모든 API 응답은 통일된 형식으로:
성공: { success: true, data: {...}, meta?: {...} }
에러: { success: false, error: { code, message, details? } }
목록 조회 시 meta에 total, page, limit, totalPages 포함.
4. 상태 코드 — 결과를 숫자로 알려주기
왜 중요한가?
상태 코드는 "이 요청이 성공했는지, 실패했다면 왜 실패했는지"를 숫자 하나로 알려줍니다. 프론트엔드는 이 숫자를 보고 어떻게 처리할지 결정합니다.
꼭 알아야 할 상태 코드
| 코드 | 의미 | 언제 쓰나? |
|---|---|---|
| 200 | 성공 | 조회, 수정 성공 |
| 201 | 생성됨 | POST로 새 리소스 생성 성공 |
| 204 | 내용 없음 | DELETE 성공 (응답 body 없음) |
| 400 | 잘못된 요청 | 입력값 검증 실패 |
| 401 | 인증 필요 | 로그인 안 했거나 토큰 만료 |
| 403 | 권한 없음 | 로그인은 했지만 권한 부족 |
| 404 | 찾을 수 없음 | 해당 리소스가 없음 |
| 409 | 충돌 | 이미 존재하는 데이터 (중복 이메일 등) |
| 422 | 처리 불가 | 형식은 맞지만 비즈니스 규칙 위반 |
| 429 | 요청 과다 | Rate limit 초과 |
| 500 | 서버 에러 | 서버 코드 버그 |
자주 하는 실수
❌ 모든 에러에 200 반환 + body에 error 표시
→ 프론트에서 매번 body를 파싱해야 함
❌ 모든 에러에 500 반환
→ 사용자 입력 오류인지 서버 버그인지 구분 불가
✅ 상황에 맞는 코드 사용
입력 오류 → 400
인증 필요 → 401
서버 버그 → 500
프롬프트에 이렇게 넣으세요
HTTP 상태 코드 사용 규칙:
- 200: 조회/수정 성공
- 201: 생성 성공
- 204: 삭제 성공
- 400: 입력값 검증 실패
- 401: 인증 필요 (토큰 없음/만료)
- 403: 권한 없음
- 404: 리소스 없음
- 409: 중복 데이터
- 429: Rate limit 초과
- 500: 서버 에러 (절대 클라이언트에 스택트레이스 노출 금지)
5. 입력값 검증 — 잘못된 데이터를 걸러내기
왜 중요한가?
검증 없이 데이터를 그대로 DB에 넣으면:
- 빈 제목의 게시글이 생성됨
- 이메일 형식이 아닌 문자열이 이메일 필드에 저장됨
- 음수 가격의 상품이 등록됨
- 악의적인 스크립트가 저장됨 (XSS)
사용자 입력은 절대 신뢰하면 안 됩니다.
검증 계층
1단계: 프론트엔드 검증 (UX용 — 빠른 피드백)
→ "이메일 형식이 아닙니다" 실시간 표시
2단계: API 검증 (필수 — 보안)
→ 프론트를 우회한 요청도 반드시 검증
3단계: DB 제약조건 (최후의 보루)
→ NOT NULL, UNIQUE, CHECK 등
검증해야 할 항목
| 항목 | 검증 내용 |
|---|---|
| 필수값 | title, email 등 빈 값 체크 |
| 타입 | 숫자 필드에 문자열이 오면 거부 |
| 길이 | 제목 100자 이하, 내용 10000자 이하 |
| 형식 | 이메일, 전화번호, URL 패턴 검증 |
| 범위 | 가격 0 이상, 수량 1~999 |
| 허용값 | status는 "draft", "published" 중 하나 |
| XSS 방지 | HTML 태그 제거 또는 이스케이프 |
프롬프트에 이렇게 넣으세요
입력값 검증 규칙:
- 모든 API에 입력값 검증 미들웨어 적용 (zod 또는 class-validator)
- 필수값, 타입, 길이, 형식, 범위 검증
- 검증 실패 시 400 응답 + 어떤 필드가 왜 실패했는지 상세 에러 반환
- XSS 방지: 사용자 입력 HTML 이스케이프
- SQL Injection 방지: ORM/Prepared Statement 사용 (raw query 금지)
예시:
POST /api/posts 검증:
- title: 필수, string, 1~200자
- content: 필수, string, 1~50000자
- categoryId: 선택, UUID 형식
- tags: 선택, string[], 최대 10개, 각 20자 이하
6. 페이지네이션 — 데이터를 나눠서 보내기
왜 중요한가?
게시글이 10,000개인데 한 번에 전부 보내면?
- 응답 시간 5초 이상 (사용자 이탈)
- 메모리 폭발 (서버 다운)
- 모바일에서 데이터 과다 소비
반드시 나눠서 보내야 합니다.
페이지네이션 방식
| 방식 | URL | 장점 | 단점 |
|---|---|---|---|
| Offset | ?page=2&limit=10 | 직관적, 페이지 이동 | 대량 데이터 시 느림 |
| Cursor | ?cursor=abc123&limit=10 | 대량 데이터에 빠름 | 페이지 번호 없음 |
프롬프트에 이렇게 넣으세요
목록 조회 API에는 반드시 페이지네이션 적용:
- 쿼리 파라미터: page (기본 1), limit (기본 10, 최대 100)
- 응답 meta에 포함: total, page, limit, totalPages
- limit 최대값 제한 (100) — 클라이언트가 limit=999999 요청 방지
예시: GET /api/posts?page=2&limit=10
응답 meta: { total: 150, page: 2, limit: 10, totalPages: 15 }
7. 에러 처리 — 에러도 API의 일부
왜 중요한가?
에러 응답이 잘 설계되면:
- 프론트엔드가 사용자에게 적절한 메시지를 보여줄 수 있음
- 개발자가 원인을 빠르게 파악할 수 있음
- 공격자에게 서버 정보를 노출하지 않음
에러 응답 설계
// 검증 에러 (400)
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "입력값을 확인해주세요.",
"details": [
{ "field": "title", "message": "제목은 필수입니다." },
{ "field": "email", "message": "올바른 이메일 형식이 아닙니다." }
]
}
}
// 권한 에러 (403)
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "이 작업을 수행할 권한이 없습니다."
}
}
// 서버 에러 (500)
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "서버 오류가 발생했습니다."
}
}
// ⚠️ 절대 스택트레이스나 DB 에러 메시지를 포함하지 않음
프롬프트에 이렇게 넣으세요
에러 처리 규칙:
- 글로벌 에러 핸들러 구현 (모든 API에 적용)
- 에러 응답 형식 통일: { success: false, error: { code, message, details? } }
- 검증 에러(400): 어떤 필드가 왜 잘못됐는지 상세히
- 서버 에러(500): 내부 정보 절대 노출 금지 (로그에만 기록)
- 예상치 못한 에러도 반드시 catch → 500 반환 (서버 크래시 방지)
8. Rate Limiting — 과도한 요청 차단
왜 중요한가?
Rate Limiting이 없으면:
- 악의적 사용자가 초당 수천 건 요청 (DDoS)
- 크롤러가 API를 무한 호출
- 서버 리소스 고갈 → 전체 서비스 다운
프롬프트에 이렇게 넣으세요
Rate Limiting 규칙:
- 일반 API: IP당 분당 60건
- 인증 API (로그인): IP당 분당 10건
- 파일 업로드: IP당 분당 5건
- 제한 초과 시 429 응답 + Retry-After 헤더
- X-RateLimit-Remaining 헤더로 남은 횟수 전달
9. 정렬과 검색 — 데이터를 원하는 순서로
프롬프트에 이렇게 넣으세요
정렬과 검색 규칙:
- 정렬: ?sort=createdAt&order=desc (기본: 최신순)
- 검색: ?search=키워드 (제목, 내용에서 검색)
- 필터: ?category=tech&status=published
- 허용된 정렬 필드만 사용 가능 (createdAt, title, viewCount 등)
- 허용되지 않은 필드로 정렬 요청 시 무시하고 기본값 적용
완성 프롬프트
아래 프롬프트를 AI에게 전달하면, 위의 모든 조건이 반영된 API를 설계할 수 있습니다.
Next.js 버전
블로그 API를 설계하고 구현해줘.
[기술 스택]
Next.js App Router + TypeScript + PostgreSQL + Prisma + zod
[리소스]
- posts: 게시글 (title, content, slug, categoryId, tags[], published, createdAt)
- categories: 카테고리 (name, slug)
- comments: 댓글 (postId, author, content, createdAt)
[URL 설계]
RESTful 명사형 복수, 중첩 리소스:
- GET /api/posts (목록, 페이지네이션)
- GET /api/posts/:slug (단건)
- POST /api/posts (생성, 인증 필요)
- PUT /api/posts/:id (수정, 인증 필요)
- DELETE /api/posts/:id (삭제, 인증 필요)
- GET /api/posts/:id/comments (댓글 목록)
- POST /api/posts/:id/comments (댓글 작성)
- GET /api/categories (카테고리 목록)
[응답 형식]
통일: { success: true, data, meta? } / { success: false, error: { code, message, details? } }
목록 meta: { total, page, limit, totalPages }
[입력값 검증]
zod로 모든 요청 body/query 검증.
posts 생성: title(필수, 1~200자), content(필수, 1~50000자), categoryId(선택, UUID), tags(선택, 최대 10개)
[페이지네이션]
?page=1&limit=10, limit 최대 100
[정렬/검색/필터]
?sort=createdAt&order=desc&search=키워드&category=tech&published=true
[에러 처리]
글로벌 에러 핸들러. 400(검증 실패), 401(인증), 403(권한), 404(없음), 409(중복), 429(Rate limit), 500(서버 에러).
500 에러 시 내부 정보 노출 금지.
[Rate Limiting]
일반 API: IP당 분당 60건. 인증 API: 분당 10건. 초과 시 429 + Retry-After.
[보안]
- SQL Injection: Prisma ORM 사용 (raw query 금지)
- XSS: 사용자 입력 sanitize
- 인증된 사용자만 POST/PUT/DELETE 가능
Spring Boot 버전
블로그 API를 설계하고 구현해줘.
[기술 스택]
Spring Boot 3.x + Java 17 + PostgreSQL + Spring Data JPA + Bean Validation
[리소스]
- Post: 게시글 (title, content, slug, categoryId, tags, published, createdAt)
- Category: 카테고리 (name, slug)
- Comment: 댓글 (postId, author, content, createdAt)
[URL 설계]
RESTful 명사형 복수:
- GET /api/posts (목록, Pageable)
- GET /api/posts/{slug} (단건)
- POST /api/posts (생성, @PreAuthorize)
- PUT /api/posts/{id} (수정, @PreAuthorize)
- DELETE /api/posts/{id} (삭제, @PreAuthorize)
- GET /api/posts/{id}/comments
- POST /api/posts/{id}/comments
- GET /api/categories
[응답 형식]
ApiResponse<T> 래퍼: { success, data, meta?, error? }
PageMeta: { total, page, limit, totalPages }
@RestControllerAdvice로 글로벌 예외 처리
[입력값 검증]
@Valid + DTO:
PostCreateRequest: @NotBlank title(@Size max=200), @NotBlank content(@Size max=50000), categoryId(UUID), tags(List, @Size max=10)
[페이지네이션]
Spring Pageable: ?page=0&size=10&sort=createdAt,desc. size 최대 100.
[에러 처리]
@RestControllerAdvice:
- MethodArgumentNotValidException → 400
- EntityNotFoundException → 404
- DataIntegrityViolationException → 409
- AccessDeniedException → 403
- 기타 → 500 (스택트레이스 노출 금지)
[Rate Limiting]
bucket4j 또는 커스텀 필터. 일반 분당 60건, 인증 분당 10건. 429 + Retry-After.
[보안]
- JPA로 파라미터 바인딩 (JPQL 직접 작성 시 :param 사용)
- XSS: 입력 sanitize
- @PreAuthorize("isAuthenticated()") 적용
프롬프트를 줄 때 핵심 원칙
1. CRUD만 요청하지 마세요
"게시글 CRUD 만들어줘"는 최소한의 기능만 만들어줍니다. 검증, 에러 처리, 페이지네이션, 보안은 명시하지 않으면 빠집니다.
2. 응답 형식을 먼저 정하세요
프론트엔드와 백엔드의 계약서가 API입니다. 응답 형식이 통일되면 프론트엔드 작업이 훨씬 수월해집니다.
3. 에러 상황을 생각하세요
정상 동작만 생각하면 안 됩니다. "없는 게시글을 조회하면?", "빈 제목을 보내면?", "권한 없는 사용자가 삭제하면?" — 이런 상황을 프롬프트에 넣어야 합니다.
4. 한 번에 전체 구조를 잡으세요
나중에 응답 형식을 바꾸면 모든 API와 프론트엔드를 수정해야 합니다. 처음부터 통일된 구조로 시작하는 것이 훨씬 효율적입니다.
마치며
API는 서비스의 골격입니다. 골격이 튼튼해야 위에 살을 붙일 수 있습니다.
바이브 코딩에서 AI는 코드를 잘 만들어줍니다. 하지만 어떤 구조로 만들어야 하는지는 당신이 결정해야 합니다. 이 글의 체크리스트를 프롬프트에 넣으면, AI가 실서비스 수준의 API를 만들어줍니다.

