InfoGrab DocsInfoGrab Docs

API 스타일 가이드

요약

이 스타일 가이드는 API 개발에 대한 모범 사례를 권장합니다. 고객에게 두 가지 유형의 API를 제공합니다: 두 API를 병렬로 지원하는 기술적 부담을 줄이기 위해, 가능한 한 많은 구현을 공유해야 합니다. 프론트엔드에서 개발할 때 어떤 API를 사용할지에 대한 자세한 내용은 프론트엔드 가이드를 참고합니다.

이 스타일 가이드는 API 개발에 대한 모범 사례를 권장합니다.

GraphQL 및 REST API#

고객에게 두 가지 유형의 API를 제공합니다:

두 API를 병렬로 지원하는 기술적 부담을 줄이기 위해, 가능한 한 많은 구현을 공유해야 합니다. 예를 들어, 동일한 서비스를 공유할 수 있습니다.

프론트엔드#

프론트엔드에서 개발할 때 어떤 API를 사용할지에 대한 자세한 내용은 프론트엔드 가이드를 참고합니다.

인스턴스 변수#

인스턴스 변수는 사용하지 않습니다. 인스턴스 변수를 사용할 필요가 없으며(Rails 뷰에서처럼 접근할 필요가 없습니다), 로컬 변수로 충분합니다.

엔티티#

엔드포인트의 페이로드를 표현할 때는 항상 Entity를 사용합니다.

엔티티 필드 정의#

엔티티에서 노출하는 모든 필드는 유효한 타입을 포함하거나 참조해야 합니다.

다른 엔티티 참조#

다른 엔티티를 참조하는 필드를 노출할 때는 using 옵션을 사용합니다. using 옵션은 API::Entities 클래스를 가리키는 상수만 받습니다. 좋은 예는 다음과 같습니다.

  expose :project, using: ::API::Entities::BasicProjectDetails

유효한 필드 타입#

필드 타입은 문자열로 지정해야 합니다. 다음 타입을 사용할 수 있습니다:

카테고리 타입
스칼라 Integer, Float, BigDecimal, Numeric, Date, DateTime, Time, String, Symbol, Boolean
구조체 Hash, Array, Set
특수 JSON, File
엔티티 참조 문자열 형태의 임의의 API::Entities::* 클래스

필드 타입 정의#

필드 타입은 documentation 해시에 정의해야 합니다:

  expose :id, documentation: { type: 'Integer', example: 1 }
  expose :name, documentation: { type: 'String', example: 'John Doe' }
  expose :active, documentation: { type: 'Boolean', example: true }
  expose :project, documentation: { type: 'API::Entities::BasicProject'}

영향도가 큰 엔티티와 기능 범위 엔티티#

UserBasic·ProjectIdentity·Commit과 같은 일부 기반 엔티티는 많은 API 엔드포인트에 포함되거나 중첩되어 있습니다. 이런 엔티티 중 하나에 expose 호출을 하나 추가하면, 그 엔티티를 직접 또는 전이적으로 사용하는 모든 엔드포인트의 JSON 응답이 커집니다. 예를 들어 UserBasic에 expose 호출을 하나 추가하면 212개 엔드포인트에, CustomAttribute에 추가하면 238개 엔드포인트에 영향을 미칩니다.

API 응답 페이로드가 통제 없이 커지는 것을 막기 위해, 영향도가 큰 엔티티는 다음 RuboCop cop이 보호합니다. API/EntityExposureGrowth 이 cop은 엔티티별로 허용된 필드의 허용 목록을 다음 파일에서 관리합니다. api_entity_exposure_baseline.yml 허용 목록에 없는 필드를 보호된 엔티티에 새 expose 호출로 추가하면 위반(offense)이 발생합니다.

이것이 중요한 이유#

  • 성능: 추가되는 필드마다 그 엔티티를 포함하는 모든 응답에서 직렬화되어, 페이로드 크기와 직렬화 시간이 늘어납니다.
  • 호환성 깨짐 위험: 노출 중인 필드를 제거하는 것은 호환성이 깨지는 변경으로 간주됩니다. 기반 엔티티에 추가된 필드는 많은 소비자에게 영향을 미치기 때문에 제거 비용이 특히 큽니다.
  • 연쇄 영향: 엔티티는 상속(class User < UserBasic)과 임베딩(expose :author, using: UserBasic)으로 구성됩니다. UserBasic에 추가한 필드 하나는 User, UserPublic, 그리고 이를 임베딩하는 모든 엔티티로 이어집니다.

권장 해결 방법#

다음 순서로 해결 방법을 시도합니다:

  1. 기존 엔티티에 필드를 추가하는 대신, 새 엔티티를 가진 새 엔드포인트로 새 필드를 옮기는 것을 고려합니다.
  2. 필드를 옵트인으로 만듭니다. 쿼리 파라미터 등으로 명시적으로 요청할 때만 직렬화하여, 대부분의 소비자가 비용을 부담하지 않도록 합니다.
  3. 두 방법이 맞지 않으면 기능 범위 엔티티를 만듭니다. 새 필드가 필요한 엔드포인트에서만 사용하는, 그 목적에 맞게 만들어진 새 엔티티 클래스입니다.
옵트인 필드 예시#

expose 호출을 if: 옵션으로 감싸서, 호출자가 쿼리 파라미터로 필드를 요청할 때만 활성화합니다:

module API
  module Entities
    class UserBasic < UserSafe
      expose :state
      expose :avatar_url
      expose :web_url
      expose :notification_email, if: ->(_, options) { options[:with_emails] }
    end
  end
end
# In your API endpoint file
params do
  optional :with_emails, type: Boolean, default: false,
    desc: 'Include email-related fields in the response'
end
get ':id/users' do
  present users, with: Entities::UserBasic, with_emails: params[:with_emails]
end

with_emails=true를 전달하지 않는 호출자는 이전과 동일한 페이로드를 받으므로, 대다수 소비자에게는 이 필드가 비용을 발생시키지 않습니다.

기능 범위 엔티티 예시#

가장 간단한 방법은 기반 엔티티를 상속하고 필요한 필드를 추가하는 새 엔티티를 만드는 것입니다:

# bad - adds :notification_email to every endpoint using UserBasic (212 endpoints)
module API
  module Entities
    class UserBasic < UserSafe
      expose :state
      expose :avatar_url
      expose :web_url
      expose :notification_email  # <-- new field inflates 212 endpoint responses
    end
  end
end

# good - create a domain-scoped entity used only by the endpoints that need it
module API
  module Entities
    module Ci
      class JobOwner < UserBasic
        expose :notification_email, documentation: { type: 'String', example: 'user@example.com' }
      end
    end
  end
end

엔티티 이름은 담고 있는 필드가 아니라(예: UserWithNotificationEmail), 도메인 맥락에서 그것이 나타내는 대상을 따서 짓습니다(예: Ci::JobOwner). UserWithNotificationEmail 같은 이름은 관련 없는 도메인에서도 재사용을 유도해 연쇄 문제를 다시 만들어냅니다. 도메인 범위를 좁힌 이름은 엔티티를 하나의 사용 사례에만 집중하게 합니다.

이후 새 엔티티는 필요한 엔드포인트에서만 사용합니다:

# In your API endpoint file
desc 'List CI job owners' do
  detail 'Returns the owners of CI jobs with notification details.'
  success Entities::Ci::JobOwner
  tags ['ci']
end
get ':id/ci/job_owners' do
  owners = find_job_owners(params[:id])
  present owners, with: Entities::Ci::JobOwner
end

허용 목록 업데이트#

다음 파일의 허용 목록은 api_entity_exposure_baseline.yml 보호된 엔티티별로 허용된 필드 이름을 기록합니다. 새 필드를 추가하려고 허용 목록을 직접 편집해서는 안 됩니다. 대신 위에서 설명한 대로 기능 범위 엔티티를 만듭니다.

필드가 정말로 영향도가 큰 엔티티에 속한다고 판단되면(예: 대다수 소비자가 필요로 하는 경우), 진행하기 전에 API Platform 팀과 트레이드오프를 논의합니다.

문서화#

신규 또는 변경된 API 엔드포인트에는 항상 문서가 있어야 합니다. 문서는 같은 머지 리퀘스트에 포함하거나, 정말 필요한 경우에는 원래 머지 리퀘스트와 같은 마일스톤의 후속 작업으로 포함합니다.

Markdown과 OpenAPI 정의 파일 모두에서 API 리소스를 문서화하는 방법에 대한 자세한 내용은 문서 스타일 가이드의 RESTful API 페이지를 참고합니다.

메서드와 파라미터 설명#

모든 메서드는 Grape DSL을 사용해 설명해야 합니다(좋은 예시는 environments.rb를 참고합니다):

  • desc: 메서드 요약. 120자를 넘지 않는 요약 문자열을 포함해야 합니다.
  • detail: desc 블록마다 하나씩. 문자열이어야 합니다.
  • success: desc 블록마다 하나씩. 성공 응답을 정의합니다.
  • tags: desc 블록마다 하나씩. 문자열 또는 문자열 배열이어야 합니다.
  • params: 메서드 파라미터. 파라미터에 대한 설명, 유효성 검사와 강제 형변환 역할을 합니다.

좋은 예는 다음과 같습니다:

desc 'Get all broadcast messages' do
  detail 'This feature was introduced in GitLab 8.12.'
  success Entities::System::BroadcastMessage
  tags ['broadcast_messages']
end
params do
  optional :page,     type: Integer, desc: 'Current page number'
  optional :per_page, type: Integer, desc: 'Number of messages per page'
end
get do
  messages = System::BroadcastMessage.all

  present paginate(messages), with: Entities::System::BroadcastMessage
end

엔드포인트 desc 정의#

