Maison > développement back-end > Golang > Repenser notre API REST : créer l'API Golden

Repenser notre API REST : créer l'API Golden

Susan Sarandon
Libérer: 2024-12-06 22:05:23
original
219 Les gens l'ont consulté

Rethinking our REST API: Building the Golden API

À un moment donné, chaque entreprise arrive à un carrefour où elle doit s'arrêter et réévaluer les outils qu'elle utilise. Pour nous, ce moment est arrivé lorsque nous avons réalisé que l'API qui alimentait notre tableau de bord Web était devenue ingérable, difficile à tester et ne répondait pas aux normes que nous avions fixées pour notre base de code.

Arcjet est avant tout un SDK de sécurité en tant que code pour aider les développeurs à mettre en œuvre des fonctionnalités de sécurité telles que la détection de robots, la validation des e-mails et la détection des informations personnelles. Celui-ci communique avec notre API gRPC de décision hautes performances et à faible latence.

Notre tableau de bord Web utilise une API REST distincte principalement pour gérer les connexions au site et examiner les analyses des demandes traitées, mais cela inclut également l'inscription de nouveaux utilisateurs et la gestion de leur compte, ce qui signifie qu'il s'agit toujours d'une partie importante du produit.

Rethinking our REST API: Building the Golden API
Capture d'écran de la page d'analyse des demandes du tableau de bord Arcjet.

Nous avons donc décidé de relever le défi de reconstruire notre API à partir de zéro, cette fois en mettant l'accent sur la maintenabilité, les performances et l'évolutivité. Cependant, nous ne voulions pas nous lancer dans un énorme projet de réécriture - qui ne fonctionne jamais bien - au lieu de cela, nous avons décidé de construire une nouvelle fondation, puis de commencer avec un seul point de terminaison d'API.

Dans cet article, je vais expliquer comment nous avons abordé cela.

Précédemment sur Arcjet

Lorsque la vitesse était notre priorité absolue, Next.js fournissait la solution la plus pratique pour créer des points de terminaison d'API que notre frontend pouvait consommer. Cela nous a permis un développement full-stack transparent au sein d’une seule base de code, et nous n’avons pas eu à trop nous soucier de l’infrastructure car nous avons déployé sur Vercel.

Nous nous sommes concentrés sur notre sécurité en tant que SDK de code et API de décision à faible latence donc pour le tableau de bord frontend, cette pile nous a permis de prototyper rapidement des fonctionnalités avec peu de friction.

Notre pile : Next.js, DrizzleORM, useSWR, NextAuth

Cependant, à mesure que notre produit évoluait, nous avons constaté que la combinaison de tous nos points de terminaison API et de notre code frontend dans le même projet conduisait à un désordre enchevêtré.

Tester notre API est devenu fastidieux (et est très difficile à faire avec Next.js de toute façon), et nous avions besoin d'un système capable de gérer à la fois interne et externe consommation. Au fur et à mesure que nous avons intégré davantage de plates-formes (comme Vercel, Fly.io et Netlify), nous avons réalisé que la vitesse de développement à elle seule n'était pas suffisante. Nous avions besoin d'une solution plus robuste.

Dans le cadre de ce projet, nous souhaitions également répondre à un problème de sécurité persistant concernant la manière dont Vercel vous oblige à exposer publiquement votre base de données. À moins que vous ne payiez pour leur « calcul sécurisé » d'entreprise, la connexion à une base de données distante nécessite qu'elle dispose d'un point de terminaison public. Nous préférons verrouiller notre base de données afin qu'elle ne soit accessible que via un réseau privé. La défense en profondeur est importante et ce serait une autre couche de protection.

Cela nous a amené à décider de dissocier l'interface utilisateur frontend de l'API backend.

Présentation de "L'API Golden"

Qu'est-ce que « l'API Golden » ? Il ne s'agit pas d'une technologie ou d'un framework spécifique, mais plutôt d'un ensemble idéal de principes qui définissent une API bien construite. Bien que les développeurs puissent avoir leurs propres préférences en matière de langages et de frameworks, il existe certains concepts valables dans toutes les piles technologiques sur lesquels la plupart peuvent s'entendre pour créer une API de haute qualité.

1. Performances et évolutivité

Nous avons déjà de l'expérience dans la fourniture d'API hautes performances.Notre API de décision est déployée à proximité de nos clients, utilise Kubernetes pour évoluer de manière dynamique et est optimisée pour les réponses à faible latence. .

Nous avons envisagé des environnements sans serveur et d'autres fournisseurs, mais avec nos clusters k8s existants déjà opérationnels, il était plus logique de réutiliser l'infrastructure en place : déploiements via Octopus Deploy, surveillance via Grafana Jaeger, Loki, Prometheus, etc.

Après une courte préparation interne de Rust vs Go, nous avons choisi Go pour sa simplicité, sa rapidité et la façon dont il atteint ses objectifs initiaux d'un excellent support pour la création de services réseau évolutifs . Nous l'utilisons également déjà pour l'API de décision et comprenons comment faire fonctionner les API Go, ce qui a finalisé la décision pour nous.

2. Documentation complète et claire

Le passage de l'API backend à Go a été simple grâce à sa simplicité et à la disponibilité d'excellents outils. Mais il y avait un problème : nous gardions l'interface Next.js et ne voulions pas écrire manuellement des types TypeScript ni maintenir une documentation séparée pour notre nouvelle API.

Entrez OpenAPI, une solution parfaitement adaptée à nos besoins. OpenAPI nous permet de définir un contrat entre le frontend et le backend, tout en nous servant également de documentation. Cela résout le problème de la maintenance d'un schéma pour les deux côtés de l'application.

3. Sécurité et authentification

L'intégration de l'authentification dans Go n'a pas été trop difficile, grâce au fait que NextAuth est relativement simple à imiter sur le backend. NextAuth (maintenant Auth.js) dispose d'API disponibles pour vérifier une session.

Cela signifiait que nous pouvions avoir un client TypeScript dans le frontend généré à partir de notre spécification OpenAPI effectuant des appels de récupération à l'API backend. Les informations d'identification sont automatiquement incluses dans l'appel de récupération et le backend peut vérifier la session avec NextAuth.

4. Testabilité

Écrire tout type de tests dans Go est très simple et il existe de nombreux exemples traitant du sujet des tests des gestionnaires HTTP.

Il est également beaucoup plus facile d'écrire des tests pour les nouveaux points de terminaison de l'API Go par rapport à l'API Next.js, notamment parce que nous souhaitons tester l'état authentifié et les appels réels à la base de données. Nous avons pu facilement écrire des tests pour le Routeur Gin et réaliser de véritables tests d'intégration sur notre base de données Postgres à l'aide de Testcontainers.

Rassembler tout cela

Écrire la spécification OpenAPI

Nous avons commencé par écrire la spécification OpenAPI 3.0 pour notre API. Une approche OpenAPI favorise la conception du contrat API avant la mise en œuvre, garantissant que toutes les parties prenantes (développeurs, chefs de produit et clients) s'accordent sur le comportement et la structure de l'API avant l'écriture de tout code. Il encourage une planification minutieuse et aboutit à une conception d’API bien pensée, cohérente et conforme aux meilleures pratiques établies. Ce sont les raisons pour lesquelles nous avons choisi d'écrire d'abord la spécification et de générer le code à partir de celle-ci, et non l'inverse.

Mon outil de choix pour cela étaitAPI Fiddle, qui vous aide à rédiger et tester rapidement les spécifications OpenAPI. Cependant, API Fiddle ne prend en charge qu'OpenAPI 3.1 (que nous n'avons pas pu utiliser car de nombreuses bibliothèques ne l'avaient pas adopté), nous sommes donc restés avec la version 3.0 et avons écrit la spécification à la main.

Voici un exemple de ce à quoi ressemblaient les spécifications de notre API :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

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

Copier après la connexion
Copier après la connexion

Avec la spécification OpenAPI en place, nous avons utilisé OAPI-codegen, un outil qui génère automatiquement du code Go à partir de la spécification OpenAPI. Il génère tous les types, gestionnaires et structures de gestion des erreurs nécessaires, ce qui rend le processus de développement beaucoup plus fluide.

1

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

Copier après la connexion
Copier après la connexion

Le résultat était un ensemble de fichiers Go, l'un contenant le squelette du serveur et l'autre avec les implémentations du gestionnaire. Voici un exemple de type Go généré pour l'objet Site :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// 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"`

}

Copier après la connexion
Copier après la connexion

Avec le code généré en place, nous avons pu implémenter la logique du gestionnaire d'API, comme ceci :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

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)

}

Copier après la connexion

La base de données : s'éloigner de DrizzleORM

Drizzle est un excellent ORM pour les projets JS et nous l'utiliserions à nouveau, mais déplacer le code de la base de données hors de Next.js signifiait que nous avions besoin de quelque chose de similaire pour Go.

Nous avons choisi GORM comme ORM et utilisé le Repository Pattern pour résumer les interactions avec la base de données. Cela nous a permis d'écrire des requêtes de base de données propres et testables.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

}

Copier après la connexion

Essai. Tout

Les tests sont essentiels pour nous. Nous voulions nous assurer que tous les appels à la base de données étaient correctement testés. Nous avons donc utilisé Testcontainers pour créer une véritable base de données pour nos tests, reflétant fidèlement notre configuration de production.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

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

Copier après la connexion
Copier après la connexion

Après avoir configuré l'environnement de test, nous avons testé toutes les opérations CRUD comme nous le ferions en production, en nous assurant que notre code se comporte correctement.

1

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

Copier après la connexion
Copier après la connexion

Pour tester nos gestionnaires d'API, nous avons utilisé le package httptest de Go et simulé les interactions de la base de données à l'aide de Mockery. Cela nous a permis de nous concentrer sur le test de la logique de l'API sans nous soucier des problèmes de base de données.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// 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"`

}

Copier après la connexion
Copier après la connexion

Consommation du frontend : frontend piloté par OpenAPI

Une fois notre API testée et déployée, nous avons tourné notre attention vers le frontend.

Nos appels d'API précédents ont été effectués à l'aide de l'AP de récupération recommandé par Next.jsI avec mise en cache intégrée. Pour des vues plus dynamiques, certains composants utilisaient SWR en plus de la récupération, nous avons donc pourrait recevoir des appels de récupération de données à rechargement automatique et de type sécurisé.

Pour consommer l'API sur le frontend, nous avons utilisé la bibliothèque openapi-typescript, qui génère des types TypeScript basés sur notre schéma OpenAPI. Cela a facilité l'intégration du backend avec notre frontend sans avoir à synchroniser manuellement les modèles de données. Il intègre Tanstack Query, qui utilise la récupération sous le capot, mais se synchronise également avec notre schéma.

Nous migrons progressivement les points de terminaison de l'API vers le nouveau serveur Go et apportons de petites améliorations en cours de route. Si vous ouvrez l'inspecteur de votre navigateur, vous verrez ces nouvelles requêtes envoyées à api.arcjet.com

Rethinking our REST API: Building the Golden API
Capture d'écran de l'inspecteur du navigateur montrant un appel d'API vers le nouveau backend Go.

La liste de contrôle d'or

Alors, avons-nous atteint l'insaisissable API Golden ? Cochons la case :

  • Performances et évolutivité – L'API est déployée sur nos clusters k8s existants qui sont déjà optimisés pour les performances. Nous disposons de mesures détaillées et pouvons les adapter si nécessaire.
  • Documentation complète et claire – La spécification OpenAPI fournit une source unique de vérité car le code est généré à partir de celle-ci plutôt que l'inverse. L'utilisation de clients générés signifie qu'il est facile pour notre équipe de travailler avec les API.
  • Sécurité et authentification – Nous déployons déjà Go en production afin de pouvoir copier nos pratiques de sécurité. L'authentification est gérée par NextAuth.
  • Testabilité – Nous avons implémenté des tests unitaires des gestionnaires et des tests d'intégration à l'aide de Testcontainers, une amélioration significative par rapport à nos API Next.js.

Nous sommes allés plus loin avec :

  • Surveillance – Le déploiement sur les clusters k8s existants signifiait que nous avons hérité des capacités de traçage, de journalisation et de métriques déjà mises en place. L'ajout de l'instrumentation OpenTelemetry à Gin était trivial.
  • Simplicité – Go a été conçu à l'origine pour les API, et cela se voit. Notre code est beaucoup plus propre et plus maintenable.

Au final, nous sommes satisfaits des résultats. Notre API est plus rapide, plus sécurisée et mieux testée. La transition vers Go en valait la peine et nous sommes désormais mieux placés pour faire évoluer et maintenir notre API à mesure que notre produit se développe.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal