REST API 설계 베스트 프랙티스 2026 — 실무 즉시 적용 가이드
실무에서 바로 적용할 수 있는 REST API 설계 원칙과 패턴 완전 가이드. URL 설계, HTTP 메서드, 상태 코드, 에러 응답, 페이지네이션, 버전 관리, 인증, Rate Limiting, OpenAPI 문서화까지 정리했습니다.
좋은 REST API는 그냥 만들어지지 않습니다
REST API는 팀원이 사용하고, 프론트엔드가 의존하며, 클라이언트가 통합합니다. 설계가 나쁘면 모든 소비자가 고통받습니다. 명확한 URL, 일관된 응답 형식, 적절한 상태 코드가 좋은 API의 기반입니다.
URL 설계 원칙
명사 사용, 동사 금지
HTTP 메서드가 동사 역할을 합니다.
Good: GET /users
Bad: GET /getUsers
Good: POST /orders
Bad: POST /createOrder
Good: DELETE /posts/123
Bad: DELETE /deletePost/123
복수형 사용
리소스 컬렉션은 항상 복수형:
Good: /users, /posts, /comments, /orders
Bad: /user, /post, /comment, /order
계층 관계 표현
GET /users/123/posts → 사용자 123의 게시글 목록
GET /users/123/posts/456 → 사용자 123의 게시글 456
POST /users/123/posts → 사용자 123에 게시글 생성
DELETE /users/123/posts/456 → 사용자 123의 게시글 456 삭제
주의: 중첩 깊이는 2단계가 한계입니다. 그 이상은 URL이 복잡해져 관리하기 어렵습니다.
URL 표기 규칙
Good: /user-profiles, /blog-posts, /order-items
Bad: /userProfiles (camelCase)
Bad: /user_profiles (snake_case)
Bad: /UserProfiles (PascalCase)
HTTP 메서드 올바른 사용
| 메서드 | 용도 | 멱등성 | 안전성 | 요청 바디 |
|---|---|---|---|---|
| GET | 조회 | O | O | 없음 |
| POST | 생성 | X | X | 있음 |
| PUT | 전체 교체 | O | X | 있음 |
| PATCH | 부분 수정 | O | X | 있음 |
| DELETE | 삭제 | O | X | 없음 |
멱등성: 같은 요청을 여러 번 해도 결과가 동일함
안전성: 서버 상태를 변경하지 않음
PUT vs PATCH
// 기존 사용자 데이터
{ "name": "홍길동", "email": "hong@example.com", "role": "user" }
// PUT: 전체 교체 (포함하지 않은 필드는 초기화)
PUT /users/123
{ "name": "홍길동", "email": "new@example.com" }
// 결과: { "name": "홍길동", "email": "new@example.com", "role": null }
// PATCH: 부분 수정 (포함한 필드만 변경)
PATCH /users/123
{ "email": "new@example.com" }
// 결과: { "name": "홍길동", "email": "new@example.com", "role": "user" }
HTTP 상태 코드
성공 응답
| 코드 | 의미 | 사용 예 |
|---|---|---|
| 200 OK | 성공 | GET, PUT, PATCH 성공 |
| 201 Created | 생성 완료 | POST로 리소스 생성 성공 |
| 204 No Content | 성공 (바디 없음) | DELETE 성공 |
| 206 Partial Content | 부분 응답 | 큰 파일 분할 전송 |
클라이언트 에러
| 코드 | 의미 | 사용 예 |
|---|---|---|
| 400 Bad Request | 잘못된 요청 | 필수 파라미터 누락 |
| 401 Unauthorized | 인증 필요 | 로그인하지 않은 접근 |
| 403 Forbidden | 권한 없음 | 다른 사용자 데이터 접근 |
| 404 Not Found | 리소스 없음 | 존재하지 않는 ID |
| 409 Conflict | 충돌 | 이메일 중복 가입 |
| 422 Unprocessable Entity | 검증 실패 | 이메일 형식 오류 |
| 429 Too Many Requests | Rate limit 초과 | — |
서버 에러
| 코드 | 의미 |
|---|---|
| 500 Internal Server Error | 서버 내부 오류 |
| 502 Bad Gateway | 업스트림 서버 오류 |
| 503 Service Unavailable | 서비스 일시 불가 |
일관된 응답 형식
성공 응답
// 단일 리소스
{
"data": {
"id": "123",
"name": "홍길동",
"email": "hong@example.com",
"createdAt": "2026-01-15T09:00:00Z"
}
}
// 컬렉션
{
"data": [...],
"pagination": {
"nextCursor": "cursor_abc",
"hasMore": true
}
}
에러 응답
{
"error": {
"code": "VALIDATION_ERROR",
"message": "입력값이 올바르지 않습니다.",
"details": [
{
"field": "email",
"message": "유효한 이메일 주소를 입력하세요."
},
{
"field": "password",
"message": "비밀번호는 8자 이상이어야 합니다."
}
]
}
}
에러 코드 컨벤션:
VALIDATION_ERROR → 입력값 검증 실패
AUTHENTICATION_ERROR → 인증 실패
AUTHORIZATION_ERROR → 권한 없음
NOT_FOUND_ERROR → 리소스 없음
CONFLICT_ERROR → 충돌 (중복 등)
RATE_LIMIT_ERROR → Rate limit 초과
INTERNAL_ERROR → 서버 내부 오류
페이지네이션
커서 기반 (권장)
GET /posts?cursor=eyJpZCI6MTAwfQ&limit=20
응답:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true,
"limit": 20
}
}
커서 기반이 좋은 이유:
- 실시간 데이터 추가·삭제 시에도 결과 일관성 유지
- 대량 데이터에서 오프셋보다 훨씬 빠른 쿼리 성능
- 무한 스크롤 구현에 적합
오프셋 기반 (관리자 페이지 등)
GET /posts?page=2&limit=20
응답:
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 156,
"totalPages": 8
}
}
총 개수 조회가 필요한 관리자 UI나 페이지 번호가 필요한 경우 적합합니다.
필터링, 정렬, 필드 선택
# 필터링
GET /posts?status=published&category=tech&authorId=123
# 정렬 (- 접두사로 내림차순)
GET /posts?sort=-createdAt # 최신순
GET /posts?sort=title # 제목 오름차순
GET /posts?sort=-views,title # 조회수 내림차순, 제목 오름차순
# 필드 선택 (필요한 필드만)
GET /posts?fields=id,title,createdAt
# 검색
GET /posts?q=typescript
버전 관리
URL 경로 방식 (권장)
GET /api/v1/users
GET /api/v2/users
# 특정 리소스만 새 버전 적용 가능
GET /api/v1/users
GET /api/v2/payments
헤더 방식 (URL 깔끔하지만 복잡)
GET /api/users
Accept: application/vnd.myapp.v2+json
버전업 시점:
- 기존 응답 필드 제거 또는 이름 변경
- 인증 방식 변경
- 기존 동작과 호환되지 않는 변경
인증
Bearer Token (JWT)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// Next.js API Route에서 JWT 검증
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";
export async function GET(req: NextRequest) {
const token = req.headers.get("authorization")?.replace("Bearer ", "");
if (!token) {
return NextResponse.json(
{ error: { code: "AUTHENTICATION_ERROR", message: "인증이 필요합니다." } },
{ status: 401 }
);
}
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET!)
);
// payload.sub = userId
} catch {
return NextResponse.json(
{ error: { code: "AUTHENTICATION_ERROR", message: "유효하지 않은 토큰입니다." } },
{ status: 401 }
);
}
}
API Key
X-API-Key: your-api-key-here
주의: API 키는 쿼리 파라미터(?api_key=...)가 아닌 헤더에 넣으세요. URL은 서버 로그에 기록됩니다.
Rate Limiting
응답 헤더로 제한 정보를 전달합니다:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1740000000
Retry-After: 60 (429 응답 시)
// express-rate-limit 예시
import rateLimit from "express-rate-limit";
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 최대 100 요청
standardHeaders: true, // X-RateLimit 헤더 자동 포함
legacyHeaders: false,
message: {
error: {
code: "RATE_LIMIT_ERROR",
message: "요청이 너무 많습니다. 잠시 후 다시 시도하세요.",
},
},
});
OpenAPI (Swagger) 문서화
# openapi.yaml
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
summary: 사용자 목록 조회
parameters:
- name: cursor
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: 성공
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
실무 체크리스트
설계:
□ URL은 복수형 명사 사용 (동사 금지)
□ URL은 소문자 + 하이픈 (camelCase 금지)
□ 계층 관계는 최대 2단계
응답:
□ 모든 에러 응답이 일관된 형식을 따르는가?
□ 적절한 HTTP 상태 코드를 사용하는가?
□ 에러 코드(문자열)가 포함되어 있는가?
기능:
□ 페이지네이션 구현 (기본 limit 설정)
□ 정렬·필터링 지원
□ Rate Limiting 적용
보안:
□ 입력값 서버 사이드 검증
□ API 키는 헤더로 전달
□ 민감 정보(비밀번호 해시 등) 응답에서 제외
□ CORS 설정
문서:
□ OpenAPI/Swagger 문서 작성
□ 에러 코드 목록 문서화
□ 인증 방법 문서화
관련 글: Supabase 사이드프로젝트 시작하기 · TypeScript 실무 패턴 · GitHub Actions CI/CD 설정법
관련 글
TypeScript 실무 패턴 2026 — 바로 쓰는 고급 패턴 완전 가이드
실무에서 즉시 활용할 수 있는 TypeScript 고급 패턴을 코드 예제와 함께 완전 정리했습니다. Discriminated Union, 브랜드 타입, Zod 검증, 제네릭 패턴, 유틸리티 타입 활용까지 포함합니다.
Supabase로 사이드프로젝트 백엔드 구축하기 2026 — 무료로 시작하는 완전 가이드
Supabase로 사이드 프로젝트의 백엔드를 무료로 구축하는 방법. PostgreSQL, 인증, RLS, 실시간 구독, Edge Functions, Next.js 연동까지 단계별로 정리했습니다.
Turborepo 모노레포 가이드 2026 — pnpm 워크스페이스 실전 설정
Turborepo와 pnpm 워크스페이스로 모노레포를 구성하는 실전 가이드. 태스크 파이프라인, 원격 캐싱, 공유 패키지 설정까지 처음부터 단계별로 설명합니다.