모든 엔드포인트는 desc 블록에 요약 문자열을 포함해야 합니다. 요약은 REST 리소스에 대한 작업을 설명하며, 생성된 OpenAPI 문서에 사용됩니다.

요약은 다음을 지키는 것이 좋습니다:

  • HTTP 메서드에 맞는 동작 동사로 시작합니다(Get, List, Create, Update, Delete)
  • 작업 대상 리소스를 명시합니다
  • 비슷한 엔드포인트를 구분해야 할 때는 수식어를 포함합니다

요약은 다음을 반드시 지켜야 합니다:

  • 문자열 리터럴이거나 보간된 문자열이어야 합니다(변수나 메서드 호출이 아님)
  • 120자를 넘지 않아야 합니다

좋은 예는 다음과 같습니다:

  desc 'Get a specific environment' do
    detail 'Returns environment details. This feature was introduced in GitLab 18.12.'
    success Entities::Environment
  end

나쁜 예는 다음과 같습니다:

  desc 'Get a specific environment. Returns environment details. This feature was introduced in GitLab 18.12.' do
    detail 'Only available to authenticated project owner.'
    success Entities::Environment
  end

엔드포인트 detail 정의#

모든 엔드포인트는 desc 블록마다 detail 값을 가져야 합니다. 이 값은 문자열이어야 합니다. detail은 desc가 다루지 않는 추가 세부 사항을 설명해야 하며, 예를 들면:

  • 엔드포인트가 추가된 GitLab 버전.
  • 기능 플래그 뒤에 있다면 그 사실을 대신 언급합니다: This feature is gated by the :feature\_flag\_symbol feature flag.
  • 엔드포인트가 지원 종료된 경우, 계획된 제거 날짜.

detail이나 desc 요약 문자열에 "experiment", "experimental", "general availability", "GA", "beta" 같은 라이프사이클 용어를 넣지 않습니다. 대신 route_setting :lifecycle을 사용합니다. 자세한 내용은 엔드포인트 라이프사이클 표시를 참고합니다.

엔드포인트 success 정의#

모든 엔드포인트는 desc 블록마다 success 값을 가져야 합니다. 이 값은 엔드포인트의 성공 응답을 정확하게 설명해야 합니다.

성공 응답을 문서화할 때 http_codes 옵션을 사용하지 않습니다. 대신 success와 failure를 사용합니다. 이 규칙은 API/DeprecatedHttpCodes RuboCop cop이 강제합니다.

success 옵션은 다음 중 하나를 받습니다:

  • Grape::Entity 클래스를 직접
  • 옵션 해시

해시 형식을 사용할 때는 다음 옵션을 사용할 수 있습니다:

옵션 타입 필수 여부 설명
code Integer 아니요 HTTP 상태 코드입니다. 필수는 아니지만 항상 의도한 응답 코드를 지정하는 것이 좋습니다.
model Entities::* JSON 응답에는 필수 응답 본문에서 반환되는 Grape::Entity 클래스입니다. model이 없으면 OpenAPI 스펙에 응답 스키마나 예시가 생성되지 않습니다. 204 No Content나 리다이렉트처럼 본문이 없는 응답에서만 생략합니다.
message String 아니요 응답에 대한 간단한 설명입니다.
is_array Boolean 아니요 응답이 모델의 배열이면 true로 설정합니다. 해시 형식에서만 필요합니다. 엔티티 클래스를 배열로 감싸는 것(success [Entities::MyEntity])과 동일합니다.
example Hash 아니요 응답 본문의 단일 인라인 예시입니다. examples와는 함께 쓸 수 없습니다. model이 필요합니다.
examples Hash 아니요 응답 본문의 명명된 예시입니다. example과는 함께 쓸 수 없습니다. model이 필요합니다.

엔드포인트가 반환하는 값에 따라 success 값의 형식을 다르게 씁니다:

  • 엔드포인트가 객체로 응답하면 Grape::Entity 클래스를 직접 전달하거나 model: 옵션을 사용합니다:

    # Direct form
    success Entities::System::BroadcastMessage
    
    # Hash form
    success code: 200, model: Entities::System::BroadcastMessage
    
  • 엔드포인트가 컬렉션으로 응답하면 엔티티 클래스를 배열로 감싸거나 해시 형식에서 is_array: true를 사용합니다. 둘은 동일합니다:

    # Direct form
    success [Entities::System::BroadcastMessage]
    
    # Hash form — use when you also need to specify other options
    success code: 200, model: Entities::System::BroadcastMessage, is_array: true
    
  • 엔드포인트가 객체로 응답하지 않으면 상태 코드와 메시지를 포함합니다:

    success code: 204, message: 'Record was deleted'
    
  • 엔드포인트가 여러 성공 코드를 반환할 수 있으면 배열을 전달합니다:

    success [
      { code: 200, model: Entities::Security::VulnerabilityScanning::SbomScan },
      { code: 202, message: 'Scan in progress' }
    ]
    
  • example:이나 examples:를 지정하지 않고 model:만 정의하면, 엔티티 필드의 documentation: { example: ... } 값에서, 또는 필드 단위 예시가 없으면 필드 타입에서 예시가 자동으로 생성됩니다.

  • 엔드포인트가 객체로 응답하고 응답 본문 전체를 예시로 보여주거나 여러 가능한 응답 본문 예시를 제공하려면, 단일 인라인 값에는 example:을, 여러 개의 명명된 시나리오에는 examples:를 사용합니다. 둘 다 model:이 필요하며 함께 쓸 수 없습니다:

    # Single example
    success code: 200, model: Entities::System::BroadcastMessage,
            example: {
              id: 1,
              message: 'Scheduled maintenance at 23:00',
              starts_at: '2024-03-01T23:00:00.000Z',
              ends_at: '2024-03-02T01:00:00.000Z',
              active: false
            }
    
    # Multiple named examples
    success code: 200, model: Entities::System::BroadcastMessage,
            examples: {
              active_message: {
                summary: 'An active broadcast message',
                value: {
                  id: 1,
                  message: 'Scheduled maintenance at 23:00',
                  starts_at: '2024-03-01T23:00:00.000Z',
                  ends_at: '2024-03-02T01:00:00.000Z',
                  active: true
                }
              },
              expired_message: {
                summary: 'An expired broadcast message',
                value: {
                  id: 2,
                  message: 'Maintenance complete',
                  starts_at: '2024-03-01T23:00:00.000Z',
                  ends_at: '2024-03-02T01:00:00.000Z',
                  active: false
                }
              }
            }
    

엔드포인트 failure 정의#

모든 엔드포인트는 desc 블록마다 failure 응답을 하나 이상 선언해야 합니다. 이를 사용해 엔드포인트가 반환할 수 있는 4xx, 5xx 응답을 문서화합니다.

failure 옵션은 해시 배열을 받습니다. 각 해시는 다음 옵션을 받습니다:

옵션 타입 필수 여부 설명
code Integer 필수 HTTP 상태 코드입니다.
message String 아니요 실패 응답에 대한 간단한 설명입니다.
failure [
  { code: 401, message: 'Unauthorized' },
  { code: 404, message: 'Not found' }
]

이 규칙은 API/DescriptionFailureResponse RuboCop cop이 강제합니다.

엔드포인트를 지원 종료로 표시#

엔드포인트를 지원 종료할 때는 desc 블록에 다음을 추가합니다:

  • deprecated true 옵션을 추가합니다. 이 옵션은 해당 작업에 표준 OpenAPI deprecated: true 플래그를 설정합니다.
  • detail 옵션에 지원 종료 시점과 이전 방법에 대한 안내를 추가합니다.

지원 종료된 엔드포인트에는 route_setting :lifecycle을 사용하지 않습니다. experiment, beta 단계와 달리, 지원 종료는 OpenAPI 스펙이 deprecated 필드로 네이티브 지원하며 deprecated true가 이 필드에 직접 매핑됩니다.

desc 'Get legacy broadcast messages' do
  detail 'Deprecated in GitLab 17.0. Use /api/v4/broadcast_messages instead.'
  deprecated true
  success Entities::System::BroadcastMessage
  tags ['broadcast_messages']
end

이 두 가지를 함께 사용하면 OpenAPI 스펙에서 지원 종료 여부를 프로그램으로 확인할 수 있습니다.

엔드포인트 라이프사이클 표시#

엔드포인트가 아직 일반 공개 상태가 아니면 route_setting :lifecycle을 사용해 개발 단계를 표시합니다. 유효한 값은 :experiment와 :beta입니다.

라이프사이클 정보를 desc 요약이나 detail 문자열에 넣지 않습니다. 이 규칙은 API/LifecycleInDescription RuboCop cop이 강제합니다.

일반 공개된 엔드포인트에서는 route_setting :lifecycle을 생략합니다.

# bad -- Specifies "experimental" in "detail"
desc 'Get all widgets' do
  detail 'This feature is experimental.'
  tags %w[widgets]
end

# good -- Specifies "experiment" as route_setting
route_setting :lifecycle, :experiment
desc 'Get all widgets' do
  detail 'Introduced in GitLab 18.10.'
  tags %w[widgets]
end

# good -- Specifies "beta" as route_setting
route_setting :lifecycle, :beta
desc 'Get all widgets' do
  detail 'Introduced in GitLab 18.10.'
  tags %w[widgets]
end

route_setting :lifecycle 값은 생성된 OpenAPI 스펙에 x-gitlab-lifecycle 벤더 확장으로 포함됩니다. 이를 통해 라이프사이클 상태를 프로그램으로 확인할 수 있습니다.

개발 단계에 대한 자세한 내용은 개발 단계와 지원을 참고합니다.

태그 선택#

모든 엔드포인트는 desc 블록마다 tags에 값을 하나 이상 정의해야 합니다. 태그는 API 호출이 다루는 객체 유형을 복수형으로 설명해야 합니다.

대부분의 경우 API의 파일 이름으로 충분하지만, 너무 세분화될 수도 있습니다.

좋은 태그 이름#

  • audit_events
  • users
  • clusters

나쁜 태그 이름#

  • commit (단수형)
  • epic_management (엔티티가 아니라 제품 카테고리에 결합됨)

태그에 적절한 이름이 명확하지 않으면 테크니컬 라이터에게 문의합니다.

문자열 파라미터 제약#

클라이언트가 무제한 페이로드를 보낼 수 없도록 String 파라미터를 제약합니다. 무제한 문자열은 호출자가 서버 메모리와 처리 시간을 소비하는 큰 요청을 제출하게 하며, 악용 가능한 범위를 넓힙니다.

가능하면 모든 String 파라미터에 다음 검증기 중 하나를 사용합니다:

  • values:: 파라미터가 정해진 값 집합만 받을 때.
  • limit:: 자유 형식 문자열의 최대 글자 수를 제한할 때.
  • regexp:: 파라미터가 특정 형식과 일치해야 할 때.
params do
  optional :state, type: String, values: %w[opened closed], desc: 'Filter by state'
  optional :name, type: String, limit: 255, desc: 'Name of the resource'
end

limit: 검증기는 API::Validations::Validators::Limit에 구현되어 있으며, 설정된 길이보다 긴 값을 거부합니다.

공유 라우트 요구 사항#

NAMESPACE_OR_PROJECT_REQUIREMENTS 같은 라우트 요구 사항 상수는 lib/api/api.rb의 API::API 클래스가 아니라 lib/api.rb의 API 모듈에 있습니다. 참조할 때는 항상 앞에 ::을 붙입니다:

# good
resource :projects, requirements: ::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
end

# bad - without the leading ::, `API` resolves to the `API::API` class
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
end

API::API는 모든 엔드포인트를 마운트하므로, 엔드포인트 클래스 본문에서 이 클래스의 상수를 해석하면 전체 API 표면이 오토로드됩니다. 먼저 마운트된 엔드포인트가 아직 로딩 중인 엔드포인트의 상수를 다시 읽으면 NameError가 발생합니다.

호환성이 깨지는 변경#

메이저 GitLab 릴리스에서도 REST API v4에 호환성이 깨지는 변경을 만들어서는 안 됩니다. 호환성이 깨지는 변경이란과 호환성이 깨지지 않는 변경이란을 참고합니다.

REST API는 GitLab 버전과 무관하게 자체 버전 관리를 유지합니다. 현재 REST API 버전은 4입니다. REST API에 시맨틱 버전 관리를 지키기로 약속했으므로 호환성이 깨지는 변경을 만들 수 없습니다. REST API의 메이저 버전 변경(가능성이 높은 값은 5)은 현재 계획되거나 예정되어 있지 않습니다.

예외는 experimental이나 beta로 표시된 API 기능입니다. 이런 기능은 언제든 제거되거나 변경될 수 있습니다.

호환성이 깨지는 변경 대신 할 일#

다음 절에서는 호환성이 깨지는 변경 대신 쓸 수 있는 대안을 제안합니다.

스키마를 호환성을 깨지 않고 변경에 맞추기#

기능이 변경되면, API에 호환성이 깨지는 변경을 만들지 않고 하위 호환성을 유지하는 것을 목표로 합니다.

호환성이 깨지는 변경을 도입하는 대신, API 소비자에게는 아무 변화도 보이지 않는 방식으로 API 컨트롤러 계층을 변경해 기능 변경에 맞춥니다.

예를 들어 머지 리퀘스트의 WIP 기능 이름을 _Draft_로 바꿀 때, 다음과 같이 했습니다:

  • API 응답에 새 draft 필드를 추가했습니다.
  • 기존 work_in_progress 필드도 유지했습니다.

고객은 기존 API 연동에서 어떤 중단도 겪지 않았습니다.

기능 제거 시 할 일#

엔드포인트가 연동하던 기능이 메이저 GitLab 버전에서 제거되면, API 하위 호환성과 사용자가 신뢰할 수 있는 결과를 반환하는 것 사이에서 균형을 유지해야 합니다.

상황에 맞는 방법을 선택합니다:

조용한 성능 저하(silent degradation) - 오류가 더 넓은 기능을 방해할 때 사용합니다:

  • 필드에서 합리적인 정적 값을 반환하거나 빈 응답을 반환합니다(예: null이나 []).
  • 인수는 계속 받지만 더 이상 동작하지 않게 만들어 아무 동작도 하지 않도록(no-op) 합니다.
  • Application Settings처럼 설정 하나를 제거해도 엔드포인트 전체가 실패해서는 안 되는 엔드포인트에 가장 적합합니다.

오류 응답 - 기능이 완전히 제거되었을 때 사용합니다:

  • 제거된 기능이 엔드포인트의 주된 목적이었다면 404 Not Found를 반환합니다.
  • 이렇게 하면 기능이 더 이상 존재하지 않는다는 사실을 사용자에게 명확히 전달합니다.

핵심 원칙은, 가능하면 기존 고객의 API 연동이 우아하게 성능이 저하되도록 하면서 기능을 더 이상 사용할 수 없을 때는 명확한 피드백을 제공하는 것입니다. 엔드포인트는 이전과 같은 필드로 응답하고 같은 인수를 받지만, 내부적으로는 해당 기능과의 연동이 더 이상 동작하지 않습니다.

의도한 변경 사항은 미리 문서화해야 합니다. v4 지원 종료 가이드를 따릅니다.

예를 들어 애플리케이션 설정 하나를 제거했을 때, 기존 API 필드는 유지했고 지금은 합리적인 정적 값을 반환합니다.

호환성이 깨지는 변경이란#

호환성이 깨지는 변경의 예는 다음과 같습니다:

  • 필드, 인수, enum 값을 제거하거나 이름을 바꾸는 것. JSON 응답에서 필드는 모든 JSON 키를 뜻합니다.
  • 엔드포인트를 제거하는 것.
  • 새 리다이렉트를 추가하는 것(모든 클라이언트가 리다이렉트를 따라가지는 않습니다).
  • 응답의 콘텐츠 타입을 바꾸는 것.
  • 응답 필드의 타입을 바꾸는 것. JSON 응답에서는 Number, String, Boolean, Array, Object 타입 중 하나를 다른 타입으로 바꾸는 것을 뜻합니다.
  • 새 필수 인수를 추가하는 것.
  • 인증, 인가, 또는 그 밖의 헤더 요구 사항을 바꾸는 것. 이는 엔드포인트가 요구하는 것을 바꾸는 경우를 다루며, 받아들이는 범위를 넓히는 변경은 해당하지 않습니다. 넓히는 변경에 대해서는 호환성이 깨지지 않는 변경이란을 참고합니다.
  • 500이 아닌 다른 상태 코드로 바꾸는 것.

호환성이 깨지지 않는 변경이란#

호환성이 깨지지 않는 변경의 예는 다음과 같습니다:

  • 엔드포인트, 필수가 아닌 인수, 필드, enum 값을 추가하는 것과 같은 모든 추가적인 변경.
  • 기존 엔드포인트에 CI/CD job 토큰 인증처럼 추가 인증 방법을 지원하는 것. 이로 인한 상태 코드 변경도 포함합니다. 새 자격 증명 유형을 제시하지 않는 요청은 영향을 받지 않아야 합니다.
  • 오류 메시지를 바꾸는 것.
  • 500 상태 코드에서 지원되는 다른 상태 코드로 바꾸는 것(버그 수정에 해당).
  • 응답에서 반환되는 필드의 순서를 바꾸는 것.

Experimental, beta, 일반 공개 기능#

API 요소를 experimental, beta 기능으로 추가할 수 있습니다. 이때 반드시 추가적인 변경이어야 하며, 그렇지 않으면 호환성이 깨지는 변경으로 분류됩니다.

experiment나 beta로 표시된 API 요소는 호환성이 깨지는 변경 정책이 적용되지 않으며, 사전 통지 없이 언제든 변경되거나 제거될 수 있습니다.

experiment 상태에서는:

beta 상태에서는:

기능이 일반 공개 상태가 되면:

선언된 파라미터#

Grape를 사용하면 params 블록으로 선언한 파라미터에만 접근할 수 있습니다. 전달되었지만 허용되지 않은 파라미터는 걸러집니다. 자세한 내용은 Ruby Grape의 declared() 문서를 참고합니다.

부모 네임스페이스의 파라미터 제외#

기본적으로 declared(params)는 모든 부모 네임스페이스에서 정의된 파라미터를 포함합니다. 자세한 내용은 Ruby Grape의 include_parent_namespaces 문서를 참고합니다.

대부분의 경우 부모 네임스페이스의 파라미터는 제외해야 합니다:

declared(params, include_parent_namespaces: false)

declared(params)를 사용할 시점#

파라미터 해시를 메서드 호출의 인수로 전달할 때는 항상 declared(params)를 사용해야 합니다.

예를 들면:

# bad
User.create(params) # imagine the user submitted `admin=1`... :)

# good
User.create(declared(params, include_parent_namespaces: false).to_h)
Note

declared(params)는 Hashie::Mash 객체를 반환하므로, 반드시 .to_h를 호출해야 합니다.

다만 단일 요소에 접근할 때는 params[key]를 직접 사용할 수 있습니다.

예를 들면:

# good
Model.create(foo: params[:foo])

배열 타입#

Grape v1.3 이상에서는 배열 타입을 coerce_with 블록으로 정의해야 하며, 그렇지 않으면 API 요청에서 문자열이 전달될 때 파라미터가 검증에 실패합니다. 자세한 내용은 Grape 업그레이드 문서를 참고합니다.

nil 입력의 자동 강제 변환#

