Heim > Backend-Entwicklung > Golang > Überdenken unserer REST-API: Aufbau der Golden API

Überdenken unserer REST-API: Aufbau der Golden API

Susan Sarandon
Freigeben: 2024-12-06 22:05:23
Original
214 Leute haben es durchsucht

Rethinking our REST API: Building the Golden API

Irgendwann kommt jedes Unternehmen an einen Scheideweg, an dem es innehalten und die von ihm verwendeten Tools neu bewerten muss. Für uns kam dieser Moment, als uns klar wurde, dass die API, die unser Web-Dashboard antreibt, nicht mehr zu verwalten und schwer zu testen war und nicht den Standards entsprach, die wir für unsere Codebasis festgelegt hatten.

Arcjet ist in erster Linie ein Security-as-Code-SDK, das Entwicklern bei der Implementierung von Sicherheitsfunktionen wie Bot-Erkennung, E-Mail-Validierung und PII-Erkennung hilft. Dies kommuniziert mit unserer leistungsstarken, Entscheidungs-gRPC-API mit geringer Latenz.

Unser Web-Dashboard verwendet eine separate REST-API in erster Linie zur Verwaltung von Site-Verbindungen und zur Überprüfung der Analyse verarbeiteter Anfragen. Dazu gehört jedoch auch die Anmeldung neuer Benutzer und die Verwaltung ihres Kontos, was bedeutet, dass dies immer noch ein wichtiger Teil des Produkts ist.

Rethinking our REST API: Building the Golden API
Screenshot der Seite zur Anforderungsanalyse des Arcjet-Dashboards.

Deshalb haben wir uns entschieden, die Herausforderung anzunehmen, unsere API von Grund auf neu zu erstellen – dieses Mal mit Schwerpunkt auf Wartbarkeit, Leistung und Skalierbarkeit. Allerdings wollten wir uns nicht auf ein riesiges Umschreibungsprojekt einlassen – das nie gut klappt – stattdessen haben wir beschlossen, eine neue Grundlage zu schaffen und dann mit einem einzigen API-Endpunkt zu beginnen.

In diesem Beitrag werde ich besprechen, wie wir das angegangen sind.

Zuvor auf Arcjet

Als Geschwindigkeit für uns oberste Priorität hatte, bot Next.js die bequemste Lösung zum Erstellen von API-Endpunkten, die unser Frontend nutzen konnte. Es ermöglichte uns eine nahtlose Full-Stack-Entwicklung innerhalb einer einzigen Codebasis und wir mussten uns nicht allzu viele Gedanken über die Infrastruktur machen, da wir auf Vercel bereitgestellt haben.

Unser Fokus lag auf unserer Sicherheit als Code-SDK und Entscheidungs-API mit geringer Latenz, sodass wir mit diesem Stack für das Frontend-Dashboard schnell und ohne Reibungsverluste Prototypen von Funktionen erstellen konnten.

Unser Stack: Next.js, DrizzleORM, useSWR, NextAuth

Als sich unser Produkt weiterentwickelte, stellten wir jedoch fest, dass die Kombination aller unserer API-Endpunkte und Frontend-Codes im selben Projekt zu einem Durcheinander führte.

Das Testen unserer API wurde umständlich (und ist mit Next.js ohnehin sehr schwierig), und wir brauchten ein System, das sowohl intern als auch externVerbrauch. Als wir uns in weitere Plattformen (wie Vercel, Fly.io und Netlify) integriert haben, wurde uns klar, dass die Entwicklungsgeschwindigkeit allein nicht ausreichte. Wir brauchten eine robustere Lösung.

Im Rahmen dieses Projekts wollten wir auch ein anhaltendes Sicherheitsproblem ansprechen, das darin besteht, dass Vercel von Ihnen verlangt, Ihre Datenbank öffentlich zugänglich zu machen. Sofern Sie nicht für die Unternehmens-„sichere Datenverarbeitung“ bezahlen, ist für die Verbindung mit einer Remote-Datenbank ein öffentlicher Endpunkt erforderlich. Wir ziehen es vor, unsere Datenbank zu sperren, sodass der Zugriff nur über ein privates Netzwerk möglich ist. Eine umfassende Verteidigung ist wichtig und dies wäre eine weitere Schutzebene.

Dies führte dazu, dass wir beschlossen, die Frontend-Benutzeroberfläche von der Backend-API zu entkoppeln.

Vorstellung von „The Golden API“

Was ist „The Golden API“? Es handelt sich nicht um eine bestimmte Technologie oder ein bestimmtes Framework, sondern vielmehr um eine Reihe idealer Prinzipien, die eine gut aufgebaute API definieren. Während Entwickler möglicherweise ihre eigenen Vorlieben für Sprachen und Frameworks haben, gibt es bestimmte Konzepte, die für alle Tech-Stacks gültig sind und auf die sich die meisten für die Erstellung einer hochwertigen API einigen können.

1. Leistung und Skalierbarkeit

