どの企業も、ある時点で、これまで使用してきたツールを停止し、再評価する必要がある岐路に達します。私たちにとって、その瞬間は、Web ダッシュボードを支えている API が管理不能になり、テストが難しくなり、コードベースに設定した基準を満たしていないことに気づいたときでした。
Arcjet は主に、開発者がボット検出、電子メール検証、PII 検出などのセキュリティ機能を実装するのに役立つ、コードとしてのセキュリティ SDK です。これは、高性能かつ低遅延の意思決定 gRPC API と通信します。
当社の Web ダッシュボードは、主にサイト接続の管理と処理されたリクエスト分析の確認に別個の REST 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 の提供の経験があります。当社の 意思決定 API は、お客様の近くにデプロイされ、Kubernetes を使用して動的に拡張され、低遅延の応答用に最適化されています。 .
サーバーレス環境と他のプロバイダーを検討しましたが、既存の k8s クラスターがすでに稼働しているため、Octopus Deploy によるデプロイメント、Grafana Yeter、Loki、Prometheus などによるモニタリングなど、インフラストラクチャを再利用することが最も合理的でした。
Rust と Go の短い内部ベークオフの後、そのシンプルさ、速度、およびスケーラブルなネットワーク サービスの構築に対する優れたサポートという本来の目標をどの程度達成しているかという理由から Go を選択しました。 。また、私たちはすでにこれを Decision API にも使用しており、Go API の操作方法を理解しているため、最終的な決定を下すことができました。
バックエンド API を Go に切り替えるのは、そのシンプルさと優れたツールのおかげで簡単でした。しかし、問題が 1 つありました。それは、Next.js フロントエンドを維持しており、手動で TypeScript タイプを記述したり、新しい API 用に別のドキュメントを維持したくなかったことです。
OpenAPI をご利用ください。これは私たちのニーズにぴったりです。 OpenAPI を使用すると、フロントエンドとバックエンドの間のコントラクトを定義できると同時に、ドキュメントとしても機能します。これにより、アプリの両側のスキーマを維持するという問題が解決されます。
NextAuth はバックエンドで比較的簡単に模倣できるため、Go での認証の統合はそれほど難しくありませんでした。 NextAuth (現在は Auth.js) には、セッションを検証するために使用できる API があります。
これは、OpenAPI 仕様から生成されたフロントエンドで TypeScript クライアントを使用して、バックエンド API へのフェッチ呼び出しを行うことができることを意味します。資格情報はフェッチ呼び出しに自動的に含まれ、バックエンドは NextAuth を使用してセッションを検証できます。
Go であらゆる種類のテストを記述するのは非常に簡単で、HTTP ハンドラーのテストのトピックをカバーする例がたくさんあります。
特に認証された状態と実際のデータベース呼び出しをテストしたいため、Next.js API と比較して、新しい Go API エンドポイントのテストを作成するのもはるかに簡単です。 Gin ルーター のテストを簡単に作成し、Testcontainers を使用して Postgres データベースに対して実際の統合テストをスピンアップすることができました。
私たちは、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) }
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"` }
API のテストとデプロイが完了したら、フロントエンドに注目しました。
以前の API 呼び出しは、キャッシュが組み込まれた Next.js 推奨のフェッチ API を使用して行われました。より動的なビューを実現するために、一部のコンポーネントはフェッチに加えて SWR を使用しました。タイプセーフな自動リロードデータフェッチ呼び出しを取得できる可能性がありました。
フロントエンドで API を使用するには、 openapi-typescript ライブラリを使用しました。このライブラリは、OpenAPI スキーマに基づいて TypeScript 型を生成します。これにより、データ モデルを手動で同期する必要がなく、バックエンドとフロントエンドを簡単に統合できるようになりました。これには Tanstack Query が組み込まれており、内部でフェッチを使用しますが、スキーマにも同期します。
私たちは API エンドポイントを新しい Go サーバーに徐々に移行し、途中で小さな改善を加えています。ブラウザのインスペクタを開くと、新しいリクエストが api.arcjet.com
に送信されていることがわかります。
新しい Go バックエンドへの API 呼び出しを示すブラウザー インスペクターのスクリーンショット。
それで、私たちはとらえどころのない Golden API を達成したのでしょうか?ボックスにチェックを入れてみましょう:
さらに次のように進めました:
最終的には、結果に満足しています。私たちの API はより高速で、より安全で、より適切にテストされています。 Go への移行には価値があり、製品の成長に合わせて API を拡張し、維持することができるようになりました。
以上がREST API の再考: ゴールデン API の構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。