Grape v1.3.3 이전에는 nil 값을 가진 배열 파라미터가 자동으로 빈 배열로 강제 변환되었습니다. 하지만 v1.3.3의 이 풀 리퀘스트 이후로는 더 이상 그렇지 않습니다. 예를 들어 옵션 파라미터가 있는 PUT /test 요청을 정의했다고 가정해 봅니다:

optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The user ids for this rule'

보통 PUT /test?user_ids 요청을 보내면 Grape는 { user_ids: nil } 형태의 params를 전달합니다.

이는 빈 배열을 기대하지만 nil 입력을 제대로 처리하지 않는 엔드포인트에서 오류를 일으킬 수 있습니다. 이전 동작을 유지하기 위해 모든 API 호출의 before 블록에서 사용하는 coerce_nil_params_to_array! 헬퍼 메서드가 있습니다:

before do
  coerce_nil_params_to_array!
end

이 변경 이후로는 PUT /test?user_ids 요청을 보내면 Grape가 params를 { user_ids: [] }로 전달합니다.

이를 더 쉽게 만들기 위한 Grape 트래커의 미해결 이슈가 있습니다.

Workhorse 지원 업로드#

파일 콘텐츠를 받는 모든 REST API 엔드포인트는 Workhorse 지원 업로드를 사용해야 합니다.

구현 세부 사항은 Workhorse 업로드 문서를 참고합니다.

HTTP 상태 헬퍼 사용#

200이 아닌 HTTP 응답에는 올바른 동작을 보장하기 위해 lib/api/helpers.rb에서 제공하는 헬퍼(예: not_found!, no_content!)를 사용합니다. 이 헬퍼들은 Grape 내부에서 throw를 실행해 엔드포인트 실행을 중단시킵니다.

DELETE 요청에서도 일반적으로 destroy_conditionally! 헬퍼를 사용하는 것이 좋습니다. 이 헬퍼는 기본적으로 성공 시 204 No Content 응답을, 주어진 If-Unmodified-Since 헤더가 범위를 벗어나면 412 Precondition Failed 응답을 반환합니다. 이 헬퍼는 전달된 리소스에서 #destroy를 호출하지만, 블록을 전달해 삭제 방법을 직접 구현할 수도 있습니다.

HTTP 메서드 선택#

새 API 라우트를 정의할 때는 올바른 HTTP 요청 메서드를 사용합니다.

PATCH와 PUT 중 선택#

Rails 애플리케이션에서는 PATCH와 PUT 요청 메서드 모두 컨트롤러의 update 메서드로 라우팅됩니다. 하지만 GitLab API를 작성할 때 사용하는 프레임워크인 Grape에서는 업데이트를 수행하는 엔드포인트에 PATCH나 PUT HTTP 메서드를 명시적으로 지정해야 합니다.

엔드포인트가 리소스의 모든 속성을 업데이트하면 PUT 요청 메서드를 사용합니다. 엔드포인트가 리소스의 일부 속성만 업데이트하면 PATCH 요청 메서드를 사용합니다.

PATCH의 좋은 예: PATCH /projects/:id/protected_branches/:name PUT의 좋은 예: PUT /projects/:id/merge_requests/:merge_request_iid/approve

좋은 PUT 엔드포인트는 대개 ID와 동사만 가집니다(위 예시에서는 "approve"). 또는 단일 값만 가지고 키/값 쌍을 나타냅니다.

Rails 블로그에는 업데이트를 수행하는 웹 API 엔드포인트에 보통 PATCH가 가장 적합한 이유가 자세히 설명되어 있습니다.

GitLab Rails 코드베이스에서 API 경로 헬퍼 사용#

상대 URL 아래에 GitLab을 설치하는 것을 지원하므로, Grape가 생성하는 API 경로 헬퍼를 사용할 때는 이를 고려해야 합니다. 이런 API 경로 헬퍼는 반드시 expose_path 헬퍼 호출로 감싸야 합니다.

예를 들면:

- endpoint = expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid))

커스텀 검증기#

API 요청의 일부 파라미터를 검증하기 위해, 이를 (예를 들어 Gitaly로) 더 전달하기 전에 검증합니다. 다음은 지금까지 추가한 커스텀 검증기와 그 사용법입니다. 새 커스텀 검증기를 추가하는 방법에 대한 가이드도 작성했습니다.

커스텀 검증기 사용#

  • FilePath:

    GitLab은 파일 경로를 순회해야 하는 다양한 기능을 지원합니다. FilePath 검증기는 여러 경우에 대해 파라미터 값을 검증합니다. 주로 경로가 상대 경로인지, File::Separator를 사용한 ../../ 상대 경로 순회를 포함하는지, 그리고 경로가 절대 경로인지(예: /etc/passwd/) 확인합니다. 기본적으로 절대 경로는 허용되지 않습니다. 다만 다음과 같은 방식으로 허용할 절대 경로의 허용 목록을 선택적으로 전달할 수 있습니다: requires :file_path, type: String, file_path: { allowlist: ['/foo/bar/', '/home/foo/', '/app/home'] }

  • Git SHA:

    Git SHA 검증기는 Git SHA 파라미터가 유효한 SHA인지 확인합니다. 이는 commit.rb 파일에 언급된 정규식을 사용해 확인합니다.

  • Absence:

    Absence 검증기는 주어진 파라미터 해시에 특정 파라미터가 없는지 확인합니다.

  • IntegerNoneAny:

    IntegerNoneAny 검증기는 주어진 파라미터 값이 Integer, None, Any 중 하나인지 확인합니다. 이 값들 중 하나일 때만 요청을 진행시킵니다.

  • ArrayNoneAny:

    ArrayNoneAny 검증기는 주어진 파라미터 값이 Array, None, Any 중 하나인지 확인합니다. 이 값들 중 하나일 때만 요청을 진행시킵니다.

  • EmailOrEmailList:

    EmailOrEmailList 검증기는 문자열이나 문자열 목록의 값이 유효한 이메일 주소로만 구성되어 있는지 확인합니다. 모든 이메일 주소가 유효한 목록만 요청을 진행시킵니다.

새 커스텀 검증기 추가#

커스텀 검증기는 파라미터를 플랫폼에 보내 추가로 처리하기 전에 검증하는 좋은 방법입니다. 처음부터 유효하지 않은 파라미터를 식별하면 서버와 플랫폼 사이의 왕복을 줄일 수 있습니다.

커스텀 검증기를 추가해야 한다면, validators 디렉터리 안에 자신만의 파일로 추가합니다. API를 추가할 때 Grape를 사용하므로 검증기 클래스는 Grape::Validations::Validators::Base 클래스를 상속합니다. 이제 params 해시와 검증할 param 이름, 이 두 파라미터를 받는 validate_param! 메서드를 정의하기만 하면 됩니다.

이 메서드의 본문은 파라미터 값을 검증하는 실제 작업을 수행하고 호출한 메서드에 적절한 오류 메시지를 반환합니다.

마지막으로, 다음 줄을 사용해 검증기를 등록합니다:

Grape::Validations.register_validator(<validator name as symbol>, ::API::Helpers::CustomValidators::)

검증기를 추가한 뒤에는 validators 디렉터리 안에 자신만의 파일로 rspec 테스트도 반드시 추가합니다.

내부 API#

내부 API는 내부 용도로 문서화되어 있습니다. 어떤 컴포넌트가 어떤 엔드포인트를 사용하는지 알 수 있도록 최신 상태로 유지합니다.

N+1 문제 피하기#

API 엔드포인트에서 레코드 컬렉션을 반환할 때 흔히 발생하는 N+1 문제를 피하려면 즉시 로딩(eager loading)을 사용해야 합니다.

API 안에서 이를 수행하는 표준적인 방법은, 모델이 API에서 반환되는 연관 관계와 데이터를 미리 로드하는 with_api_entity_associations라는 스코프를 구현하는 것입니다. 이 스코프의 예시는 다음에서 확인할 수 있습니다. Issue 모델

동일한 모델이 API에서 여러 엔티티를 가지는 경우 (예: UserBasic, User, UserPublic)에는 이 스코프를 적용할지 재량으로 판단해야 합니다. 가장 기본적인 엔티티에 맞춰 최적화하고, 이후 엔티티들이 그 스코프 위에 쌓아 올리는 방식일 수도 있습니다.

with_api_entity_associations 스코프는 할 일 API에서 반환될 때 Todo의 _대상(target)_에 대해서도 데이터를 자동으로 미리 로드합니다.

미리 로드에 대한 더 많은 맥락과 논의는 이 스코프를 도입한 이 머지 리퀘스트를 참고합니다.

테스트로 검증#

API 엔드포인트가 컬렉션을 반환할 때는, 지금뿐 아니라 앞으로도 그 엔드포인트에 N+1 문제가 없는지 확인하는 테스트를 항상 추가합니다. 이는 ActiveRecord::QueryRecorder를 사용해 할 수 있습니다.

예시:

def make_api_request
  get api('/foo', personal_access_token: pat)
end

it 'avoids N+1 queries', :request_store do
  # Firstly, record how many PostgreSQL queries the endpoint will make
  # when it returns a single record
  create_record

  control = ActiveRecord::QueryRecorder.new { make_api_request }

  # Now create a second record and ensure that the API does not execute
  # any more queries than before
  create_record

  expect { make_api_request }.not_to exceed_query_limit(control)
end

테스트#

새 API 엔드포인트의 테스트를 작성할 때는 /spec/fixtures/api/schemas에 있는 스키마 픽스처를 사용하는 것을 고려합니다. 응답이 주어진 스키마와 일치하는지 expect로 확인할 수 있습니다:

expect(response).to match_response_schema('merge_requests')

테스트에서 N+1 성능 검증하기도 참고합니다.

changelog 항목 포함#

클라이언트에 노출되는 모든 변경 사항에는 changelog 항목을 포함해야 합니다. 여기에는 내부 API가 포함되지 않습니다.

API 스타일 가이드

GitLab v19.4
원문 보기

요약

이 스타일 가이드는 API 개발에 대한 모범 사례를 권장합니다. 고객에게 두 가지 유형의 API를 제공합니다: 두 API를 병렬로 지원하는 기술적 부담을 줄이기 위해, 가능한 한 많은 구현을 공유해야 합니다. 프론트엔드에서 개발할 때 어떤 API를 사용할지에 대한 자세한 내용은 프론트엔드 가이드를 참고합니다.

이 스타일 가이드는 API 개발에 대한 모범 사례를 권장합니다.

GraphQL 및 REST API#

고객에게 두 가지 유형의 API를 제공합니다:

두 API를 병렬로 지원하는 기술적 부담을 줄이기 위해, 가능한 한 많은 구현을 공유해야 합니다. 예를 들어, 동일한 서비스를 공유할 수 있습니다.

프론트엔드#

프론트엔드에서 개발할 때 어떤 API를 사용할지에 대한 자세한 내용은 프론트엔드 가이드를 참고합니다.

인스턴스 변수#

인스턴스 변수는 사용하지 않습니다. 인스턴스 변수를 사용할 필요가 없으며(Rails 뷰에서처럼 접근할 필요가 없습니다), 로컬 변수로 충분합니다.

엔티티#

엔드포인트의 페이로드를 표현할 때는 항상 Entity를 사용합니다.

엔티티 필드 정의#

엔티티에서 노출하는 모든 필드는 유효한 타입을 포함하거나 참조해야 합니다.

다른 엔티티 참조#

다른 엔티티를 참조하는 필드를 노출할 때는 using 옵션을 사용합니다. using 옵션은 API::Entities 클래스를 가리키는 상수만 받습니다. 좋은 예는 다음과 같습니다.

  expose :project, using: ::API::Entities::BasicProjectDetails

유효한 필드 타입#

필드 타입은 문자열로 지정해야 합니다. 다음 타입을 사용할 수 있습니다:

카테고리 타입
스칼라 Integer, Float, BigDecimal, Numeric, Date, DateTime, Time, String, Symbol, Boolean
구조체 Hash, Array, Set
특수 JSON, File
엔티티 참조 문자열 형태의 임의의 API::Entities::* 클래스

필드 타입 정의#

필드 타입은 documentation 해시에 정의해야 합니다:

  expose :id, documentation: { type: 'Integer', example: 1 }
  expose :name, documentation: { type: 'String', example: 'John Doe' }
  expose :active, documentation: { type: 'Boolean', example: true }
  expose :project, documentation: { type: 'API::Entities::BasicProject'}

영향도가 큰 엔티티와 기능 범위 엔티티#

UserBasic·ProjectIdentity·Commit과 같은 일부 기반 엔티티는 많은 API 엔드포인트에 포함되거나 중첩되어 있습니다. 이런 엔티티 중 하나에 expose 호출을 하나 추가하면, 그 엔티티를 직접 또는 전이적으로 사용하는 모든 엔드포인트의 JSON 응답이 커집니다. 예를 들어 UserBasic에 expose 호출을 하나 추가하면 212개 엔드포인트에, CustomAttribute에 추가하면 238개 엔드포인트에 영향을 미칩니다.

API 응답 페이로드가 통제 없이 커지는 것을 막기 위해, 영향도가 큰 엔티티는 다음 RuboCop cop이 보호합니다. API/EntityExposureGrowth 이 cop은 엔티티별로 허용된 필드의 허용 목록을 다음 파일에서 관리합니다. api_entity_exposure_baseline.yml 허용 목록에 없는 필드를 보호된 엔티티에 새 expose 호출로 추가하면 위반(offense)이 발생합니다.

이것이 중요한 이유#

  • 성능: 추가되는 필드마다 그 엔티티를 포함하는 모든 응답에서 직렬화되어, 페이로드 크기와 직렬화 시간이 늘어납니다.
  • 호환성 깨짐 위험: 노출 중인 필드를 제거하는 것은 호환성이 깨지는 변경으로 간주됩니다. 기반 엔티티에 추가된 필드는 많은 소비자에게 영향을 미치기 때문에 제거 비용이 특히 큽니다.
  • 연쇄 영향: 엔티티는 상속(class User < UserBasic)과 임베딩(expose :author, using: UserBasic)으로 구성됩니다. UserBasic에 추가한 필드 하나는 User, UserPublic, 그리고 이를 임베딩하는 모든 엔티티로 이어집니다.

권장 해결 방법#

다음 순서로 해결 방법을 시도합니다:

  1. 기존 엔티티에 필드를 추가하는 대신, 새 엔티티를 가진 새 엔드포인트로 새 필드를 옮기는 것을 고려합니다.
  2. 필드를 옵트인으로 만듭니다. 쿼리 파라미터 등으로 명시적으로 요청할 때만 직렬화하여, 대부분의 소비자가 비용을 부담하지 않도록 합니다.
  3. 두 방법이 맞지 않으면 기능 범위 엔티티를 만듭니다. 새 필드가 필요한 엔드포인트에서만 사용하는, 그 목적에 맞게 만들어진 새 엔티티 클래스입니다.
옵트인 필드 예시#

expose 호출을 if: 옵션으로 감싸서, 호출자가 쿼리 파라미터로 필드를 요청할 때만 활성화합니다:

module API
  module Entities
    class UserBasic < UserSafe
      expose :state
      expose :avatar_url
      expose :web_url
      expose :notification_email, if: ->(_, options) { options[:with_emails] }
    end
  end
end
# In your API endpoint file
params do
  optional :with_emails, type: Boolean, default: false,
    desc: 'Include email-related fields in the response'
end
get ':id/users' do
  present users, with: Entities::UserBasic, with_emails: params[:with_emails]
end

with_emails=true를 전달하지 않는 호출자는 이전과 동일한 페이로드를 받으므로, 대다수 소비자에게는 이 필드가 비용을 발생시키지 않습니다.

기능 범위 엔티티 예시#

가장 간단한 방법은 기반 엔티티를 상속하고 필요한 필드를 추가하는 새 엔티티를 만드는 것입니다:

# bad - adds :notification_email to every endpoint using UserBasic (212 endpoints)
module API
  module Entities
    class UserBasic < UserSafe
      expose :state
      expose :avatar_url
      expose :web_url
      expose :notification_email  # <-- new field inflates 212 endpoint responses
    end
  end
end

# good - create a domain-scoped entity used only by the endpoints that need it
module API
  module Entities
    module Ci
      class JobOwner < UserBasic
        expose :notification_email, documentation: { type: 'String', example: 'user@example.com' }
      end
    end
  end
end

엔티티 이름은 담고 있는 필드가 아니라(예: UserWithNotificationEmail), 도메인 맥락에서 그것이 나타내는 대상을 따서 짓습니다(예: Ci::JobOwner). UserWithNotificationEmail 같은 이름은 관련 없는 도메인에서도 재사용을 유도해 연쇄 문제를 다시 만들어냅니다. 도메인 범위를 좁힌 이름은 엔티티를 하나의 사용 사례에만 집중하게 합니다.

이후 새 엔티티는 필요한 엔드포인트에서만 사용합니다:

# In your API endpoint file
desc 'List CI job owners' do
  detail 'Returns the owners of CI jobs with notification details.'
  success Entities::Ci::JobOwner
  tags ['ci']
end
get ':id/ci/job_owners' do
  owners = find_job_owners(params[:id])
  present owners, with: Entities::Ci::JobOwner
end

허용 목록 업데이트#

다음 파일의 허용 목록은 api_entity_exposure_baseline.yml 보호된 엔티티별로 허용된 필드 이름을 기록합니다. 새 필드를 추가하려고 허용 목록을 직접 편집해서는 안 됩니다. 대신 위에서 설명한 대로 기능 범위 엔티티를 만듭니다.

필드가 정말로 영향도가 큰 엔티티에 속한다고 판단되면(예: 대다수 소비자가 필요로 하는 경우), 진행하기 전에 API Platform 팀과 트레이드오프를 논의합니다.

문서화#

신규 또는 변경된 API 엔드포인트에는 항상 문서가 있어야 합니다. 문서는 같은 머지 리퀘스트에 포함하거나, 정말 필요한 경우에는 원래 머지 리퀘스트와 같은 마일스톤의 후속 작업으로 포함합니다.

Markdown과 OpenAPI 정의 파일 모두에서 API 리소스를 문서화하는 방법에 대한 자세한 내용은 문서 스타일 가이드의 RESTful API 페이지를 참고합니다.

메서드와 파라미터 설명#

모든 메서드는 Grape DSL을 사용해 설명해야 합니다(좋은 예시는 environments.rb를 참고합니다):

  • desc: 메서드 요약. 120자를 넘지 않는 요약 문자열을 포함해야 합니다.
  • detail: desc 블록마다 하나씩. 문자열이어야 합니다.
  • success: desc 블록마다 하나씩. 성공 응답을 정의합니다.
  • tags: desc 블록마다 하나씩. 문자열 또는 문자열 배열이어야 합니다.
  • params: 메서드 파라미터. 파라미터에 대한 설명, 유효성 검사와 강제 형변환 역할을 합니다.

좋은 예는 다음과 같습니다:

desc 'Get all broadcast messages' do
  detail 'This feature was introduced in GitLab 8.12.'
  success Entities::System::BroadcastMessage
  tags ['broadcast_messages']
end
params do
  optional :page,     type: Integer, desc: 'Current page number'
  optional :per_page, type: Integer, desc: 'Number of messages per page'
end
get do
  messages = System::BroadcastMessage.all

  present paginate(messages), with: Entities::System::BroadcastMessage
end

엔드포인트 desc 정의#

모든 엔드포인트는 desc 블록에 요약 문자열을 포함해야 합니다. 요약은 REST 리소스에 대한 작업을 설명하며, 생성된 OpenAPI 문서에 사용됩니다.

요약은 다음을 지키는 것이 좋습니다:

  • HTTP 메서드에 맞는 동작 동사로 시작합니다(Get, List, Create, Update, Delete)
  • 작업 대상 리소스를 명시합니다
  • 비슷한 엔드포인트를 구분해야 할 때는 수식어를 포함합니다

요약은 다음을 반드시 지켜야 합니다:

  • 문자열 리터럴이거나 보간된 문자열이어야 합니다(변수나 메서드 호출이 아님)
  • 120자를 넘지 않아야 합니다

좋은 예는 다음과 같습니다:

  desc 'Get a specific environment' do
    detail 'Returns environment details. This feature was introduced in GitLab 18.12.'
    success Entities::Environment
  end

나쁜 예는 다음과 같습니다:

  desc 'Get a specific environment. Returns environment details. This feature was introduced in GitLab 18.12.' do
    detail 'Only available to authenticated project owner.'
    success Entities::Environment
  end

엔드포인트 detail 정의#

모든 엔드포인트는 desc 블록마다 detail 값을 가져야 합니다. 이 값은 문자열이어야 합니다. detail은 desc가 다루지 않는 추가 세부 사항을 설명해야 하며, 예를 들면:

  • 엔드포인트가 추가된 GitLab 버전.
  • 기능 플래그 뒤에 있다면 그 사실을 대신 언급합니다: This feature is gated by the :feature\_flag\_symbol feature flag.
  • 엔드포인트가 지원 종료된 경우, 계획된 제거 날짜.

detail이나 desc 요약 문자열에 "experiment", "experimental", "general availability", "GA", "beta" 같은 라이프사이클 용어를 넣지 않습니다. 대신 route_setting :lifecycle을 사용합니다. 자세한 내용은 엔드포인트 라이프사이클 표시를 참고합니다.

엔드포인트 success 정의#

모든 엔드포인트는 desc 블록마다 success 값을 가져야 합니다. 이 값은 엔드포인트의 성공 응답을 정확하게 설명해야 합니다.

성공 응답을 문서화할 때 http_codes 옵션을 사용하지 않습니다. 대신 success와 failure를 사용합니다. 이 규칙은 API/DeprecatedHttpCodes RuboCop cop이 강제합니다.

success 옵션은 다음 중 하나를 받습니다:

  • Grape::Entity 클래스를 직접
  • 옵션 해시

해시 형식을 사용할 때는 다음 옵션을 사용할 수 있습니다:

옵션 타입 필수 여부 설명
code Integer 아니요 HTTP 상태 코드입니다. 필수는 아니지만 항상 의도한 응답 코드를 지정하는 것이 좋습니다.
model Entities::* JSON 응답에는 필수 응답 본문에서 반환되는 Grape::Entity 클래스입니다. model이 없으면 OpenAPI 스펙에 응답 스키마나 예시가 생성되지 않습니다. 204 No Content나 리다이렉트처럼 본문이 없는 응답에서만 생략합니다.
message String 아니요 응답에 대한 간단한 설명입니다.
is_array Boolean 아니요 응답이 모델의 배열이면 true로 설정합니다. 해시 형식에서만 필요합니다. 엔티티 클래스를 배열로 감싸는 것(success [Entities::MyEntity])과 동일합니다.
example Hash 아니요 응답 본문의 단일 인라인 예시입니다. examples와는 함께 쓸 수 없습니다. model이 필요합니다.
examples Hash 아니요 응답 본문의 명명된 예시입니다. example과는 함께 쓸 수 없습니다. model이 필요합니다.

엔드포인트가 반환하는 값에 따라 success 값의 형식을 다르게 씁니다:

  • 엔드포인트가 객체로 응답하면 Grape::Entity 클래스를 직접 전달하거나 model: 옵션을 사용합니다:

    # Direct form
    success Entities::System::BroadcastMessage
    
    # Hash form
    success code: 200, model: Entities::System::BroadcastMessage
    
  • 엔드포인트가 컬렉션으로 응답하면 엔티티 클래스를 배열로 감싸거나 해시 형식에서 is_array: true를 사용합니다. 둘은 동일합니다:

    # Direct form
    success [Entities::System::BroadcastMessage]
    
    # Hash form — use when you also need to specify other options
    success code: 200, model: Entities::System::BroadcastMessage, is_array: true
    
  • 엔드포인트가 객체로 응답하지 않으면 상태 코드와 메시지를 포함합니다:

    success code: 204, message: 'Record was deleted'
    
  • 엔드포인트가 여러 성공 코드를 반환할 수 있으면 배열을 전달합니다:

    success [
      { code: 200, model: Entities::Security::VulnerabilityScanning::SbomScan },
      { code: 202, message: 'Scan in progress' }
    ]
    
  • example:이나 examples:를 지정하지 않고 model:만 정의하면, 엔티티 필드의 documentation: { example: ... } 값에서, 또는 필드 단위 예시가 없으면 필드 타입에서 예시가 자동으로 생성됩니다.

  • 엔드포인트가 객체로 응답하고 응답 본문 전체를 예시로 보여주거나 여러 가능한 응답 본문 예시를 제공하려면, 단일 인라인 값에는 example:을, 여러 개의 명명된 시나리오에는 examples:를 사용합니다. 둘 다 model:이 필요하며 함께 쓸 수 없습니다:

    # Single example
    success code: 200, model: Entities::System::BroadcastMessage,
            example: {
              id: 1,
              message: 'Scheduled maintenance at 23:00',
              starts_at: '2024-03-01T23:00:00.000Z',
              ends_at: '2024-03-02T01:00:00.000Z',
              active: false
            }
    
    # Multiple named examples
    success code: 200, model: Entities::System::BroadcastMessage,
            examples: {
              active_message: {
                summary: 'An active broadcast message',
                value: {
                  id: 1,
                  message: 'Scheduled maintenance at 23:00',
                  starts_at: '2024-03-01T23:00:00.000Z',
                  ends_at: '2024-03-02T01:00:00.000Z',
                  active: true
                }
              },
              expired_message: {
                summary: 'An expired broadcast message',
                value: {
                  id: 2,
                  message: 'Maintenance complete',
                  starts_at: '2024-03-01T23:00:00.000Z',
                  ends_at: '2024-03-02T01:00:00.000Z',
                  active: false
                }
              }
            }
    

엔드포인트 failure 정의#

모든 엔드포인트는 desc 블록마다 failure 응답을 하나 이상 선언해야 합니다. 이를 사용해 엔드포인트가 반환할 수 있는 4xx, 5xx 응답을 문서화합니다.

failure 옵션은 해시 배열을 받습니다. 각 해시는 다음 옵션을 받습니다:

옵션 타입 필수 여부 설명
code Integer 필수 HTTP 상태 코드입니다.
message String 아니요 실패 응답에 대한 간단한 설명입니다.
failure [
  { code: 401, message: 'Unauthorized' },
  { code: 404, message: 'Not found' }
]

이 규칙은 API/DescriptionFailureResponse RuboCop cop이 강제합니다.

엔드포인트를 지원 종료로 표시#

엔드포인트를 지원 종료할 때는 desc 블록에 다음을 추가합니다:

  • deprecated true 옵션을 추가합니다. 이 옵션은 해당 작업에 표준 OpenAPI deprecated: true 플래그를 설정합니다.
  • detail 옵션에 지원 종료 시점과 이전 방법에 대한 안내를 추가합니다.

지원 종료된 엔드포인트에는 route_setting :lifecycle을 사용하지 않습니다. experiment, beta 단계와 달리, 지원 종료는 OpenAPI 스펙이 deprecated 필드로 네이티브 지원하며 deprecated true가 이 필드에 직접 매핑됩니다.

desc 'Get legacy broadcast messages' do
  detail 'Deprecated in GitLab 17.0. Use /api/v4/broadcast_messages instead.'
  deprecated true
  success Entities::System::BroadcastMessage
  tags ['broadcast_messages']
end

이 두 가지를 함께 사용하면 OpenAPI 스펙에서 지원 종료 여부를 프로그램으로 확인할 수 있습니다.

엔드포인트 라이프사이클 표시#

엔드포인트가 아직 일반 공개 상태가 아니면 route_setting :lifecycle을 사용해 개발 단계를 표시합니다. 유효한 값은 :experiment와 :beta입니다.

라이프사이클 정보를 desc 요약이나 detail 문자열에 넣지 않습니다. 이 규칙은 API/LifecycleInDescription RuboCop cop이 강제합니다.

일반 공개된 엔드포인트에서는 route_setting :lifecycle을 생략합니다.

# bad -- Specifies "experimental" in "detail"
desc 'Get all widgets' do
  detail 'This feature is experimental.'
  tags %w[widgets]
end

# good -- Specifies "experiment" as route_setting
route_setting :lifecycle, :experiment
desc 'Get all widgets' do
  detail 'Introduced in GitLab 18.10.'
  tags %w[widgets]
end

# good -- Specifies "beta" as route_setting
route_setting :lifecycle, :beta
desc 'Get all widgets' do
  detail 'Introduced in GitLab 18.10.'
  tags %w[widgets]
end

route_setting :lifecycle 값은 생성된 OpenAPI 스펙에 x-gitlab-lifecycle 벤더 확장으로 포함됩니다. 이를 통해 라이프사이클 상태를 프로그램으로 확인할 수 있습니다.

개발 단계에 대한 자세한 내용은 개발 단계와 지원을 참고합니다.

태그 선택#

모든 엔드포인트는 desc 블록마다 tags에 값을 하나 이상 정의해야 합니다. 태그는 API 호출이 다루는 객체 유형을 복수형으로 설명해야 합니다.

