Développement

API

Construire l'API publique Documenso - Le pourquoi et le comment

Construire l'API publique Documenso - Le pourquoi et le comment

8 mars 2024

Cet article couvre le processus de construction de l'API publique pour Documenso. Il commence par expliquer pourquoi l'API était nécessaire pour une entreprise de signature de documents numériques en premier lieu. Ensuite, il plongera dans les étapes que nous avons suivies pour la construire. Enfin, il présentera les exigences que nous devions respecter et les contraintes dans lesquelles nous devions travailler.

Pourquoi l'API publique

Nous avons décidé de construire l'API publique pour ouvrir une nouvelle façon d'interagir avec Documenso. Bien que l'application web fasse bien le travail, il existe des cas d'utilisation où cela ne suffit pas. Dans ces cas, les utilisateurs peuvent souhaiter interagir avec la plateforme de manière programmatique. En général, c'est pour intégrer Documenso avec d'autres applications.

Avec la nouvelle API publique, cela est maintenant possible. Vous pouvez intégrer les fonctionnalités de Documenso au sein d'autres applications pour automatiser des tâches, créer des solutions personnalisées et construire des flux de travail personnalisés, pour n'en nommer que quelques-uns.

L'API fournit 12 points de terminaison au moment d'écrire cet article :

  • - (GET) /api/v1/documents - récupérer tous les documents

  • - (POST) /api/v1/documents - télécharger un nouveau document et obtenir une URL présignée

  • - (GET) /api/v1/documents/{id} - récupérer un document spécifique

  • - (DELETE) /api/v1/documents/{id} - supprimer un document spécifique

  • - (POST) /api/v1/templates/{templateId}/create-document - créer un nouveau document à partir d'un modèle existant

  • - (POST) /api/v1/documents/{id}/send - envoyer un document pour signature

  • - (POST) /api/v1/documents/{id}/recipients - créer un destinataire de document

  • - (PATCH) /api/v1/documents/{id}/recipients/{recipientId} - mettre à jour les détails d'un destinataire de document

  • - (DELETE) /api/v1/documents/{id}/recipients/{recipientId} - supprimer un destinataire spécifique d'un document

  • - (POST) /api/v1/documents/{id}/fields - créer un champ pour un document

  • - (PATCH) /api/v1/documents/{id}/fields - mettre à jour les détails d'un champ de document

  • - (DELETE) /api/v1/documents/{id}/fields - supprimer un champ d'un document

Consultez la documentation de l'API.

De plus, cela nous permet également d'améliorer la plateforme en apportant d'autres intégrations à Documenso, comme Zapier.

En conclusion, la nouvelle API publique étend les capacités de Documenso, offre plus de flexibilité aux utilisateurs et ouvre un monde de possibilités plus large.

Choisir la bonne approche et la bonne technologie

Une fois que nous avons décidé de construire l'API, nous avons dû choisir l'approche et les technologies à utiliser. Il y avait 2 options :

  1. Construire une application supplémentaire

  2. Lancer l'API dans le code de base existant

1. Construire une application supplémentaire

Cela signifierait créer un nouveau code de base et construire l'API depuis zéro. Avoir une application séparée pour l'API entraînerait des avantages tels que :

  • réponses à faible latence

  • soutenir des téléchargements de champs plus volumineux

  • séparation entre les applications (Documenso et l'API)

  • personnalisabilité et flexibilité

  • tests et débogage plus faciles

Cette approche a des avantages significatifs. Cependant, un inconvénient majeur est qu'elle nécessite des ressources supplémentaires.

Nous devrions passer beaucoup de temps juste sur les éléments de base, comme la construction et la configuration du serveur de base. Après cela, nous passerions du temps à mettre en œuvre les points de terminaison et l'autorisation, entre autres. Lorsque la construction est terminée, il y aura une autre application à déployer et à gérer. Tout cela mettrait à l'épreuve nos ressources déjà limitées.

Ainsi, nous nous sommes demandé s'il existait un autre moyen de le faire sans sacrifier la qualité de l'API et l'expérience des développeurs.

2. Lancer l'API dans le code de base existant

L'autre option était de lancer l'API dans le code de base existant. Plutôt que d'écrire tout depuis zéro, nous pourrions utiliser la plupart de notre code existant.

Puisque nous utilisons tRPC pour notre API interne (backend), nous avons recherché des solutions qui fonctionnent bien avec tRPC. Nous avons réduit les choix à :

Les deux technologies vous permettent de construire des API publiques. La technologie trpc-openapi vous permet de transformer facilement les procédures tRPC en points de terminaison REST. C'est plutôt comme un plugin pour tRPC.

D'autre part, `ts-rest` est plus une solution autonome. ts-rest vous permet de créer un contrat pour l'API, qui peut être utilisé à la fois sur le client et le serveur. Vous pouvez consommer et mettre en œuvre le contrat dans votre application, garantissant ainsi une sécurité de type de bout en bout et un client similaire à RPC.

Vous pouvez voir une comparaison entre trpc-openapi et ts-rest ici.

Ainsi, la principale différence entre les 2 est que trpc-openapi est comme un plugin qui étend les capacités de tRPC, tandis que ts-rest fournit les outils pour construire une API autonome.

Notre choix

Après avoir analysé et comparé les 2 options, nous avons décidé d'opter pour ts-rest en raison de ses avantages. Voici un paragraphe de la documentation de ts-rest qui touche juste :

tRPC dispose de nombreux plugins pour résoudre ce problème en mappant l'implémentation de l'API à une API de type REST, cependant, ces approches sont souvent un peu maladroites et réduisent la sécurité du système dans son ensemble, ts-rest effectue ce travail lourd dans les implémentations client et serveur plutôt que d'exiger une seconde couche d'abstraction et que les points de terminaison de l'API soient définis.

Exigences de l'API

Nous avons défini les exigences suivantes pour l'API :

  • L'API doit utiliser un versionnement basé sur le chemin (par exemple, `/v1`)

  • Le système doit utiliser des jetons porteur pour l'authentification de l'API

    • Le jeton API doit être une chaîne aléatoire de 32 à 40 caractères

  • Le système doit hacher le jeton et stocker la valeur hachée

  • Le système doit n'afficher le jeton API que lorsqu'il est créé

  • L'API doit avoir une documentation générée automatiquement comme Swagger

  • Les utilisateurs doivent pouvoir créer une clé API

    • Les utilisateurs doivent pouvoir choisir un nom de jeton

    • Les utilisateurs doivent pouvoir choisir une date d'expiration pour le jeton

      • L'utilisateur doit pouvoir choisir entre 7 jours, 1 mois, 3 mois, 6 mois, 12 mois, jamais

  • Le système doit afficher tous les jetons de l'utilisateur sur la page des paramètres

    • Le système doit afficher le nom du jeton, la date de création, la date d'expiration et un bouton de suppression

  • Les utilisateurs doivent pouvoir supprimer une clé API

  • Les utilisateurs doivent pouvoir récupérer tous les documents de leur compte

  • Les utilisateurs doivent pouvoir télécharger un nouveau document

    • Les utilisateurs doivent recevoir une URL présignée S3 après un téléchargement réussi

  • Les utilisateurs doivent pouvoir récupérer un document spécifique de leur compte par son identifiant

  • Les utilisateurs doivent pouvoir supprimer un document spécifique de leur compte par son identifiant

  • Les utilisateurs doivent pouvoir créer un nouveau document à partir d'un modèle de document existant

  • Les utilisateurs doivent pouvoir envoyer un document pour signature à un ou plusieurs destinataires

  • Les utilisateurs doivent pouvoir créer un destinataire pour un document

  • Les utilisateurs doivent pouvoir mettre à jour les détails d'un destinataire

  • Les utilisateurs doivent pouvoir supprimer un destinataire d'un document

  • Les utilisateurs doivent pouvoir créer un champ (par exemple, signature, e-mail, nom, date) pour un document

  • Les utilisateurs doivent pouvoir mettre à jour un champ pour un document

  • Les utilisateurs doivent pouvoir supprimer un champ d'un document

Contraintes

Nous avons également été confrontés aux contraintes suivantes lors du développement de l'API :

1. Ressources

Des ressources limitées étaient l'une des principales contraintes. Nous sommes une nouvelle startup avec une équipe relativement petite. Construire et maintenir une application supplémentaire mettrait à l'épreuve nos ressources limitées.

2. Technologie stack

Une autre contrainte était la technologie stack. Notre stack technologique inclut TypeScript, Prisma, et tRPC, entre autres. Nous utilisons également Vercel pour l'hébergement.

En conséquence, nous voulions utiliser des technologies avec lesquelles nous sommes à l'aise. Cela nous a permis de tirer parti de nos connaissances existantes et a assuré la cohérence à travers nos applications.

Utiliser des technologies familières signifiait également que nous pouvions développer l'API plus rapidement, car nous n'avions pas à passer du temps à apprendre de nouvelles technologies. Nous pouvions également tirer parti du code et des outils existants utilisés dans notre application principale.

Il convient de mentionner que ce n'est pas une décision permanente. Nous sommes ouverts à déplacer l'API vers un autre code de base/stack technologique quand cela a du sens (par exemple, l'API est fortement utilisée et nécessite de meilleures performances).

3. Téléchargements de fichiers

En raison de notre architecture actuelle, nous supportons les téléchargements de fichiers d'une taille maximale de 50 Mo. Pour contourner cela, nous avons créé une étape supplémentaire pour télécharger des documents.

Les utilisateurs effectuent une requête POST à l'endpoint /api/v1/documents et l'API répond avec une URL présignée S3. Les utilisateurs effectuent ensuite une deuxième requête à l'URL présignée avec leur document.

Comment nous avons construit l'API

Notre base de code est un monorepo, donc nous avons créé un nouveau package API dans le répertoire packages. Il contient à la fois l'implémentation de l'API et sa documentation. Les deux principaux blocs de l'implémentation consistent en le contrat API et le code pour les points de terminaison de l'API.

En quelques mots, le contrat API définit la structure de l'API, le format des requêtes et des réponses, comment authentifier les appels d'API, les points de terminaison disponibles et leurs verbes HTTP associés. Vous pouvez explorer le contrat API sur GitHub.

Ensuite, il y a la partie implémentation, qui est le code réel pour chaque point de terminaison défini dans le contrat API. L'implémentation est là où le contrat API prend vie et devient fonctionnel.

Prenons le point de terminaison /api/v1/documents comme exemple.

export const ApiContractV1 = c.router(
  {
    getDocuments: {
      method: 'GET',
      path: '/api/v1/documents',
      query: ZGetDocumentsQuerySchema,
      responses: {
        200: ZSuccessfulResponseSchema,
        401: ZUnsuccessfulResponseSchema,
        404: ZUnsuccessfulResponseSchema,
      },
      summary: 'Get all documents',
    },
    ...
  }
);

Le contrat API spécifie les éléments suivants pour getDocuments :

  • la méthode HTTP de requête autorisée est GET, donc essayer de faire une requête POST, par exemple, entraîne une erreur

  • le chemin est /api/v1/documents

  • les paramètres de requête que l'utilisateur peut passer avec la requête

    • dans ce cas - page et perPage

  • les réponses autorisées et leur schéma

    • 200 renvoie un objet contenant un tableau de tous les documents et un champ totalPages, qui est explicite

    • 401 renvoie un objet avec un message tel que "Non autorisé"

    • 404 renvoie un objet avec un message tel que "Non trouvé"

L'implémentation de ce point de terminaison doit correspondre complètement au contrat ; sinon, ts-rest se plaignera, et votre API pourrait ne pas fonctionner comme prévu.

La fonction getDocuments du fichier implementation.ts s'exécute lorsque l'utilisateur accède au point de terminaison.

export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
  getDocuments: authenticatedMiddleware(async (args, user, team) => {
    const page = Number(args.query.page) || 1;
    const perPage = Number(args.query.perPage) || 10;
    const { data: documents, totalPages } = await findDocuments({
      page,
      perPage,
      userId: user.id,
      teamId: team?.id,
    });
    return {
      status: 200,
      body: {
        documents,
        totalPages,
      },
    };
  }),
  ...
});

Il y a aussi un middleware, authenticatedMiddleware, qui gère l'authentification des requêtes API. Il garantit que le jeton API existe et que le jeton utilisé dispose des privilèges appropriés pour la ressource qu'il accède.

C'est ainsi que les autres points de terminaison fonctionnent également. Le code diffère, mais les principes sont les mêmes. Vous pouvez explorer l'implémentation de l'API et le code du middleware sur GitHub.

Documentation

Pour la documentation, nous avons décidé d'utiliser Swagger UI, qui génère automatiquement la documentation à partir de la spécification OpenAPI.

La spécification OpenAPI décrit une API contenant les points de terminaison disponibles et leurs méthodes de requêtes HTTP, méthodes d'authentification, etc. Son objectif est d'aider à la fois les machines et les humains à comprendre l'API sans avoir à regarder le code.

La spécification OpenAPI de Documenso est en direct ici.

Heureusement, ts-rest rend la génération de la spécification OpenAPI transparente.

import { generateOpenApi } from '@ts-rest/open-api';
import { ApiContractV1 } from './contract';
export const OpenAPIV1 = generateOpenApi(
  ApiContractV1,
  {
    info: {
      title: 'Documenso API',
      version: '1.0.0',
      description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
    },
  },
  {
    setOperationId: true,
  },
);

Ensuite, Swagger UI prend la spécification OpenAPI comme prop et génère la documentation. Le code ci-dessous montre le composant responsable de la génération de la documentation.

'use client';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
export const OpenApiDocsPage = () => {
   return ; 
};
export default OpenApiDocsPage;

Enfin, nous créons un point de terminaison API pour afficher la documentation Swagger. Le code ci-dessous importe dynamiquement le composant OpenApiDocsPage et l'affiche.

'use client';
import dynamic from 'next/dynamic';
const Docs = dynamic(async () => import('@documenso/api/v1/api-documentation'), {
  ssr: false,
});
export default function OpenApiDocsPage() {
   return ; 
}

Vous pouvez accéder et jouer avec la documentation à documenso.com/api/v1/openapi. Vous devriez voir une page comme celle montrée dans la capture d'écran ci-dessous.

Cet article montre comment générer une documentation Swagger pour une API Next.js.

Ainsi, c'est ainsi que nous avons construit la première itération de l'API publique après avoir pris en compte toutes les contraintes et les besoins actuels. La demande de tirage GitHub pour l'API est publiquement disponible sur GitHub. Vous pouvez le parcourir à votre rythme.

Conclusion

L'architecture et l'approche actuelles fonctionnent bien pour notre stade et nos besoins actuels. Cependant, à mesure que nous continuons à croître et à évoluer, notre architecture et notre approche devront probablement s'adapter. Nous surveillons régulièrement l'utilisation de l'API et ses performances et recueillons les retours des utilisateurs. Cela nous permet de trouver des domaines d'amélioration, de comprendre les besoins de nos utilisateurs et de prendre des décisions éclairées sur les prochaines étapes.