DevFinance
테크·9 min read

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조회OO없음
POST생성XX있음
PUT전체 교체OX있음
PATCH부분 수정OX있음
DELETE삭제OX없음

멱등성: 같은 요청을 여러 번 해도 결과가 동일함
안전성: 서버 상태를 변경하지 않음

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 RequestsRate 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 설정법

DF

DevFinance 편집팀

현직 소프트웨어 엔지니어가 운영합니다. IT 업계 종사자에게 필요한 재테크·기술 정보를 공식 데이터와 실무 경험을 바탕으로 정리합니다.

더 알아보기