대부분의 경우 API의 파일 이름으로 충분하지만, 너무 세분화될 수도 있습니다.

좋은 태그 이름#

  • audit_events
  • users
  • clusters

나쁜 태그 이름#

  • commit (단수형)
  • epic_management (엔티티가 아니라 제품 카테고리에 결합됨)

태그에 적절한 이름이 명확하지 않으면 테크니컬 라이터에게 문의합니다.

문자열 파라미터 제약#

클라이언트가 무제한 페이로드를 보낼 수 없도록 String 파라미터를 제약합니다. 무제한 문자열은 호출자가 서버 메모리와 처리 시간을 소비하는 큰 요청을 제출하게 하며, 악용 가능한 범위를 넓힙니다.

가능하면 모든 String 파라미터에 다음 검증기 중 하나를 사용합니다:

  • values:: 파라미터가 정해진 값 집합만 받을 때.
  • limit:: 자유 형식 문자열의 최대 글자 수를 제한할 때.
  • regexp:: 파라미터가 특정 형식과 일치해야 할 때.
params do
  optional :state, type: String, values: %w[opened closed], desc: 'Filter by state'
  optional :name, type: String, limit: 255, desc: 'Name of the resource'
end

limit: 검증기는 API::Validations::Validators::Limit에 구현되어 있으며, 설정된 길이보다 긴 값을 거부합니다.

공유 라우트 요구 사항#

NAMESPACE_OR_PROJECT_REQUIREMENTS 같은 라우트 요구 사항 상수는 lib/api/api.rb의 API::API 클래스가 아니라 lib/api.rb의 API 모듈에 있습니다. 참조할 때는 항상 앞에 ::을 붙입니다:

# good
resource :projects, requirements: ::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
end

# bad - without the leading ::, `API` resolves to the `API::API` class
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
end

API::API는 모든 엔드포인트를 마운트하므로, 엔드포인트 클래스 본문에서 이 클래스의 상수를 해석하면 전체 API 표면이 오토로드됩니다. 먼저 마운트된 엔드포인트가 아직 로딩 중인 엔드포인트의 상수를 다시 읽으면 NameError가 발생합니다.

호환성이 깨지는 변경#

메이저 GitLab 릴리스에서도 REST API v4에 호환성이 깨지는 변경을 만들어서는 안 됩니다. 호환성이 깨지는 변경이란과 호환성이 깨지지 않는 변경이란을 참고합니다.

REST API는 GitLab 버전과 무관하게 자체 버전 관리를 유지합니다. 현재 REST API 버전은 4입니다. REST API에 시맨틱 버전 관리를 지키기로 약속했으므로 호환성이 깨지는 변경을 만들 수 없습니다. REST API의 메이저 버전 변경(가능성이 높은 값은 5)은 현재 계획되거나 예정되어 있지 않습니다.

예외는 experimental이나 beta로 표시된 API 기능입니다. 이런 기능은 언제든 제거되거나 변경될 수 있습니다.

호환성이 깨지는 변경 대신 할 일#

다음 절에서는 호환성이 깨지는 변경 대신 쓸 수 있는 대안을 제안합니다.

스키마를 호환성을 깨지 않고 변경에 맞추기#

기능이 변경되면, API에 호환성이 깨지는 변경을 만들지 않고 하위 호환성을 유지하는 것을 목표로 합니다.

호환성이 깨지는 변경을 도입하는 대신, API 소비자에게는 아무 변화도 보이지 않는 방식으로 API 컨트롤러 계층을 변경해 기능 변경에 맞춥니다.

예를 들어 머지 리퀘스트의 WIP 기능 이름을 _Draft_로 바꿀 때, 다음과 같이 했습니다:

  • API 응답에 새 draft 필드를 추가했습니다.
  • 기존 work_in_progress 필드도 유지했습니다.

고객은 기존 API 연동에서 어떤 중단도 겪지 않았습니다.

기능 제거 시 할 일#

엔드포인트가 연동하던 기능이 메이저 GitLab 버전에서 제거되면, API 하위 호환성과 사용자가 신뢰할 수 있는 결과를 반환하는 것 사이에서 균형을 유지해야 합니다.

상황에 맞는 방법을 선택합니다:

조용한 성능 저하(silent degradation) - 오류가 더 넓은 기능을 방해할 때 사용합니다:

  • 필드에서 합리적인 정적 값을 반환하거나 빈 응답을 반환합니다(예: null이나 []).
  • 인수는 계속 받지만 더 이상 동작하지 않게 만들어 아무 동작도 하지 않도록(no-op) 합니다.
  • Application Settings처럼 설정 하나를 제거해도 엔드포인트 전체가 실패해서는 안 되는 엔드포인트에 가장 적합합니다.

오류 응답 - 기능이 완전히 제거되었을 때 사용합니다:

  • 제거된 기능이 엔드포인트의 주된 목적이었다면 404 Not Found를 반환합니다.
  • 이렇게 하면 기능이 더 이상 존재하지 않는다는 사실을 사용자에게 명확히 전달합니다.

핵심 원칙은, 가능하면 기존 고객의 API 연동이 우아하게 성능이 저하되도록 하면서 기능을 더 이상 사용할 수 없을 때는 명확한 피드백을 제공하는 것입니다. 엔드포인트는 이전과 같은 필드로 응답하고 같은 인수를 받지만, 내부적으로는 해당 기능과의 연동이 더 이상 동작하지 않습니다.

의도한 변경 사항은 미리 문서화해야 합니다. v4 지원 종료 가이드를 따릅니다.

예를 들어 애플리케이션 설정 하나를 제거했을 때, 기존 API 필드는 유지했고 지금은 합리적인 정적 값을 반환합니다.

호환성이 깨지는 변경이란#

호환성이 깨지는 변경의 예는 다음과 같습니다:

  • 필드, 인수, enum 값을 제거하거나 이름을 바꾸는 것. JSON 응답에서 필드는 모든 JSON 키를 뜻합니다.
  • 엔드포인트를 제거하는 것.
  • 새 리다이렉트를 추가하는 것(모든 클라이언트가 리다이렉트를 따라가지는 않습니다).
  • 응답의 콘텐츠 타입을 바꾸는 것.
  • 응답 필드의 타입을 바꾸는 것. JSON 응답에서는 Number, String, Boolean, Array, Object 타입 중 하나를 다른 타입으로 바꾸는 것을 뜻합니다.
  • 새 필수 인수를 추가하는 것.
  • 인증, 인가, 또는 그 밖의 헤더 요구 사항을 바꾸는 것. 이는 엔드포인트가 요구하는 것을 바꾸는 경우를 다루며, 받아들이는 범위를 넓히는 변경은 해당하지 않습니다. 넓히는 변경에 대해서는 호환성이 깨지지 않는 변경이란을 참고합니다.
  • 500이 아닌 다른 상태 코드로 바꾸는 것.

호환성이 깨지지 않는 변경이란#

호환성이 깨지지 않는 변경의 예는 다음과 같습니다:

  • 엔드포인트, 필수가 아닌 인수, 필드, enum 값을 추가하는 것과 같은 모든 추가적인 변경.
  • 기존 엔드포인트에 CI/CD job 토큰 인증처럼 추가 인증 방법을 지원하는 것. 이로 인한 상태 코드 변경도 포함합니다. 새 자격 증명 유형을 제시하지 않는 요청은 영향을 받지 않아야 합니다.
  • 오류 메시지를 바꾸는 것.
  • 500 상태 코드에서 지원되는 다른 상태 코드로 바꾸는 것(버그 수정에 해당).
  • 응답에서 반환되는 필드의 순서를 바꾸는 것.

Experimental, beta, 일반 공개 기능#

API 요소를 experimental, beta 기능으로 추가할 수 있습니다. 이때 반드시 추가적인 변경이어야 하며, 그렇지 않으면 호환성이 깨지는 변경으로 분류됩니다.

experiment나 beta로 표시된 API 요소는 호환성이 깨지는 변경 정책이 적용되지 않으며, 사전 통지 없이 언제든 변경되거나 제거될 수 있습니다.

experiment 상태에서는:

beta 상태에서는:

기능이 일반 공개 상태가 되면:

선언된 파라미터#

Grape를 사용하면 params 블록으로 선언한 파라미터에만 접근할 수 있습니다. 전달되었지만 허용되지 않은 파라미터는 걸러집니다. 자세한 내용은 Ruby Grape의 declared() 문서를 참고합니다.

부모 네임스페이스의 파라미터 제외#

기본적으로 declared(params)는 모든 부모 네임스페이스에서 정의된 파라미터를 포함합니다. 자세한 내용은 Ruby Grape의 include_parent_namespaces 문서를 참고합니다.

대부분의 경우 부모 네임스페이스의 파라미터는 제외해야 합니다:

declared(params, include_parent_namespaces: false)

declared(params)를 사용할 시점#

파라미터 해시를 메서드 호출의 인수로 전달할 때는 항상 declared(params)를 사용해야 합니다.

예를 들면:

# bad
User.create(params) # imagine the user submitted `admin=1`... :)

# good
User.create(declared(params, include_parent_namespaces: false).to_h)
Note

declared(params)는 Hashie::Mash 객체를 반환하므로, 반드시 .to_h를 호출해야 합니다.

다만 단일 요소에 접근할 때는 params[key]를 직접 사용할 수 있습니다.

예를 들면:

# good
Model.create(foo: params[:foo])

배열 타입#

Grape v1.3 이상에서는 배열 타입을 coerce_with 블록으로 정의해야 하며, 그렇지 않으면 API 요청에서 문자열이 전달될 때 파라미터가 검증에 실패합니다. 자세한 내용은 Grape 업그레이드 문서를 참고합니다.

nil 입력의 자동 강제 변환#

Grape v1.3.3 이전에는 nil 값을 가진 배열 파라미터가 자동으로 빈 배열로 강제 변환되었습니다. 하지만 v1.3.3의 이 풀 리퀘스트 이후로는 더 이상 그렇지 않습니다. 예를 들어 옵션 파라미터가 있는 PUT /test 요청을 정의했다고 가정해 봅니다:

optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The user ids for this rule'

보통 PUT /test?user_ids 요청을 보내면 Grape는 { user_ids: nil } 형태의 params를 전달합니다.

이는 빈 배열을 기대하지만 nil 입력을 제대로 처리하지 않는 엔드포인트에서 오류를 일으킬 수 있습니다. 이전 동작을 유지하기 위해 모든 API 호출의 before 블록에서 사용하는 coerce_nil_params_to_array! 헬퍼 메서드가 있습니다:

before do
  coerce_nil_params_to_array!
end

이 변경 이후로는 PUT /test?user_ids 요청을 보내면 Grape가 params를 { user_ids: [] }로 전달합니다.

이를 더 쉽게 만들기 위한 Grape 트래커의 미해결 이슈가 있습니다.

Workhorse 지원 업로드#

파일 콘텐츠를 받는 모든 REST API 엔드포인트는 Workhorse 지원 업로드를 사용해야 합니다.

구현 세부 사항은 Workhorse 업로드 문서를 참고합니다.

HTTP 상태 헬퍼 사용#

200이 아닌 HTTP 응답에는 올바른 동작을 보장하기 위해 lib/api/helpers.rb에서 제공하는 헬퍼(예: not_found!, no_content!)를 사용합니다. 이 헬퍼들은 Grape 내부에서 throw를 실행해 엔드포인트 실행을 중단시킵니다.

DELETE 요청에서도 일반적으로 destroy_conditionally! 헬퍼를 사용하는 것이 좋습니다. 이 헬퍼는 기본적으로 성공 시 204 No Content 응답을, 주어진 If-Unmodified-Since 헤더가 범위를 벗어나면 412 Precondition Failed 응답을 반환합니다. 이 헬퍼는 전달된 리소스에서 #destroy를 호출하지만, 블록을 전달해 삭제 방법을 직접 구현할 수도 있습니다.

HTTP 메서드 선택#

새 API 라우트를 정의할 때는 올바른 HTTP 요청 메서드를 사용합니다.

PATCH와 PUT 중 선택#

Rails 애플리케이션에서는 PATCH와 PUT 요청 메서드 모두 컨트롤러의 update 메서드로 라우팅됩니다. 하지만 GitLab API를 작성할 때 사용하는 프레임워크인 Grape에서는 업데이트를 수행하는 엔드포인트에 PATCH나 PUT HTTP 메서드를 명시적으로 지정해야 합니다.

엔드포인트가 리소스의 모든 속성을 업데이트하면 PUT 요청 메서드를 사용합니다. 엔드포인트가 리소스의 일부 속성만 업데이트하면 PATCH 요청 메서드를 사용합니다.

PATCH의 좋은 예: PATCH /projects/:id/protected_branches/:name PUT의 좋은 예: PUT /projects/:id/merge_requests/:merge_request_iid/approve

좋은 PUT 엔드포인트는 대개 ID와 동사만 가집니다(위 예시에서는 "approve"). 또는 단일 값만 가지고 키/값 쌍을 나타냅니다.

Rails 블로그에는 업데이트를 수행하는 웹 API 엔드포인트에 보통 PATCH가 가장 적합한 이유가 자세히 설명되어 있습니다.

GitLab Rails 코드베이스에서 API 경로 헬퍼 사용#

상대 URL 아래에 GitLab을 설치하는 것을 지원하므로, Grape가 생성하는 API 경로 헬퍼를 사용할 때는 이를 고려해야 합니다. 이런 API 경로 헬퍼는 반드시 expose_path 헬퍼 호출로 감싸야 합니다.

예를 들면:

- endpoint = expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid))

커스텀 검증기#

API 요청의 일부 파라미터를 검증하기 위해, 이를 (예를 들어 Gitaly로) 더 전달하기 전에 검증합니다. 다음은 지금까지 추가한 커스텀 검증기와 그 사용법입니다. 새 커스텀 검증기를 추가하는 방법에 대한 가이드도 작성했습니다.

커스텀 검증기 사용#

  • FilePath:

    GitLab은 파일 경로를 순회해야 하는 다양한 기능을 지원합니다. FilePath 검증기는 여러 경우에 대해 파라미터 값을 검증합니다. 주로 경로가 상대 경로인지, File::Separator를 사용한 ../../ 상대 경로 순회를 포함하는지, 그리고 경로가 절대 경로인지(예: /etc/passwd/) 확인합니다. 기본적으로 절대 경로는 허용되지 않습니다. 다만 다음과 같은 방식으로 허용할 절대 경로의 허용 목록을 선택적으로 전달할 수 있습니다: requires :file_path, type: String, file_path: { allowlist: ['/foo/bar/', '/home/foo/', '/app/home'] }

  • Git SHA:

    Git SHA 검증기는 Git SHA 파라미터가 유효한 SHA인지 확인합니다. 이는 commit.rb 파일에 언급된 정규식을 사용해 확인합니다.

  • Absence:

    Absence 검증기는 주어진 파라미터 해시에 특정 파라미터가 없는지 확인합니다.

  • IntegerNoneAny:

    IntegerNoneAny 검증기는 주어진 파라미터 값이 Integer, None, Any 중 하나인지 확인합니다. 이 값들 중 하나일 때만 요청을 진행시킵니다.

  • ArrayNoneAny:

    ArrayNoneAny 검증기는 주어진 파라미터 값이 Array, None, Any 중 하나인지 확인합니다. 이 값들 중 하나일 때만 요청을 진행시킵니다.

  • EmailOrEmailList:

    EmailOrEmailList 검증기는 문자열이나 문자열 목록의 값이 유효한 이메일 주소로만 구성되어 있는지 확인합니다. 모든 이메일 주소가 유효한 목록만 요청을 진행시킵니다.

새 커스텀 검증기 추가#

커스텀 검증기는 파라미터를 플랫폼에 보내 추가로 처리하기 전에 검증하는 좋은 방법입니다. 처음부터 유효하지 않은 파라미터를 식별하면 서버와 플랫폼 사이의 왕복을 줄일 수 있습니다.

커스텀 검증기를 추가해야 한다면, validators 디렉터리 안에 자신만의 파일로 추가합니다. API를 추가할 때 Grape를 사용하므로 검증기 클래스는 Grape::Validations::Validators::Base 클래스를 상속합니다. 이제 params 해시와 검증할 param 이름, 이 두 파라미터를 받는 validate_param! 메서드를 정의하기만 하면 됩니다.

이 메서드의 본문은 파라미터 값을 검증하는 실제 작업을 수행하고 호출한 메서드에 적절한 오류 메시지를 반환합니다.

마지막으로, 다음 줄을 사용해 검증기를 등록합니다:

Grape::Validations.register_validator(<validator name as symbol>, ::API::Helpers::CustomValidators::)

검증기를 추가한 뒤에는 validators 디렉터리 안에 자신만의 파일로 rspec 테스트도 반드시 추가합니다.

내부 API#

내부 API는 내부 용도로 문서화되어 있습니다. 어떤 컴포넌트가 어떤 엔드포인트를 사용하는지 알 수 있도록 최신 상태로 유지합니다.

N+1 문제 피하기#

API 엔드포인트에서 레코드 컬렉션을 반환할 때 흔히 발생하는 N+1 문제를 피하려면 즉시 로딩(eager loading)을 사용해야 합니다.

API 안에서 이를 수행하는 표준적인 방법은, 모델이 API에서 반환되는 연관 관계와 데이터를 미리 로드하는 with_api_entity_associations라는 스코프를 구현하는 것입니다. 이 스코프의 예시는 다음에서 확인할 수 있습니다. Issue 모델

동일한 모델이 API에서 여러 엔티티를 가지는 경우 (예: UserBasic, User, UserPublic)에는 이 스코프를 적용할지 재량으로 판단해야 합니다. 가장 기본적인 엔티티에 맞춰 최적화하고, 이후 엔티티들이 그 스코프 위에 쌓아 올리는 방식일 수도 있습니다.

with_api_entity_associations 스코프는 할 일 API에서 반환될 때 Todo의 _대상(target)_에 대해서도 데이터를 자동으로 미리 로드합니다.

미리 로드에 대한 더 많은 맥락과 논의는 이 스코프를 도입한 이 머지 리퀘스트를 참고합니다.

테스트로 검증#

API 엔드포인트가 컬렉션을 반환할 때는, 지금뿐 아니라 앞으로도 그 엔드포인트에 N+1 문제가 없는지 확인하는 테스트를 항상 추가합니다. 이는 ActiveRecord::QueryRecorder를 사용해 할 수 있습니다.

예시:

def make_api_request
  get api('/foo', personal_access_token: pat)
end

it 'avoids N+1 queries', :request_store do
  # Firstly, record how many PostgreSQL queries the endpoint will make
  # when it returns a single record
  create_record

  control = ActiveRecord::QueryRecorder.new { make_api_request }

  # Now create a second record and ensure that the API does not execute
  # any more queries than before
  create_record

  expect { make_api_request }.not_to exceed_query_limit(control)
end

테스트#

새 API 엔드포인트의 테스트를 작성할 때는 /spec/fixtures/api/schemas에 있는 스키마 픽스처를 사용하는 것을 고려합니다. 응답이 주어진 스키마와 일치하는지 expect로 확인할 수 있습니다:

expect(response).to match_response_schema('merge_requests')

테스트에서 N+1 성능 검증하기도 참고합니다.

changelog 항목 포함#

클라이언트에 노출되는 모든 변경 사항에는 changelog 항목을 포함해야 합니다. 여기에는 내부 API가 포함되지 않습니다.