ホームページ > バックエンド開発 > Golang > REST API の再考: ゴールデン API の構築

REST API の再考: ゴールデン API の構築

Susan Sarandon
リリース: 2024-12-06 22:05:23
オリジナル
214 人が閲覧しました

Rethinking our REST API: Building the Golden API

どの企業も、ある時点で、これまで使用してきたツールを停止し、再評価する必要がある岐路に達します。私たちにとって、その瞬間は、Web ダッシュボードを支えている API が管理不能になり、テストが難しくなり、コードベースに設定した基準を満たしていないことに気づいたときでした。

Arcjet は主に、開発者がボット検出、電子メール検証、PII 検出などのセキュリティ機能を実装するのに役立つ、コードとしてのセキュリティ SDK です。これは、高性能かつ低遅延の意思決定 gRPC API と通信します。

当社の Web ダッシュボードは、主にサイト接続の管理と処理されたリクエスト分析の確認に別個の REST API を使用しますが、これには新しいユーザーのサインアップとアカウントの管理も含まれており、依然として製品の重要な部分であることを意味します。

Rethinking our REST API: Building the Golden API
Arcjet ダッシュボードのリクエスト分析ページのスクリーンショット。

そこで、今回は保守性、パフォーマンス、スケーラビリティに焦点を当てて、API をゼロから再構築するという課題に取り組むことにしました。しかし、私たちは大規模な書き換えプロジェクトに着手するつもりはありませんでした。決してうまくいきません。代わりに、新しい基盤を構築し、単一の API エンドポイントから始めることにしました。

この投稿では、これにどのようにアプローチしたかについて説明します。

以前はアークジェットで

速度が最優先事項だった場合、Next.js は、フロントエンドが使用できる API エンドポイントを構築するための最も便利なソリューションを提供しました。これにより、単一のコードベース内でシームレスなフルスタック開発が可能になり、Vercel 上にデプロイしたためインフラストラクチャについてあまり心配する必要がなくなりました。

私たちはコード SDK と 低レイテンシの意思決定 API としてのセキュリティに重点を置いていました。そのため、フロントエンド ダッシュボードでは、このスタックにより、ほとんど手間をかけずに機能のプロトタイプを迅速に作成できるようになりました。

私たちのスタック: Next.js、DrizzleORM、useSWR、NextAuth

しかし、製品が進化するにつれて、すべての API エンドポイントとフロントエンド コードを同じプロジェクト内で組み合わせると、混乱が生じることがわかりました。

API のテストが面倒になり (そしてとにかく Next.js で行うのは非常に難しい)、内部外部 の両方を処理できるシステムが必要になりました。 🎜>消費。より多くのプラットフォーム (Vercel、Fly.io、Netlify など) と統合するにつれて、開発のスピードだけでは十分ではないことに気づきました。より堅牢なソリューションが必要でした。

このプロジェクトの一環として、Vercel がデータベースの公開をどのように要求しているかについて、根強いセキュリティ上の懸念にも対処したいと考えました。 エンタープライズ「セキュア コンピューティング」 の料金を支払わない限り、リモート データベースに接続するにはパブリック エンドポイントが必要です。私たちはデータベースをロックダウンして、プライベート ネットワーク経由でのみアクセスできるようにすることを好みます。多層防御が重要であり、これはもう 1 つの保護層となります。

この結果、フロントエンド UI をバックエンド API から切り離すことを決定しました。

「ゴールデンAPI」のご紹介

「ゴールデン API」とは何ですか?これは特定のテクノロジーやフレームワークではなく、適切に構築された API を定義する理想的な一連の原則です。開発者は言語やフレームワークに対して独自の好みを持っているかもしれませんが、高品質の API を構築するためにほとんどの技術スタックにわたって有効であり、ほとんどの人が同意できる特定の概念があります。

1. パフォーマンスとスケーラビリティ

当社には、すでに高パフォーマンス API の提供の経験があります。当社の 意思決定 API は、お客様の近くにデプロイされ、Kubernetes を使用して動的に拡張され、低遅延の応答用に最適化されています。 .

サーバーレス環境と他のプロバイダーを検討しましたが、既存の k8s クラスターがすでに稼働しているため、Octopus Deploy によるデプロイメント、Grafana Yeter、Loki、Prometheus などによるモニタリングなど、インフラストラクチャを再利用することが最も合理的でした。

Rust と Go の短い内部ベークオフの後、そのシンプルさ、速度、およびスケーラブルなネットワーク サービスの構築に対する優れたサポートという本来の目標をどの程度達成しているかという理由から Go を選択しました。 。また、私たちはすでにこれを Decision API にも使用しており、Go API の操作方法を理解しているため、最終的な決定を下すことができました。

2. 包括的かつ明確なドキュメント

バックエンド API を Go に切り替えるのは、そのシンプルさと優れたツールのおかげで簡単でした。しかし、問題が 1 つありました。それは、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 のみをサポートしています (多くのライブラリが 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 ファイルのセットで、1 つはサーバー スケルトンを含み、もう 1 つはハンドラー実装を含みます。以下は、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 推奨のフェッチ API を使用して行われました。より動的なビューを実現するために、一部のコンポーネントはフェッチに加えて 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 仕様は、その逆ではなくそこからコードが生成されるため、唯一の信頼できる情報源を提供します。生成されたクライアントを使用するということは、チームが API を簡単に操作できることを意味します。
  • セキュリティと認証 – すでに実稼働環境に Go をデプロイしているため、セキュリティ慣行をコピーできます。認証は NextAuth によって処理されます。
  • テスト容易性 – ハンドラーの単体テストと、Testcontainers を使用した統合テストを実装しました。これは Next.js API の大幅な改善です。

さらに次のように進めました:

  • モニタリング – 既存の k8s クラスターにデプロイするということは、すでに設定されているトレース、ロギング、およびメトリクスの機能を継承することを意味します。 OpenTelemetry インストルメンテーションを Pin に追加するのは簡単でした。
  • シンプルさ – Go は元々 API 用に設計されており、それがそれを示しています。私たちのコードははるかにクリーンで保守しやすくなりました。

最終的には、結果に満足しています。私たちの API はより高速で、より安全で、より適切にテストされています。 Go への移行には価値があり、製品の成長に合わせて API を拡張し、維持することができるようになりました。

以上がREST API の再考: ゴールデン API の構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート