Entwicklung

API

Den Documenso Public API erstellen - Das Warum und Wie

Den Documenso Public API erstellen - Das Warum und Wie

08.03.2024

Dieser Artikel beschreibt den Prozess des Aufbaus der öffentlichen API für Documenso. Er beginnt mit einer Erklärung, warum die API für ein Unternehmen zur digitalen Dokumentenunterzeichnung überhaupt benötigt wurde. Danach geht es um die Schritte, die wir unternommen haben, um sie zu erstellen. Schließlich werden die Anforderungen präsentiert, die wir erfüllen mussten, sowie die Einschränkungen, innerhalb derer wir arbeiten mussten.

Warum die öffentliche API

Wir haben uns entschieden, die öffentliche API zu erstellen, um eine neue Möglichkeit der Interaktion mit Documenso zu eröffnen. Während die Webanwendung ihre Aufgabe gut erfüllt, gibt es Anwendungsfälle, in denen dies nicht ausreicht. In diesen Fällen möchten die Nutzer möglicherweise programmatisch mit der Plattform interagieren. Dies geschieht normalerweise, um Documenso in andere Anwendungen zu integrieren.

Mit der neuen öffentlichen API ist das jetzt möglich. Sie können die Funktionen von Documenso in anderen Anwendungen integrieren, um Aufgaben zu automatisieren, benutzerdefinierte Lösungen zu erstellen und individuelle Arbeitsabläufe zu erstellen, um nur einige Beispiele zu nennen.

Die API bietet zum Zeitpunkt des Schreibens dieses Artikels 12 Endpunkte:

  • - (GET) /api/v1/documents - alle Dokumente abrufen

  • - (POST) /api/v1/documents - ein neues Dokument hochladen und eine vorab signierte URL erhalten

  • - (GET) /api/v1/documents/{id} - ein bestimmtes Dokument abrufen

  • - (DELETE) /api/v1/documents/{id} - ein bestimmtes Dokument löschen

  • - (POST) /api/v1/templates/{templateId}/create-document - ein neues Dokument aus einer vorhandenen Vorlage erstellen

  • - (POST) /api/v1/documents/{id}/send - ein Dokument zur Unterzeichnung senden

  • - (POST) /api/v1/documents/{id}/recipients - einen Dokumentempfänger erstellen

  • - (PATCH) /api/v1/documents/{id}/recipients/{recipientId} - die Details eines Dokumentempfängers aktualisieren

  • - (DELETE) /api/v1/documents/{id}/recipients/{recipientId} - einen bestimmten Empfänger aus einem Dokument löschen

  • - (POST) /api/v1/documents/{id}/fields - ein Feld für ein Dokument erstellen

  • - (PATCH) /api/v1/documents/{id}/fields - die Details eines Dokumentfelds aktualisieren

  • - (DELETE) /api/v1/documents/{id}/fields - ein Feld aus einem Dokument löschen

Schauen Sie sich die API-Dokumentation an.

Darüber hinaus ermöglicht es uns auch, die Plattform zu verbessern, indem wir andere Integrationen in Documenso bringen, wie z.B. Zapier.

Zusammenfassend erweitert die neue öffentliche API die Möglichkeiten von Documenso, bietet den Nutzern mehr Flexibilität und eröffnet eine breitere Welt von Möglichkeiten.

Die richtige Vorgehensweise und Technologie wählen

Sobald wir uns entschieden hatten, die API zu erstellen, mussten wir die Vorgehensweise und Technologien auswählen, die wir verwenden wollten. Es gab 2 Optionen:

  1. Eine zusätzliche Anwendung entwickeln

  2. Die API im bestehenden Code veröffentlichen

1. Eine zusätzliche Anwendung entwickeln

Das würde bedeuten, eine neue Codebasis zu erstellen und die API von Grund auf neu zu entwickeln. Eine separate App für die API hätte Vorteile wie:

  • geringere Latenzantworten

  • Unterstützung größerer Dateiuploads

  • Trennung zwischen den Apps (Documenso und der API)

  • Individualisierbarkeit und Flexibilität

  • einfacheres Testen und Debuggen

Dieser Ansatz hat erhebliche Vorteile. Ein großer Nachteil ist jedoch, dass er zusätzliche Ressourcen erfordert.

Wir müssten viel Zeit nur für die grundlegenden Dinge aufwenden, wie den grundlegenden Server aufzubauen und zu konfigurieren. Danach würden wir Zeit mit der Implementierung der Endpunkte und der Autorisierung verbringen, unter anderem. Wenn der Aufbau abgeschlossen ist, gibt es eine weitere Anwendung, die bereitgestellt und verwaltet werden muss. All dies würde unsere bereits begrenzten Ressourcen strapazieren.

Also haben wir uns gefragt, ob es einen anderen Weg gibt, dies zu tun, ohne die Qualität der API und das Entwicklererlebnis zu opfern.

2. Die API im bestehenden Code veröffentlichen

Die andere Option bestand darin, die API im bestehenden Code zu veröffentlichen. Anstatt alles von Grund auf neu zu schreiben, könnten wir den Großteil unseres vorhandenen Codes verwenden.

Da wir tRPC für unsere interne API (Backend) verwenden, haben wir nach Lösungen gesucht, die gut mit tRPC funktionieren. Wir haben die Auswahl auf folgende Optionen eingegrenzt:

Beide Technologien ermöglichen es, öffentliche APIs zu erstellen. Die Technologie trpc-openapi ermöglicht es Ihnen, tRPC-Prozeduren einfach in REST-Endpunkte umzuwandeln. Es ist eher wie ein Plugin für tRPC.

Auf der anderen Seite ist ts-rest eher eineStandalone-Lösung. ts-rest ermöglicht es Ihnen, einen Vertrag für die API zu erstellen, der sowohl auf dem Client als auch auf dem Server verwendet werden kann. Sie können den Vertrag in Ihrer Anwendung konsumieren und implementieren, wodurch End-to-End-Typensicherheit und ein RPC-ähnlicher Client bereitgestellt wird.

Sie können hier einen Vergleich zwischen trpc-openapi und ts-rest sehen.

Der Hauptunterschied zwischen den beiden besteht darin, dass trpc-openapi wie ein Plugin ist, das die Fähigkeiten von tRPC erweitert, während ts-rest die Werkzeuge zum Erstellen einer eigenständigen API bereitstellt.

Unsere Wahl

Nach der Analyse und dem Vergleich der beiden Optionen haben wir uns für ts-rest aufgrund seiner Vorteile entschieden. Hier ist ein Abschnitt aus der ts-rest-Dokumentation, der den Nagel auf den Kopf trifft:

tRPC hat viele Plugins, um dieses Problem zu lösen, indem die API-Implementierung in eine REST-ähnliche API mapping, jedoch sind diese Ansätze oft ein wenig umständlich und verringern die Sicherheit des Systems insgesamt, ts-rest erledigt dieses schwere Heben in den Client- und Serverimplementierungen, anstatt eine zweite Abstraktionsschicht und API-Endpunkt(e) definieren zu müssen.

API-Anforderungen

Wir haben die folgenden Anforderungen für die API definiert:

  • Die API sollte pfadbasiertes Versionieren verwenden (z. B. "/v1")

  • Das System sollte Bearer-Token zur Authentifizierung der API verwenden

    • Das API-Token sollte eine zufällige Zeichenkette von 32 bis 40 Zeichen sein

  • Das System sollte das Token hashen und den Hashwert speichern

  • Das System sollte das API-Token nur bei seiner Erstellung anzeigen

  • Die API sollte selbstgenerierte Dokumentation wie Swagger haben

  • Benutzer sollten in der Lage sein, einen API-Schlüssel zu erstellen

    • Benutzer sollten in der Lage sein, einen Token-Namen auszuwählen

    • Benutzer sollten in der Lage sein, ein Ablaufdatum für das Token auszuwählen

      • Der Benutzer sollte zwischen 7 Tagen, 1 Monat, 3 Monaten, 6 Monaten, 12 Monaten und nie wählen können

  • Das System sollte alle Tokens des Benutzers auf der Einstellungsseite anzeigen

    • Das System sollte den Token-Namen, das Erstellungsdatum, das Ablaufdatum und einen Löschen-Button anzeigen

  • Benutzer sollten in der Lage sein, einen API-Schlüssel zu löschen

  • Benutzer sollten in der Lage sein, alle Dokumente aus ihrem Konto abzurufen

  • Benutzer sollten in der Lage sein, ein neues Dokument hochzuladen

    • Benutzer sollten nach einem erfolgreichen Upload eine S3-vorab signierte URL erhalten

  • Benutzer sollten in der Lage sein, ein bestimmtes Dokument anhand seiner ID aus ihrem Konto abzurufen

  • Benutzer sollten in der Lage sein, ein bestimmtes Dokument anhand seiner ID aus ihrem Konto zu löschen

  • Benutzer sollten in der Lage sein, ein neues Dokument aus einer vorhandenen Dokumentvorlage zu erstellen

  • Benutzer sollten in der Lage sein, ein Dokument zur Unterzeichnung an 1 oder mehr Empfänger zu senden

  • Benutzer sollten in der Lage sein, einen Empfänger für ein Dokument zu erstellen

  • Benutzer sollten in der Lage sein, die Details eines Empfängers zu aktualisieren

  • Benutzer sollten in der Lage sein, einen Empfänger aus einem Dokument zu löschen

  • Benutzer sollten in der Lage sein, ein Feld (z. B. Unterschrift, E-Mail, Name, Datum) für ein Dokument zu erstellen

  • Benutzer sollten in der Lage sein, ein Feld für ein Dokument zu aktualisieren

  • Benutzer sollten in der Lage sein, ein Feld aus einem Dokument zu löschen

