搜索
首页后端开发Golang重新思考我们的 REST API:构建黄金 API

Rethinking our REST API: Building the Golden API

在某个时刻,每家公司都会到达一个十字路口,他们需要停下来重新评估他们一直在使用的工具。对于我们来说,那一刻到来了,我们意识到为 Web 仪表板提供支持的 API 变得难以管理、难以测试,并且不符合我们为代码库设定的标准。

Arcjet 主要是一个安全即代码 SDK,可帮助开发人员实现机器人检测、电子邮件验证和 PII 检测等安全功能。这与我们的高性能、低延迟决策 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 如何要求您公开公开数据库。除非您为他们的企业“安全计算”付费,否则连接到远程数据库需要它具有公共端点。我们更喜欢锁定我们的数据库,以便只能通过专用网络访问它。纵深防御很重要,这将是另一层保护。

这导致我们决定将前端 UI 与后端 API 分离。

介绍“黄金 API”

什么是“黄金 API”?它不是特定的技术或框架,而是定义构建良好的 API 的一组理想原则。虽然开发人员可能对语言和框架有自己的偏好,但在构建高质量 API 时,大多数人都同意某些跨技术堆栈有效的概念。

1. 性能和可扩展性

我们已经拥有交付高性能 API 的经验。我们的 Decision API 部署在靠近客户的位置,使用 Kubernetes 进行动态扩展,并针对低延迟响应进行了优化.

我们考虑了无服务器环境和其他提供商,但由于我们现有的 k8s 集群已经在运行,因此重用现有的基础设施是最有意义的:通过 Octopus Deploy 进行部署,通过 Grafana Jaeger、Loki、Prometheus 等进行监控

经过短暂的内部 Rust 与 Go 对比后,我们选择了 Go,因为它的简单性、速度以及它实现其最初目标 对构建可扩展网络服务的出色支持的效果。我们也已经将它用于 Decision API 并了解如何操作 Go API,这为我们最终确定了决策。

2. 全面、清晰的文档

由于 Go 的简单性和强大工具的可用性,将后端 API 切换到 Go 非常简单。但有一个问题:我们保留了 Next.js 前端,并且不想手动编写 TypeScript 类型或为我们的新 API 维护单独的文档。

引入 OpenAPI——它非常适合我们的需求。 OpenAPI 允许我们定义前端和后端之间的契约,同时也充当我们的文档。这解决了维护应用程序两侧架构的问题。

3. 安全与认证

在 Go 中集成身份验证并不太困难,这要归功于 NextAuth 在后端的模仿相对简单。 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:>
    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
</https:>

OpenAPI 规范就位后,我们使用了 OAPI-codegen,这是一个根据 OpenAPI 规范自动生成 Go 代码的工具。它生成所有必要的类型、处理程序和错误处理结构,使开发过程更加顺利。

//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 代码。

我们选择 GORM 作为我们的 ORM,并使用 存储库模式 来抽象数据库交互。这使我们能够编写干净、可测试的数据库查询。

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:>
    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
</https:>

设置测试环境后,我们像在生产中一样测试了所有 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,它在底层使用 fetch,但也同步到我们的架构。

我们正在逐步将 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 集群意味着我们继承了已经设置的跟踪、日志记录和指标功能。向 Gin 添加 OpenTelemetry 仪器非常简单。
  • 简单性 – Go 最初是为 API 设计的,这一点也体现出来了。我们的代码更干净、更易于维护。

最后,我们对结果感到满意。我们的 API 更快、更安全且经过更好的测试。向 Go 的过渡是值得的,随着我们产品的增长,我们现在可以更好地扩展和维护我们的 API。

以上是重新思考我们的 REST API:构建黄金 API的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
了解Goroutines:深入研究GO的并发了解Goroutines:深入研究GO的并发May 01, 2025 am 12:18 AM

goroutinesarefunctionsormethodsthatruncurranceingo,启用效率和灯威量。1)shememanagedbodo'sruntimemultimusingmultiplexing,允许千sstorunonfewerosthreads.2)goroutinessimproverentimensImproutinesImproutinesImproveranceThroutinesImproveranceThrountinesimproveranceThroundinesImproveranceThroughEasySytaskParallowalizationAndeff

了解GO中的初始功能:目的和用法了解GO中的初始功能:目的和用法May 01, 2025 am 12:16 AM

purposeoftheInitfunctionoIsistoInitializeVariables,setUpConfigurations,orperformneccesSetarySetupBeforEtheMainFunctionExeCutes.useInitby.UseInitby:1)placingitinyourcodetorunautoamenationally oneraty oneraty oneraty on inity in ofideShortAndAndAndAndForemain,2)keepitiTshortAntAndFocusedonSimImimpletasks,3)

了解GO界面:综合指南了解GO界面:综合指南May 01, 2025 am 12:13 AM

Gointerfacesaremethodsignaturesetsthattypesmustimplement,enablingpolymorphismwithoutinheritanceforcleaner,modularcode.Theyareimplicitlysatisfied,usefulforflexibleAPIsanddecoupling,butrequirecarefulusetoavoidruntimeerrorsandmaintaintypesafety.

从恐慌中恢复:何时以及如何使用recover()从恐慌中恢复:何时以及如何使用recover()May 01, 2025 am 12:04 AM

在Go中使用recover()函数可以从panic中恢复。具体方法是:1)在defer函数中使用recover()捕获panic,避免程序崩溃;2)记录详细的错误信息以便调试;3)根据具体情况决定是否恢复程序执行;4)谨慎使用,以免影响性能。

您如何使用'字符串”包装操纵串中的琴弦?您如何使用'字符串”包装操纵串中的琴弦?Apr 30, 2025 pm 02:34 PM

本文讨论了使用GO的“字符串”软件包进行字符串操作,详细介绍了共同的功能和最佳实践,以提高效率并有效地处理Unicode。

您如何使用'加密”在Go中执行加密操作的软件包?您如何使用'加密”在Go中执行加密操作的软件包?Apr 30, 2025 pm 02:33 PM

本文使用GO的“加密”软件包详细介绍了加密操作,讨论了安全实施的关键生成,管理和最佳实践。

您如何使用'时间”处理日期和时间的包装?您如何使用'时间”处理日期和时间的包装?Apr 30, 2025 pm 02:32 PM

本文详细介绍了GO的“时间”包用于处理日期,时间和时区,包括获得当前时间,创建特定时间,解析字符串以及测量经过的时间。

您如何使用'反映”包裹检查GO中变量的类型和值?您如何使用'反映”包裹检查GO中变量的类型和值?Apr 30, 2025 pm 02:29 PM

文章讨论了使用GO的“反射”软件包进行可变检查和修改,突出显示方法和性能注意事项。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境