> 백엔드 개발 > Golang > REST API 재고하기: 골든 API 구축

REST API 재고하기: 골든 API 구축

Susan Sarandon
풀어 주다: 2024-12-06 22:05:23
원래의
213명이 탐색했습니다.

Rethinking our REST API: Building the Golden API

언젠가는 모든 회사가 멈춰서 지금까지 사용해 온 도구를 재평가해야 하는 교차로에 도달하게 됩니다. 우리에게는 웹 대시보드를 지원하는 API가 관리하기 어렵고, 테스트하기 어렵고, 코드베이스에 대해 설정한 표준을 충족하지 못한다는 사실을 깨달은 순간이 왔습니다.

Arcjet은 주로 개발자가 봇 감지, 이메일 확인, PII 감지와 같은 보안 기능을 구현하는 데 도움이 되는 코드형 보안 SDK입니다. 이는 고성능, 저지연 결정 gRPC API와 통신합니다.

저희 웹 대시보드는 주로 사이트 연결을 관리하고 처리된 요청 분석을 검토하기 위해 별도의 REST API를 사용합니다. 그러나 여기에는 신규 사용자 가입 및 계정 관리도 포함되므로 이는 여전히 제품의 중요한 부분입니다.

Rethinking our REST API: Building the Golden API
Arcjet 대시보드의 요청 분석 페이지 스크린샷

그래서 우리는 이번에는 유지 관리성, 성능 및 확장성에 중점을 두고 API를 처음부터 다시 구축하는 도전에 착수하기로 결정했습니다. 그러나 우리는 결코 잘 되지 않는 대규모 재작성 프로젝트에 착수하고 싶지 않았습니다. 대신 새로운 기반을 구축한 다음 단일 API 엔드포인트로 시작하기로 결정했습니다.

이 게시물에서는 이에 대한 접근 방식에 대해 논의하겠습니다.

이전에 Arcjet에서

속도가 최우선 과제였을 때 Next.js는 프런트엔드가 사용할 수 있는 API 엔드포인트를 구축하기 위한 가장 편리한 솔루션을 제공했습니다. 단일 코드베이스 내에서 원활한 풀 스택 개발이 가능했고 Vercel에 배포했기 때문에 인프라에 대해 너무 걱정할 필요가 없었습니다.

우리는 코드 SDK 및 낮은 지연 시간 결정 API로서의 보안에 중점을 두었습니다. 따라서 프런트엔드 대시보드의 경우 이 스택을 통해 마찰이 거의 없이 기능의 프로토타입을 빠르게 제작할 수 있었습니다.

우리 스택: Next.js, DrizzleORM, useSWR, NextAuth

그러나 제품이 발전하면서 모든 API 엔드포인트와 프런트엔드 코드를 동일한 프로젝트에 결합하면 혼란스러운 문제가 발생한다는 사실을 발견했습니다.

API 테스트가 번거로워졌고(어쨌든 Next.js로는 매우 어렵습니다) 내부외부 소비. 더 많은 플랫폼(Vercel, Fly.io, Netlify 등)과 통합하면서 개발 속도만으로는 충분하지 않다는 것을 깨달았습니다. 우리에게는 더욱 강력한 솔루션이 필요했습니다.

이 프로젝트의 일환으로 우리는 Vercel에서 데이터베이스를 공개적으로 노출하도록 요구하는 방식에 대한 지속적인 보안 문제를 해결하고 싶었습니다. 엔터프라이즈 "보안 컴퓨팅" 비용을 지불하지 않는 한 원격 데이터베이스에 연결하려면 공용 엔드포인트가 필요합니다. 우리는 개인 네트워크를 통해서만 액세스할 수 있도록 데이터베이스를 잠그는 것을 선호합니다. 심층적인 방어가 중요하며 이는 또 다른 보호 계층이 될 것입니다.

이로 인해 우리는 백엔드 API에서 프런트엔드 UI를 분리하기로 결정했습니다.

"골든 API" 소개

'골든 API'란 무엇인가요? 이는 특정 기술이나 프레임워크가 아니라 잘 구축된 API를 정의하는 이상적인 원칙 집합에 가깝습니다. 개발자는 언어와 프레임워크에 대해 각자 선호하는 것이 있을 수 있지만, 고품질 API를 구축하기 위해 대부분이 동의할 수 있는 기술 스택 전반에 걸쳐 유효한 특정 개념이 있습니다.

1. 성능과 확장성

저희는 이미 고성능 API를 제공한 경험이 있습니다.저희 Decision API는 고객 가까이에 배포되고 Kubernetes를 사용하여 동적으로 확장되며 짧은 지연 시간 응답에 최적화되어 있습니다. .

서버리스 환경과 기타 제공업체를 고려했지만 기존 k8s 클러스터가 이미 작동 중이므로 Octopus Deploy를 통한 배포, Grafana Jaeger, Loki, Prometheus 등을 통한 모니터링 등 인프라를 제자리에서 재사용하는 것이 가장 합리적이었습니다.

Rust와 Go의 짧은 내부 테스트 끝에 우리는 단순성, 속도 및 확장 가능한 네트워크 서비스 구축에 대한 탁월한 지원이라는 원래 목표를 얼마나 잘 달성하는지를 고려하여 Go를 선택했습니다. . 우리는 이미 Decision API에도 이를 사용하고 있으며 Go API 작동 방법을 이해하고 있어 최종 결정을 내렸습니다.

2. 포괄적이고 명확한 문서화

간단함과 훌륭한 도구의 가용성 덕분에 백엔드 API를 Go로 전환하는 것은 간단했습니다. 하지만 한 가지 문제가 있었습니다. 우리는 Next.js 프런트엔드를 유지하고 있었고 TypeScript 유형을 수동으로 작성하거나 새 API에 대한 별도의 문서를 유지하고 싶지 않았습니다.

우리 요구 사항에 딱 맞는 OpenAPI를 만나보세요. OpenAPI를 사용하면 프런트엔드와 백엔드 간의 계약을 정의하는 동시에 문서로도 사용할 수 있습니다. 이는 앱 양쪽의 스키마를 유지하는 문제를 해결합니다.

3. 보안 및 인증

NextAuth가 백엔드에서 비교적 간단하게 모방할 수 있기 때문에 Go에 인증을 통합하는 것은 그리 어렵지 않았습니다. NextAuth(현재 Auth.js)에는 세션을 확인하는 데 사용할 수 있는 API가 있습니다.

이는 OpenAPI 사양에서 생성된 프런트엔드에 TypeScript 클라이언트가 있어 백엔드 API에 대한 가져오기 호출을 수행할 수 있음을 의미합니다. 자격 증명은 가져오기 호출에 자동으로 포함되며 백엔드는 NextAuth로 세션을 확인할 수 있습니다.

4. 테스트 가능성

Go에서 모든 종류의 테스트를 작성하는 것은 매우 쉽고 HTTP 핸들러 테스트 주제를 다루는 많은 예제가 있습니다.

또한 Next.js API에 비해 새로운 Go API 엔드포인트에 대한 테스트를 작성하는 것이 훨씬 쉽습니다. 특히 인증된 상태와 실제 데이터베이스 호출을 테스트하고 싶기 때문입니다. Gin 라우터에 대한 테스트를 쉽게 작성할 수 있었고 Testcontainers를 사용하여 Postgres 데이터베이스에 대한 실제 통합 테스트를 시작할 수 있었습니다.

모든 것을 하나로 합치기

OpenAPI 사양 작성

우리는 API에 대한 OpenAPI 3.0 사양을 작성하는 것부터 시작했습니다. OpenAPI 우선 접근 방식은 구현 전에 API 계약 설계를 촉진하여 코드가 작성되기 전에 모든 이해 관계자(개발자, 제품 관리자 및 클라이언트)가 API의 동작 및 구조에 동의하도록 합니다. 이는 신중한 계획을 장려하고 확립된 모범 사례를 일관되고 준수하는 세심하게 계획된 API 설계로 이어집니다. 이것이 우리가 사양을 먼저 작성하고 그 반대로 코드를 생성하기로 선택한 이유입니다.

이를 위해 제가 선택한 도구는 API Fiddle이었습니다. 이 도구는 OpenAPI 사양을 빠르게 작성하고 테스트하는 데 도움이 됩니다. 하지만 API Fiddle은 OpenAPI 3.1(많은 라이브러리에서 이를 채택하지 않았기 때문에 사용할 수 없음)만 지원하므로 버전 3.0을 고수하고 사양을 직접 작성했습니다.

다음은 API 사양의 예입니다.

openapi: 3.0.0
info:
  title: Arcjet Sites API
  description: A CRUD API to manage sites.
  version: 1.0.0

servers:
  - url: <https://api.arcjet.com/v1>
    description: Base URL for all API operations

paths:
  /teams/{teamId}/sites:
    get:
      operationId: GetTeamSites
      summary: Get a list of sites for a team
      description: Returns a list of all Sites associated with a given Team.
      parameters:
        - name: teamId
          in: path
          required: true
          description: The ID of the team
          schema:
            type: string
      responses:
        "200":
          description: A list of sites
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Site"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Site:
      type: object
      properties:
        id:
          type: string
          description: The ID of the site
        name:
          type: string
          description: The name of the site
        teamId:
          type: string
          description: The ID of the team this site belongs to
        createdAt:
          type: string
          format: date-time
          description: The timestamp when the site was created
        updatedAt:
          type: string
          format: date-time
          description: The timestamp when the site was last updated
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: The timestamp when the site was deleted (if applicable)
    Error:
      required:
        - code
        - message
        - details
      properties:
        code:
          type: integer
          format: int32
          description: Error code
        message:
          type: string
          description: Error message
        details:
          type: string
          description: Details that can help resolve the issue
로그인 후 복사
로그인 후 복사

OpenAPI 사양을 갖춘 상태에서 OpenAPI 사양에서 Go 코드를 자동으로 생성하는 도구인 OAPI-codegen을 사용했습니다. 필요한 모든 유형, 핸들러 및 오류 처리 구조를 생성하여 개발 프로세스를 훨씬 원활하게 만듭니다.

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
로그인 후 복사
로그인 후 복사

출력은 Go 파일 세트로, 하나는 서버 뼈대를 포함하고 다른 하나는 핸들러 구현을 포함했습니다. 다음은 Site 개체에 대해 생성된 Go 유형의 예입니다.

// Site defines model for Site.
type Site struct {
    // CreatedAt The timestamp when the site was created
    CreatedAt *time.Time `json:"createdAt,omitempty"`

    // DeletedAt The timestamp when the site was deleted (if applicable)
    DeletedAt *time.Time `json:"deletedAt"`

    // Id The ID of the site
    Id *string `json:"id,omitempty"`

    // Name The name of the site
    Name *string `json:"name,omitempty"`

    // TeamId The ID of the team this site belongs to
    TeamId *string `json:"teamId,omitempty"`

    // UpdatedAt The timestamp when the site was last updated
    UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
로그인 후 복사
로그인 후 복사

생성된 코드를 사용하여 다음과 같은 API 핸들러 로직을 구현할 수 있었습니다.

func (s Server) GetTeamSites(w http.ResponseWriter, r *http.Request, teamId string) {
    ctx := r.Context()

    // Check user has permission to access team resources
    isAllowed, err := s.userIsAllowed(ctx, teamId)
    if err != nil {
        slog.ErrorContext(
            ctx,
            "failed to check permissions",
            slogext.Err("err", err),
            slog.String("teamId", teamId),
        )
        SendInternalServerError(ctx, w)
        return
    }

    if !isAllowed {
        SendForbidden(ctx, w)
        return
    }

    // Retrieve sites from database
    sites, err := s.repo.GetSitesForTeam(ctx, teamId)
    if err != nil {
        slog.ErrorContext(
            ctx,
            "list sites for team query returned an error",
            slogext.Err("err", err),
            slog.String("teamId", teamId),
        )
        SendInternalServerError(ctx, w)
        return
    }

    SendOk(ctx, w, sites)
}
로그인 후 복사

데이터베이스: DrizzleORM에서 벗어나기

Drizzle은 JS 프로젝트를 위한 훌륭한 ORM이므로 다시 사용하고 싶지만 데이터베이스 코드를 Next.js에서 이동한다는 것은 Go에도 비슷한 것이 필요하다는 것을 의미했습니다.

우리는 ORM으로 GORM을 선택하고 저장소 패턴을 사용하여 데이터베이스 상호 작용을 추상화했습니다. 이를 통해 깨끗하고 테스트 가능한 데이터베이스 쿼리를 작성할 수 있었습니다.

type ApiRepo interface {
    GetSitesForTeam(ctx context.Context, teamId string) ([]Site, error)
}

type apiRepo struct {
    db *gorm.DB
}

func (r apiRepo) GetSitesForTeam(ctx context.Context, teamId string) ([]Site, error) {
    var sites []Site
    result := r.db.WithContext(ctx).Where("team_id = ?", teamId).Find(&sites)
    if result.Error != nil {
        return nil, ErrorNotFound
    }
    return sites, nil
}
로그인 후 복사

테스트. 모든 것

테스트는 우리에게 매우 중요합니다. 우리는 모든 데이터베이스 호출이 제대로 테스트되었는지 확인하고 싶었기 때문에 Testcontainers를 사용하여 테스트용 실제 데이터베이스를 가동하고 프로덕션 설정을 밀접하게 반영했습니다.

openapi: 3.0.0
info:
  title: Arcjet Sites API
  description: A CRUD API to manage sites.
  version: 1.0.0

servers:
  - url: <https://api.arcjet.com/v1>
    description: Base URL for all API operations

paths:
  /teams/{teamId}/sites:
    get:
      operationId: GetTeamSites
      summary: Get a list of sites for a team
      description: Returns a list of all Sites associated with a given Team.
      parameters:
        - name: teamId
          in: path
          required: true
          description: The ID of the team
          schema:
            type: string
      responses:
        "200":
          description: A list of sites
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Site"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Site:
      type: object
      properties:
        id:
          type: string
          description: The ID of the site
        name:
          type: string
          description: The name of the site
        teamId:
          type: string
          description: The ID of the team this site belongs to
        createdAt:
          type: string
          format: date-time
          description: The timestamp when the site was created
        updatedAt:
          type: string
          format: date-time
          description: The timestamp when the site was last updated
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: The timestamp when the site was deleted (if applicable)
    Error:
      required:
        - code
        - message
        - details
      properties:
        code:
          type: integer
          format: int32
          description: Error code
        message:
          type: string
          description: Error message
        details:
          type: string
          description: Details that can help resolve the issue
로그인 후 복사
로그인 후 복사

테스트 환경을 설정한 후 프로덕션 환경에서와 마찬가지로 모든 CRUD 작업을 테스트하여 코드가 올바르게 작동하는지 확인했습니다.

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
로그인 후 복사
로그인 후 복사

API 핸들러를 테스트하기 위해 Go의 httptest 패키지를 사용하고 Mockery를 사용하여 데이터베이스 상호 작용을 모의했습니다. 이를 통해 데이터베이스 문제에 대한 걱정 없이 API 로직 테스트에 집중할 수 있었습니다.

// Site defines model for Site.
type Site struct {
    // CreatedAt The timestamp when the site was created
    CreatedAt *time.Time `json:"createdAt,omitempty"`

    // DeletedAt The timestamp when the site was deleted (if applicable)
    DeletedAt *time.Time `json:"deletedAt"`

    // Id The ID of the site
    Id *string `json:"id,omitempty"`

    // Name The name of the site
    Name *string `json:"name,omitempty"`

    // TeamId The ID of the team this site belongs to
    TeamId *string `json:"teamId,omitempty"`

    // UpdatedAt The timestamp when the site was last updated
    UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
로그인 후 복사
로그인 후 복사

프런트엔드 소비: OpenAPI 기반 프런트엔드

API를 테스트하고 배포한 후 프런트엔드에 관심을 돌렸습니다.

이전 API 호출은 캐싱이 내장된 Next.js의 권장 fetch API를 사용하여 이루어졌습니다. 보다 동적인 보기를 위해 일부 구성 요소는 fetch 외에 SWR을 사용했기 때문에 유형이 안전하고 자동으로 다시 로드되는 데이터 가져오기 호출을 받을 수 있습니다.

프런트엔드에서 API를 사용하기 위해 OpenAPI 스키마를 기반으로 TypeScript 유형을 생성하는 openapi-typescript 라이브러리를 사용했습니다. 이를 통해 데이터 모델을 수동으로 동기화할 필요 없이 백엔드를 프런트엔드와 쉽게 통합할 수 있었습니다. 여기에는 내부적으로 가져오기를 사용하고 스키마와 동기화하는 Tanstack Query가 내장되어 있습니다.

우리는 점차적으로 API 엔드포인트를 새로운 Go 서버로 마이그레이션하고 그 과정에서 작은 개선을 이루고 있습니다. 브라우저 검사기를 열면 api.arcjet.com으로 이동하는 새 요청을 볼 수 있습니다

Rethinking our REST API: Building the Golden API
새로운 Go 백엔드에 대한 API 호출을 보여주는 브라우저 검사기 스크린샷.

골든 체크리스트

그렇다면 우리는 찾기 힘든 Golden API를 달성했나요? 확인란을 선택해 보겠습니다.

  • 성능 및 확장성 – API는 이미 성능에 맞춰 조정된 기존 k8s 클러스터에 배포됩니다. 우리는 상세한 지표를 보유하고 있으며 필요에 따라 확장할 수 있습니다.
  • 포괄적이고 명확한 문서 – OpenAPI 사양은 코드가 반대가 아닌 OpenAPI 사양에서 생성되기 때문에 단일 정보 소스를 제공합니다. 생성된 클라이언트를 사용하면 우리 팀이 API 작업을 쉽게 할 수 있습니다.
  • 보안 및 인증 – 우리는 보안 관행을 복사할 수 있도록 이미 프로덕션에 Go를 배포했습니다. 인증은 NextAuth에서 처리합니다.
  • 테스트 용이성 – 핸들러의 단위 테스트와 Testcontainers를 사용한 통합 테스트를 구현했는데, 이는 Next.js API에 비해 크게 개선되었습니다.

더 나아가 다음을 수행했습니다.

  • 모니터링 – 기존 k8s 클러스터에 배포한다는 것은 이미 설정된 추적, 로깅 및 측정 기능을 상속받았다는 의미입니다. Gin에 OpenTelemetry 계측을 추가하는 것은 쉽지 않았습니다.
  • 단순성 – Go는 원래 API용으로 설계되었으며 그 사실을 보여줍니다. 우리의 코드는 훨씬 더 깔끔하고 유지 관리가 더 쉽습니다.

결국 결과에 만족합니다. 우리의 API는 더 빠르고, 더 안전하며, 더 잘 테스트되었습니다. Go로의 전환은 그만한 가치가 있었고 이제 우리는 제품이 성장함에 따라 API를 확장하고 유지 관리할 수 있는 더 나은 위치에 있습니다.

위 내용은 REST API 재고하기: 골든 API 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