API 스타일 가이드

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

인스턴스 변수

인스턴스 변수를 사용하지 마십시오. 불필요합니다. (우리가 Rails 뷰에서 하는 것처럼 접근할 필요가 없기 때문에), 로컬 변수를 사용하세요.

엔터티

엔드포인트의 payload를 나타내기 위해 항상 엔터티를 사용하세요.

문서

새로운 또는 업데이트된 API 엔드포인트마다 문서가 있어야 합니다. 단, 내부적이거나 기능 플래그 뒤에 숨겨진 경우를 제외하고요.
문서는 동일한 병합 요청 안에 있어야 하며, 엄격하게 필요한 경우에는 원본 병합 요청과 동일한 마일스톤을 가진 후속문서에 작성되어야 합니다.

자세한 내용은 문서 스타일 가이드 RESTful API 페이지에서 API 리소스를 Markdown 및 OpenAPI 정의 파일로 문서화하는 방법을 확인하세요.

메서드 및 매개변수 설명

각 메서드는 Grape DSL를 사용하여 설명되어야 합니다. (좋은 예는environments.rb 에 있습니다):

  • 메서드 요약에 대해 desc를 사용하세요. 추가 세부 정보를 위해 블록을 전달해야 합니다. 예를 들어:
    • 엔드포인트가 추가된 GitLab 버전. 기능 플래그 뒤에 숨겨졌다면 대신 언급하세요: 이 기능은 :feature\_flag\_symbol 기능 플래그에 의해 제한됩니다.
    • 엔드포인트가 폐지됐는지 여부, 그리고 그 경우 폐지 예정일
  • 메서드 매개변수에 대한 설명으로 params를 사용하세요. 이는 매개변수의 설명, 유효성 검사 및 매개변수 강제 역할을 합니다.

다음은 좋은 예입니다:

desc '모든 브로드캐스트 메시지 가져오기' do
  detail '이 기능은 GitLab 8.12에서 소개되었습니다.'
  success Entities::System::BroadcastMessage
end
params do
  optional :page,     type: Integer, desc: '현재 페이지 번호'
  optional :per_page, type: Integer, desc: '페이지당 메시지 수'
end
get do
  messages = System::BroadcastMessage.all

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

호환성에 영향을 미치는 변경사항

우리는 주요 GitLab 릴리스에서 REST API v4에 대한 호환성이 깨지는 변경을 가해서는 안됩니다.

우리 REST API는 GitLab 버전과는 독립적으로 버전이 지정됩니다. 현재 REST API 버전은 4입니다. REST API에 대한 의미 체계적으로 버전을 따를 것을 약속하는데, 이는 5로의 주요 버전 변경(아마도 5)이 일어나기 전까지는 호환성이 깨지는 변화를 가할 수 없음을 의미합니다.

버전 5가 예정되어 있지 않기 때문에, 우리는 드물게 예외를 허용합니다.

호환성 깨뜨리는 대신 역호환성 수용

API에서 역관용성을 유지하려면 종종 변경된 기능을 이전 API 스키마로 계속 적응시켜서 역호환성을 유지할 수 있습니다. 예를 들어, 우리 REST API는 work_in_progressdraft 필드를 모두 노출합니다 (https://gitlab.com/gitlab-org/gitlab/-/blob/c104f6b8/lib/api/entities/merge_request_basic.rb#L43-47).

예외

이 예외는 다음 경우에만 적용됩니다:

  • 주요 GitLab 릴리스에서 기능을 제거해야 하는 경우.
  • 역호환성을 유지할 수 없는 경우 어떠한 형태에서도

이 예외는 드물어야 합니다.

이러한 예외 상황에서도, 필드나 매개변수를 제거하는 대신 항상 다음을 수행해야합니다:

  • 필드에 대해 빈 응답 반환 (예: "null" 또는 []).
  • 인수를 no-op으로 전환.

호환성을 깨뜨리는 변경사항이란

호환성을 깨뜨리는 변경사항의 몇 가지 예는 다음과 같습니다:

  • 필드, 인수 또는 열거형 값의 제거 또는 이름 변경. JSON 응답에 있어서, 필드는 JSON 키입니다.
  • 엔드포인트의 제거.
  • 새로운 리디렉션 추가 (모든 클라이언트가 리디렉션을 따르지는 않음).
  • 어떤 응답의 콘텐츠 유형 변경.
  • 응답의 필드 유형 변경. JSON 응답에 있어서, Number, String, Boolean, Array, 또는 Object 유형 중 하나를 다른 유형으로 변경하면 됩니다.
  • 새로운 필수 인수의 추가.
  • 인증, 권한 또는 다른 헤더 요구 사항의 변경.
  • 500 이외의 어떠한 상태 코드 변경.

호환성을 깨뜨리지 않는 변경사항이란

호환성을 깨뜨리지 않는 변경사항의 몇 가지 예는 다음과 같습니다:

  • 포함적 변경, 예를 들어, 엔드포인트, 필수가 아닌 인수, 필드 또는 열거형 값 추가.
  • 오류 메시지 변경.
  • 500 상태 코드부터 지원되는 어떠한 상태 코드로의 변경 (이는 버그 수정입니다).
  • 응답에 반환된 필드 순서 변경.

선언된 매개변수

Grape를 통해 params 블록에서 선언된 매개변수만 액세스할 수 있습니다. 허용되지 않은 매개변수를 전달하지만 필터링해줍니다.

https://github.com/ruby-grape/grape#declared

부모 네임스페이스에서 매개변수 제외하기

기본적으로 declared(params)에는 모든 부모 네임스페이스에서 정의된 매개변수가 포함됩니다.

https://github.com/ruby-grape/grape#include-parent-namespaces

대부분의 경우에는 부모 네임스페이스에서 매개변수를 제외해야 합니다:

declared(params, include_parent_namespaces: false)

declared(params)를 사용해야 하는 경우

매개변수 해시를 메소드 호출의 인수로 전달할 때는 항상 declared(params)를 사용해야 합니다.

예를 들면:

# 안 좋은 예
User.create(params) # 사용자가 `admin=1`을 제출했다고 상상해보세요... :)

# 좋은 예
User.create(declared(params, include_parent_namespaces: false).to_h)

참고: declared(params)Hashie::Mash 객체를 반환하며, 여기에 .to_h를 호출해야 합니다.

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

예를 들면:

# 좋은 예
Model.create(foo: params[:foo])

배열 유형

Grape v1.3+에서는 배열 유형을 coerce_with 블록이나 매개변수와 함께 정의해야 하며, API 요청에서 문자열을 전달할 때 유효성 검사에 실패합니다. 자세한 내용은 Grape 업그레이드 문서를 참조하세요.

nil 입력의 자동 강제 변환

Grape v1.3.3 이전에는 nil 값을 갖는 배열 매개변수가 자동으로 빈 배열로 강제 변환되었습니다. 그러나 v1.3.3의 이 PR로 인해 더 이상 그렇지 않습니다. 예를 들어 선택적 매개변수를 갖는 PUT /test 요청을 정의한다고 가정해보면:

optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'This rule에 대한 사용자 ID'

일반적으로 PUT /test?user_ids로 요청하면 Grape가 params{ user_ids: nil }로 전달합니다.

이는 빈 배열을 예상하는 엔드포인트에서 오류를 발생시킬 수 있으며, 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 트래커에 열린 이슈가 있습니다.

HTTP 상태 도우미 사용하기

200이 아닌 HTTP 응답에 대해서는 lib/api/helpers.rb에서 제공된 도우미를 사용하여 올바른 동작을 보장하세요 (not_found! 또는 no_content! 같은). 이러한 도우미는 Grape 내에서 throw를 호출하며 엔드포인트의 실행이 중단됩니다.

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

HTTP 동사 선택하기

API 라우트를 정의할 때, 올바른 HTTP 요청 방법을 사용하세요.

PATCHPUT 선택하기

Rails 애플리케이션에서는 PATCHPUT 요청 방법이 모두 컨트롤러의 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 경로 도우미를 사용할 때에는 expose_path 도우미 호출로 둘러싸야 합니다.

예를 들어:

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

사용자 정의 유효성 검사기

API 요청 내에서 일부 매개변수를 유효성 검사하기 위해 해당 매개변수를 더 전달하기 전에 유효성을 검사합니다. 다음은 현재까지 추가한 사용자 정의 유효성 검사기와 그 사용 방법입니다. 새로운 사용자 정의 유효성 검사기를 추가하는 방법에 대한 안내도 작성했습니다.

사용자 정의 유효성 검사기 사용

  • FilePath:

    GitLab은 파일 경로를 탐색해야 하는 여러 기능을 지원합니다. FilePath 유효성 검사기는 매개변수 값에 대해 다른 경우를 유효성을 검사합니다. 주로 상대적 경로를 포함하고 있는지, ../../ 상대적 탐색을 사용하고 있는지, 절대 경로인지를 확인합니다. 예를 들어, /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 파일에서 언급된 정규식을 사용하여 확인합니다.

  • 부재:

    부재 유효성 검사기는 특정 매개변수가 주어진 매개변수 해시에 없는지 확인합니다.

  • 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(<검사기 이름(심볼로)>, ::API::Helpers::CustomValidators::<사용자 정의 검사기 클래스 이름>)

유효성 검사기를 추가한 후에는 해당 사용자 정의 유효성 검사기에 대한 rspecvalidators 디렉토리 내의 별도 파일에 추가해야 합니다.

내부 API

내부 API는 내부 사용을 위해 문서화되었습니다. 각 구성 요소가 어떤 엔드포인트를 사용하는지 알 수 있도록 최신 상태를 유지하세요.

N+1 문제 피하기

API 엔드포인트에서 레코드 컬렉션을 반환할 때 일반적인 N+1 문제를 피하기 위해 즉시 로딩을 사용해야 합니다.

API 내에서 이를 수행하는 표준 방법은 모델이 with_api_entity_associations라고 하는 스코프를 구현하여 API에서 사전로드된 연관 및 데이터를 반환하는 것입니다. 이 스코프의 예는 Issue 모델에서 볼 수 있습니다.

동일한 모델이 API에서 여러 엔터티를 가지는 상황(예: UserBasic, User, UserPublic 등)에서는 이러한 스코프를 적용할 때 각각의 엔터티에 대해 판단을 가할 필요가 있습니다. 연속적인 엔터티가 이러한 스코프를 기반으로 만들어질 수 있도록 기본적인 엔터티에 대해 최적화해야 할 수도 있습니다.

with_api_entity_associations 스코프는 또한 할 일 API에서 Todo의 _대상_을 반환할 때 데이터를 자동으로 사전로드합니다.

더 많은 콘텍스트 및 사전로드에 대한 논의는 이 머지 리퀘스트에서 볼 수 있습니다.

테스트로 검증하기

API 엔드포인트가 컬렉션을 반환할 때 항상 N+1 문제가 없음을 확인하는 테스트를 추가해야 합니다. 우리는 ActiveRecord::QueryRecorder를 사용하여 현재와 미래에도 N+1 문제가 없음을 확인할 수 있습니다.

예시:

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

it 'N+1 쿼리를 피합니다', :request_store do
  # 먼저, API가 단일 레코드를 반환할 때 포스트그레SQL 쿼리가 몇 번 실행되는지 기록합니다
  create_record

  control = ActiveRecord::QueryRecorder.new { make_api_request }

  # 이제 두 번째 레코드를 생성하고 API가 이전보다 더 많은 쿼리를 실행하지 않도록 확인합니다
  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을 참조하세요.

변경 로그 항목 포함

모든 클라이언트에게 노출되는 변경 사항은 반드시 변경 로그 항목을 포함해야 합니다. 내부 API는 이에 해당하지 않습니다.