Wir haben bereits Erfahrung in der Bereitstellung leistungsstarker APIs.Unsere Entscheidungs-API wird in der Nähe unserer Kunden bereitgestellt, nutzt Kubernetes zur dynamischen Skalierung und ist für Antworten mit geringer Latenz optimiert .

Wir haben serverlose Umgebungen und andere Anbieter in Betracht gezogen, aber da unsere bestehenden k8s-Cluster bereits in Betrieb waren, war es am sinnvollsten, die vorhandene Infrastruktur wiederzuverwenden: Bereitstellungen durch Octopus Deploy, Überwachung durch Grafana Jaeger, Loki, Prometheus usw.

Nach einem kurzen internen Rust-vs-Go-Test entschieden wir uns für Go aufgrund seiner Einfachheit, Geschwindigkeit und weil es seine ursprünglichen Ziele einer hervorragenden Unterstützung für den Aufbau skalierbarer Netzwerkdienste erreicht . Wir verwenden es auch bereits für die Entscheidungs-API und verstehen, wie man Go-APIs bedient, was die Entscheidung für uns finalisiert hat.

2. Umfassende und klare Dokumentation

Die Umstellung der Backend-API auf Go war dank ihrer Einfachheit und der Verfügbarkeit großartiger Tools unkompliziert. Aber es gab einen Haken: Wir behielten das Next.js-Frontend bei und wollten weder TypeScript-Typen manuell schreiben noch eine separate Dokumentation für unsere neue API pflegen.

Jetzt kommt OpenAPI ins Spiel – genau das Richtige für unsere Anforderungen. OpenAPI ermöglicht es uns, einen Vertrag zwischen Frontend und Backend zu definieren und dient gleichzeitig als unsere Dokumentation. Dies löst das Problem der Pflege eines Schemas für beide Seiten der App.

3. Sicherheit und Authentifizierung

Die Integration der Authentifizierung in Go war nicht allzu schwierig, da NextAuth im Backend relativ einfach nachzuahmen war. NextAuth (jetzt Auth.js) verfügt über APIs zur Überprüfung einer Sitzung.

Das bedeutete, dass wir einen TypeScript-Client im Frontend haben konnten, der aus unserer OpenAPI-Spezifikation generiert wurde und Abrufaufrufe an die Backend-API durchführte. Die Anmeldeinformationen werden automatisch in den Abrufaufruf einbezogen und das Backend kann die Sitzung mit NextAuth überprüfen.

4. Testbarkeit

Das Schreiben jeglicher Art von Tests in Go ist sehr einfach und es gibt viele Beispiele, die das Thema Testen von HTTP-Handlern abdecken.

Im Vergleich zur Next.js-API ist es auch viel einfacher, Tests für die neuen Go-API-Endpunkte zu schreiben, insbesondere weil wir authentifizierte Status- und echte Datenbankaufrufe testen möchten. Mit Testcontainern konnten wir problemlos Tests für den Gin-Router schreiben und echte Integrationstests für unsere Postgres-Datenbank starten.

Alles zusammenfügen

Schreiben der OpenAPI-Spezifikation

Wir begannen mit dem Schreiben der OpenAPI 3.0-Spezifikation für unsere API. Ein OpenAPI-First-Ansatz fördert die Gestaltung des API-Vertrags vor der Implementierung und stellt sicher, dass sich alle Beteiligten (Entwickler, Produktmanager und Kunden) über das Verhalten und die Struktur der API einigen, bevor Code geschrieben wird. Es fördert eine sorgfältige Planung und führt zu einem gut durchdachten API-Design, das konsistent ist und etablierten Best Practices entspricht. Aus diesen Gründen haben wir uns dafür entschieden, zuerst die Spezifikation zu schreiben und daraus den Code zu generieren, und nicht umgekehrt.

Mein bevorzugtes Tool hierfür warAPI Fiddle, mit dem Sie schnell OpenAPI-Spezifikationen entwerfen und testen können. Allerdings unterstützt API Fiddle nur OpenAPI 3.1 (das wir nicht verwenden konnten, weil viele Bibliotheken es nicht übernommen hatten), also sind wir bei Version 3.0 geblieben und haben die Spezifikation von Hand geschrieben.

Hier ist ein Beispiel dafür, wie die Spezifikation für unsere API aussah:

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
Nach dem Login kopieren
Nach dem Login kopieren

Nachdem die OpenAPI-Spezifikation vorhanden war, verwendeten wir OAPI-codegen, ein Tool, das automatisch Go-Code aus der OpenAPI-Spezifikation generiert. Es generiert alle notwendigen Typen, Handler und Fehlerbehandlungsstrukturen, was den Entwicklungsprozess wesentlich reibungsloser macht.

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
Nach dem Login kopieren
Nach dem Login kopieren

Die Ausgabe bestand aus einer Reihe von Go-Dateien, von denen eine das Servergerüst und eine andere die Handler-Implementierungen enthielt. Hier ist ein Beispiel für einen Go-Typ, der für das Site-Objekt generiert wurde:

// 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"`
}
Nach dem Login kopieren
Nach dem Login kopieren