Einschränkungen

Wir hatten auch mit den folgenden Einschränkungen zu kämpfen, während wir die API entwickelten:

1. Ressourcen

Begrenzte Ressourcen waren eine der Hauptbeschränkungen. Wir sind ein neues Start-up mit einem relativ kleinen Team. Der Aufbau und die Wartung einer zusätzlichen Anwendung würden unsere begrenzten Ressourcen überstrapazieren.

2. Technologie-Stack

Eine weitere Einschränkung war der Technologie-Stack. Unser Technologie-Stack umfasst unter anderem TypeScript, Prisma und tRPC. Wir verwenden auch Vercel für das Hosting.

Infolgedessen wollten wir Technologien verwenden, mit denen wir vertraut sind. Dies ermöglichte es uns, unser bestehendes Wissen zu nutzen und Konsistenz über unsere Anwendungen hinweg zu gewährleisten.

Die Verwendung vertrauter Technologien bedeutete auch, dass wir die API schneller entwickeln konnten, da wir keine Zeit mit dem Erlernen neuer Technologien verbringen mussten. Wir konnten auch vorhandenen Code und Werkzeuge nutzen, die in unserer Hauptanwendung verwendet werden.

Es sei darauf hingewiesen, dass dies keine dauerhafte Entscheidung ist. Wir sind offen dafür, die API in eine andere Codebasis/Technologie-Stack zu verschieben, wenn es sinnvoll ist (z. B. die API wird stark genutzt und benötigt bessere Leistung).

3. Datei-Uploads

Aufgrund unserer aktuellen Architektur unterstützen wir Datei-Uploads mit einer maximalen Größe von 50 MB. Um dies zu umgehen, haben wir einen zusätzlichen Schritt zum Hochladen von Dokumenten erstellt.

Benutzer stellen eine POST-Anfrage an den /api/v1/documents Endpunkt, und die API antwortet mit einer S3-vorab signierten URL. Die Benutzer stellen dann eine 2. Anfrage an die vorab signierte URL mit ihrem Dokument.

Wie wir die API gebaut haben

Unsere Codebasis ist ein Monorepo, daher haben wir ein neues API-Paket im packages Verzeichnis erstellt. Es enthält sowohl die API-Implementierung als auch die Dokumentation. Die beiden Hauptblöcke der Implementierung bestehen aus dem API-Vertrag und dem Code für die API-Endpunkte.

In wenigen Worten definiert der API-Vertrag die API-Struktur, das Format der Anfragen und Antworten, wie API-Aufrufe authentifiziert werden, die verfügbaren Endpunkte und ihre zugehörigen HTTP-Verben. Sie können den API-Vertrag auf GitHub erkunden.

Dann gibt es den Implementierungsteil, der der tatsächliche Code für jeden im API-Vertrag definierten Endpunkt ist. Die Implementierung ist der Teil, in dem der API-Vertrag zum Leben erweckt und funktionsfähig gemacht wird.

Nehmen wir den Endpunkt /api/v1/documents als Beispiel.

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',
    },
    ...
  }
);

Der API-Vertrag spezifiziert die folgenden Dinge für getDocuments:

  • die erlaubte HTTP-Anfragemethode ist GET, sodass beispielsweise der Versuch, eine POST-Anfrage zu stellen, zu einem Fehler führt

  • der Pfad ist /api/v1/documents

  • die Abfrageparameter, die der Benutzer mit der Anfrage übergeben kann

    • in diesem Fall - page und perPage

  • die erlaubten Antworten und deren Schema

    • 200 gibt ein Objekt zurück, das ein Array aller Dokumente und ein Feld totalPages enthält, das selbsterklärend ist

    • 401 gibt ein Objekt mit einer Nachricht wie "Nicht autorisiert" zurück

    • 404 gibt ein Objekt mit einer Nachricht wie "Nicht gefunden" zurück

Die Implementierung dieses Endpunkts muss den Vertrag vollständig erfüllen; andernfalls wird ts-rest sich beschweren und Ihre API möglicherweise nicht wie beabsichtigt funktionieren.

Die Funktion getDocuments aus der implementation.ts-Datei wird ausgeführt, wenn der Benutzer den Endpunkt aufruft.

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,
      },
    };
  }),
  ...
});

Es gibt auch ein Middleware, authenticatedMiddleware, das die Authentifizierung für API-Anfragen behandelt. Es stellt sicher, dass das API-Token vorhanden ist und das verwendete Token die entsprechenden Berechtigungen für die Ressource hat, auf die zugegriffen wird.

So funktionieren auch die anderen Endpunkte. Der Code unterscheidet sich, aber die Prinzipien bleiben gleich. Sie können die API-Implementierung und den Middleware-Code auf GitHub erkunden.

Dokumentation

Für die Dokumentation haben wir uns entschieden, Swagger UI zu verwenden, das automatisch die Dokumentation aus der OpenAPI-Spezifikation generiert.

Die OpenAPI-Spezifikation beschreibt eine API, die die verfügbaren Endpunkte und ihre HTTP-Anfragemethoden, Authentifizierungsmethoden usw. enthält. Sie dient dazu, sowohl Maschinen als auch Menschen zu helfen, die API zu verstehen, ohne den Code ansehen zu müssen.

Die Documenso OpenAPI-Spezifikation ist live hier.

Glücklicherweise macht es ts-rest nahtlos, die OpenAPI-Spezifikation zu generieren.

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,
  },
);

Dann nimmt die Swagger UI die OpenAPI-Spezifikation als Prop und generiert die Dokumentation. Der folgende Code zeigt die Komponente, die für die Generierung der Dokumentation verantwortlich ist.

'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;

Zuletzt erstellen wir einen API-Endpunkt, um die Swagger-Dokumentation anzuzeigen. Der folgende Code importiert dynamisch die OpenApiDocsPage-Komponente und zeigt sie an.

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

Sie können auf die Dokumentation zugreifen und damit experimentieren unter documenso.com/api/v1/openapi. Sie sollten eine Seite sehen, die wie im Screenshot unten dargestellt aussieht.

Dieser Artikel zeigt, wie man Swagger-Dokumentation für eine Next.js-API generiert.

So haben wir also die erste Iteration der öffentlichen API erstellt, nachdem wir alle Einschränkungen und die aktuellen Bedürfnisse berücksichtigt haben. Der GitHub-Pull-Request für die API ist auf GitHub öffentlich verfügbar. Sie können ihn in Ihrem eigenen Tempo durchsehen.

Fazit

Die aktuelle Architektur und Vorgehensweise funktionieren gut für unsere aktuellen Anforderungen und Bedürfnisse. Dennoch wird sich unsere Architektur und Vorgehensweise mit unserem Wachstum und unserer Weiterentwicklung voraussichtlich anpassen müssen. Wir überwachen regelmäßig die API-Nutzung und -Leistung und sammeln Feedback von Benutzern. Dies ermöglicht es uns, Verbesserungsmöglichkeiten zu finden, die Bedürfnisse unserer Benutzer zu verstehen und fundierte Entscheidungen über die nächsten Schritte zu treffen.