Mit dem generierten Code konnten wir die API-Handler-Logik wie folgt implementieren:

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)
}
Nach dem Login kopieren

Die Datenbank: Abkehr von DrizzleORM

Drizzle ist ein großartiges ORM für JS-Projekte und wir würden es wieder verwenden, aber die Verschiebung des Datenbankcodes aus Next.js bedeutete, dass wir etwas Ähnliches für Go brauchten.

Wir haben GORM als unser ORM ausgewählt und das Repository-Muster verwendet, um Datenbankinteraktionen zu abstrahieren. Dadurch konnten wir saubere, testbare Datenbankabfragen schreiben.

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
}
Nach dem Login kopieren

Testen. Alles

Testen ist für uns von entscheidender Bedeutung. Wir wollten sicherstellen, dass alle Datenbankaufrufe ordnungsgemäß getestet wurden, deshalb haben wir Testcontainer verwendet, um eine echte Datenbank für unsere Tests aufzubauen, die unserem Produktionsaufbau genau entspricht.

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
Nach dem Login kopieren
Nach dem Login kopieren

Nach dem Einrichten der Testumgebung haben wir alle CRUD-Vorgänge wie in der Produktion getestet, um sicherzustellen, dass sich unser Code korrekt verhält.

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
Nach dem Login kopieren
Nach dem Login kopieren

Zum Testen unserer API-Handler haben wir das httptest-Paket von Go verwendet und die Datenbankinteraktionen mithilfe von Mockery simuliert. Dadurch konnten wir uns auf das Testen der API-Logik konzentrieren, ohne uns um Datenbankprobleme kümmern zu müssen.

// 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"`
}
Nach dem Login kopieren
Nach dem Login kopieren

Frontend-Nutzung: OpenAPI-gesteuertes Frontend

Nachdem unsere API getestet und bereitgestellt wurde, richteten wir unsere Aufmerksamkeit auf das Frontend.

Unsere vorherigen API-Aufrufe erfolgten über den von Next.js empfohlenen Abruf-API mit integriertem Caching. Für dynamischere Ansichten verwendeten einige Komponenten zusätzlich zum Abruf SWR könnte typsichere, automatisch nachladende Datenabrufaufrufe erhalten.

Um die API im Frontend zu nutzen, haben wir die Bibliothek openapi-typescript verwendet, die TypeScript-Typen basierend auf unserem OpenAPI-Schema generiert. Dies machte es einfach, das Backend mit unserem Frontend zu integrieren, ohne Datenmodelle manuell synchronisieren zu müssen. Darin ist Tanstack Query integriert, das Fetch unter der Haube nutzt, aber auch mit unserem Schema synchronisiert wird.

Wir migrieren die API-Endpunkte schrittweise auf den neuen Go-Server und nehmen dabei kleine Verbesserungen vor. Wenn Sie Ihren Browser-Inspektor öffnen, sehen Sie, dass diese neuen Anfragen an api.arcjet.com gehen

Rethinking our REST API: Building the Golden API
Screenshot des Browser-Inspektors, der einen API-Aufruf an das neue Go-Backend zeigt.

Die goldene Checkliste

Haben wir also die schwer fassbare Golden API erreicht? Aktivieren wir das Kontrollkästchen:

  • Leistung und Skalierbarkeit – Die API wird auf unseren bestehenden k8s-Clustern bereitgestellt, die bereits auf Leistung abgestimmt sind. Wir verfügen über detaillierte Metriken und können diese bei Bedarf skalieren.
  • Umfassende und klare Dokumentation – Die OpenAPI-Spezifikation bietet eine einzige Quelle der Wahrheit, da Code daraus generiert wird und nicht umgekehrt. Durch die Verwendung generierter Clients ist es für unser Team einfach, mit den APIs zu arbeiten.
  • Sicherheit und Authentifizierung – Wir implementieren Go bereits in der Produktion, damit wir unsere Sicherheitspraktiken kopieren können. Die Authentifizierung erfolgt über NextAuth.
  • Testbarkeit – Wir haben Unit-Tests der Handler und Integrationstests mithilfe von Testcontainern implementiert, eine deutliche Verbesserung gegenüber unseren Next.js-APIs.

Wir gingen weiter mit:

  • Überwachung – Die Bereitstellung auf den vorhandenen k8s-Clustern bedeutete, dass wir die bereits eingerichteten Ablaufverfolgungs-, Protokollierungs- und Metrikfunktionen übernommen haben. Das Hinzufügen von OpenTelemetry-Instrumenten zu Gin war trivial.
  • Einfachheit – Go wurde ursprünglich für APIs entwickelt, und das merkt man. Unser Code ist viel sauberer und wartbarer.

Am Ende sind wir mit den Ergebnissen zufrieden. Unsere API ist schneller, sicherer und besser getestet. Der Übergang zu Go hat sich gelohnt und wir sind jetzt besser in der Lage, unsere API zu skalieren und zu pflegen, wenn unser Produkt wächst.

Das obige ist der detaillierte Inhalt vonÜberdenken unserer REST-API: Aufbau der Golden API. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage