API-First-Design mit TypeSpec

post-thumb

API-First oder API-First Design ist eines der vielen Buzzwords in der modernen Softwareentwicklung. Doch für die praktische Umsetzung von API-First-Design scheint es an Tools zu mangeln. OpenAPI-Spezifikationen (OAS) sind sehr umfangreich und diese mit Hand zu schreiben, ist durch die vielen Redundanzen im Vergleich zu Code-First fehleranfällig und ineffizient. Ergo bleibt die gängige Standardpraxis Code-First und die Generierung einer OAS aus dem Backend-Code, wodurch die Vorteile des API-First-Designs unter den Tisch fallen.

Dieser Lücke im Werkzeuggürtel der modernen Webentwicklung hat sich Microsoft angenommen und eine API-Spezifikationssprache entwickelt, die verwendet werden kann, um OAS und mehr zu emittieren. Dabei kommen Patterns und Sprachkonstrukte zum Einsatz, die aus Hochsprachen wie TypeScript und C# bekannt sind. In diesem Beitrag betrachten wir diese API-Spezifikationssprache TypeSpec und geben einen Überblick über die wichtigsten Features der Sprache.

Es folgt eine kurze Einführung in das Thema API-First-Design. Die Ungeduldigen können direkt zum Kapitel TypeSpec springen.

API-First-Design

API-First beschreibt den Softwareentwicklungsansatz bei dem die API mit Hilfe einer API-Spezifikationssprache wie OAS oder TypeSpec als erster Schritt im Entwicklungsprozess definiert wird. APIs spielen in der modernen Technologielandschaft eine immer wichtigere Rolle und mit dem API-First-Ansatz wird dieser Fokus auch in der Softwareentwicklung sichtbar und spürbar. Bereits 2018 wurde dieser in der Skill-Up-Umfrage API Driven Architecture als vielversprechender Ansatz in der Softwareentwicklung gesehen.

Skill-Up Survey 2018

Abbildung 1: Auszug aus der Skill-Up-Umfrage 2018 1

Eine wesentliche Motivation für das API-First-Design liegt in der Reduktion von Redundanzen durch die Verwendung einer Single Source of Truth und der damit verbundenen Reduktion von Fehlermöglichkeiten.

API-Last

Abbildung 2: Redundanzen in APIs

Abbildung 2 symbolisiert die Redundanz, die bei der Entwicklung von APIs entsteht. Sowohl im Frontend- als auch im Backend-Code werden Datenschemata definiert, die über die API ausgetauscht werden, sowie Endpunkte, die festlegen, wo und wie diese Datenschemata verwendet werden. Diese Redundanz stellt, wie jede Redundanz in der Entwicklung, eine Fehlerquelle dar.

API-First-SSOT

Abbildung 3: API-First-Design mit SSOT

Die in Abbildung 3 dargestellte Vorgehensweise zeigt die Verwendung einer ausgelagerten API-Spezifikation als Single Source of Truth (SSOT). Diese SSOT wird dann mit Hilfe von Codegeneratoren in Frontend- und Backend-Stubs übersetzt. Gleichzeitig dient sie als Nachschlagewerk, um die API mit Tools wie Swagger UI schnell und einfach erkunden zu können.

Darüber hinaus können einfach weitere verschiedene Frontends aus der SSOT entwickelt werden. Selbst der Wechsel zu einer neuen Backend-Technologie wird durch die Auslagerung der API-Spezifikation vereinfacht.

Bei den generierten Stubs handelt es sich um Code, der die API lediglich syntaktisch implementiert.

OpenAPI

Die OpenAPI-Spezifikation (OAS) ist mittlerweile der De-facto-Standard unter den API-Spezifikationssprachen. Auch wenn OAS gut lesbar und weit verbreit ist, lässt sich OAS auf Grund der Verbosität nicht gut von Hand schreiben. Es fehlen Sprachkonstrukte, die eine schnelle und elegante Wiederverwendung bestimmter Muster auf allen Ebenen der beschriebenen API ermöglichen.

Der Standard lenkt Entwickelnde quasi zu einem Code-First-Ansatz, bei der die OAS aus dem Code des Backends erstellt wird. Code-First im Projekt

Abbildung 4: Code-First mit OAS

Die in der Abbildung 4 dargestellte Vorgehensweise zeigt den Ablauf, wie aus dem Code des Backends eine OAS generiert wird, die dann für die Entwicklung des Frontends verwendet werden kann. Neben der Verwendung als Referenz bei der Entwicklung des Frontends mit der OAS gibt es mittlerweile unzählige Codegeneratoren für die unterschiedlichsten Frameworks, die aus der OAS die syntaktische Implementierung der API generieren. 2

TypeSpec

TypeSpec ist eine von Microsoft als Open-Source-Projekt 3 entwickelte Sprache, die entwickelt wurde, um APIs zu beschreiben und andere API-Definitionssprachen, Client- und Servercodes, Dokumentationen und andere Komponenten zu generieren. Laut einer Präsentation von Mitgliedern des TypeSpec-Entwicklungsteams besteht das Hauptziel der Entwicklung von TypeSpec jedoch darin, als bessere Toolchain für die Generierung von OAS zu dienen. 4

API-First mit TypeSpec

Abbildung 5: API-First mit TypeSpec

Abbildung 5 zeigt die Verwendung von TypeSpec als SSOT. Aus TypeSpec wird OAS generiert, aus dem mit Hilfe vorhandener Codegeneratoren Stubs für Frontend und Backend erzeugt werden können.

TypeSpec ermöglicht die Beschreibung von APIs auf einer höheren Abstraktionsebene als der derzeitige Industriestandard OpenAPI. Dies beinhaltet die Wiederverwendung abstrakter Muster im Design der API selbst und erleichtert die Identifikation dieser Muster beim Design einer API. Darüber hinaus erlaubt TypeSpec die Definition von Best Practices. Spezifikationen können anschließend auf die Anwendung dieser geprüft werden. Die Syntax von TypeSpec weist viele Ähnlichkeiten mit bekannten Programmiersprachen wie TypeScript oder C# auf.

TypeSpec befindet sich noch in einem frühen Entwicklungsstadium. Die in diesem Blogbeitrag beschriebene Version der Sprache ist Version 0.46.0. Neben Emittern für OAS sind auch Emitter für JSON-Schema und Protobuf verfügbar, die jedoch noch nicht voll funktionsfähig sind. 5

Im Folgenden werden die grundlegenden Sprachkonstrukte von TypeSpec erklärt. Für weitere Beispiele und zum interaktiven Experimentieren bietet Microsoft einen Playground für TypeSpec an. Der Playground übersetzt TypeSpec live zu OAS.

TypeSpec-Model

Ein TypeSpec-Model beschreibt ein Datenschema, das in der API verwendet wird. Models können DTOs sein, die über die API ausgetauscht werden, aber auch Fehlertypen, Request-Parameter oder jedes andere Datenschema.

model Pet {
  id: int64;
  name: string;
  type?: "cat" | "dog";
}

Listing 1: TypeSpec Pet Model

Die Eigenschaften von Models werden ähnlich wie die Eigenschaften von Interfaces in TypeScript deklariert. Listing 1 zeigt ein solches Model mit dem Namen Pet. Es hat die Eigenschaften id vom Typ int64, name vom Typ string. type ist vom Typ string und kann nur die Werte cat oder dog annehmen. Der Typ von type ist dabei mit einem anonymen Union-Type in TypeScript vergleichbar. type ist im Beispiel hierbei eine optionale Eigenschaft, was durch das Fragezeichen hinter dem Namen definiert ist.

Enums in TypeSpec

TypeSpec ermöglicht die Definition von Enums. Diese können unter anderem als Typ von Eigenschaften von Models genutzt werden. Dadurch kann beispielsweise dem anonymen Union-Type im Beispiel Listing 2 ein spezifischer Name gegeben werden. Hierdurch lässt sich der Typ mehrfach verwenden. Die Umsetzung hiervon ist in Listing 3 gegeben.

enum PetType {
  "cat";
  "dog";
}

Listing 2: TypeSpec PetType Enum

Das Enum PetType könnte dann in Zeile 4 von Listing 1, wie in Listing 3 dargestellt, angewandt werden.

type?: PetType

Listing 3: Verwendung von PetType in Pet Darüber hinaus müssen Enums in TypeSpec nicht vom Typ string sein.

Decorators in TypeSpec

Decorators erlauben das Anhängen von Metadaten an Typen und Eigenschaften der Sprache. Das Konzept von Decorators ermöglicht es somit, verschiedene Arten von APIs mit TypeSpec zu beschreiben. Metadaten können hierbei Dokumentationen, Constraints und vieles mehr sein.

TypeSpec selbst enthält nur eine kleine Anzahl an Decorators. Es existieren jedoch viele offizielle TypeSpec-Bibliotheken, die weitere Decorators definieren, um typische Funktionalitäten zu implementieren. Je nach Anwendungsfall werden diese Decorators über den Import der jeweiligen Bibliotheken dem spezifischen TypeSpec-Projekt hinzugefügt. Die Decorators sind in Namespaces gekapselt, welche später in diesem Kapitel erläutert werden.

Ein Beispiel für Decorators findet sich in Listing 8. Hier sind @minLength() und @pattern() Decorators.

Scalars

Scalars sind Typen wie string, int32 oder boolean, die keine eigenen Eigenschaften haben. Scalars können mit dem extends Keyword andere Scalars erweitern. Ein Beispiel hierfür ist in Listing 4 gegeben. Password ist damit ein Datentyp, der bspw. in Models verwendet werden kann. Die Decorators beschreiben, dass die Passwörter eine minimale Länge von 8 haben sollen und dem regulären Ausdruck entsprechen müssen.


@minLength(8)
@pattern("^(?=.*?[#?!@$%^&*-])")
scalar Password extends string; 

Listing 4: Decorators in TypeSpec

Namespaces

Namespaces erlauben die Gruppierung von Typen wie Scalars, Enums und Models in eigenen Namensräumen. Dies verhindert Namenskonflikte in großen APIs und ermöglicht klar definierte Grenzen innerhalb einer API. Namespaces und die in ihnen enthaltenen Typen können hierbei von überall leicht referenziert werden. Namespaces selbst können wiederum auch in anderen Namespaces enthalten sein, wobei es keine maximale Verschachtelungstiefe gibt. Darüber hinaus können Namespaces mit dem using Keyword in einer anderen TypeSpec-Datei verwendet werden, wenn diese importiert wurde.

Im Beispiel Listing 8 wird in Zeile 1 eine andere TypeSpec-Datei importiert und in Zeile 2 der Namespace TypeSpec.Http importiert. Beim importierten Namespace TypeSpec.Http handelt es sich hierbei um einen offiziellen Namespace der Sprache, der neben Models und Scalars auch Decorators definiert. Die Decorators des Namespaces müssen hierbei in JavaScript-Dateien definiert werden. Derselbe Mechanismus, der es erlaubt Decorators in den offiziellen Namespaces zu definieren und diese anderen Namespaces durch den Import zur Verfügung zu stellen, kann auch dafür eingesetzt werden, die Sprache mit selbstdefinierten Decorators zu erweitern.

Operations

Operations definieren Operationen, die in der definierten API ausgeführt werden können. Sie entsprechen beispielsweise den Endpunkten in einer REST-API und bestehen in TypeSpec aus einem Namen, Parametern und einem Rückgabetyp. Operations können entweder mit dem Keyword is oder innerhalb von Strukturen zur Wiederverwendung von Operationen, die Interfaces (siehe Interfaces) genannt werden, wiederverwendet werden. Das Keyword, um Operations zu deklarieren, ist op. Operations gleichen in ihrer Struktur den Methoden in TypeScript. Ein Beispiel für eine Operation ist in Listing 4 gegeben. Hier ist die Operation getPet definiert, die als Pfadparameter eine id vom Typ int64 erwartet und ein Model vom Typ Pet zurückgibt.

op getPet(@path id: int64): Pet;

Listing 5: Einzelne Operation in TypeSpec

Der OAS-Emitter von TypeSpec erkennt die in Listing 4 definierte Operation hierbei eindeutig als GET-Operation. TypeSpec erkennt anhand des Datentyps und des Decorators der Parameter und Rückgabewerte welche HTTP-Methode einer Operation zuzuordnen ist. Die HTTP-Methode kann jedoch auch über Decorators explizit gesetzt werden.

Interfaces

Interfaces werden in TypeSpec verwendet, um Operations zu gruppieren und sie wiederverwendbar zu machen. Interfaces können hierbei generisch sein und so beispielsweise ein CRUD-Interface generisch darstellen. Listing 5 zeigt ein Beispiel, wie ein solches CRUD-Interface wiederverwendet werden kann. Es wird hierbei ein generisches Interface erstellt, welches Operations für alle 4 CRUD-Methoden bereitstellt.

interface CRUDService<TType> {
  @get get(@path id: string): TType | Error;
  @post post(@path id: string, type: TType): TType | Error;
  @put put(@path id: string, type: TType): void | Error;
  @delete delete(@path id: string): void | Error;
}
 
@route("/pets")
interface petService extends CRUDService<Pet> {}
  
@route("/fish")
interface fishService extends CRUDService<Fish> {}

Listing 6: Wiederverwendung von generischen Interfaces in TypeSpec

Innerhalb der beiden Implementierungen des generischen Interfaces können hierbei weitere, vom Typ abhängige Methoden spezifiziert werden. Diese Verwendung von generischen Interfaces entspricht in etwa der Vererbung von Methoden von Klassen in der objektorientierten Programmierung. Ein Interface kann in TypeSpec darüber hinaus auch von mehreren anderen Interfaces erben.

Spread-Operator

Der Spread-Operator ... erlaubt es, die Eigenschaften eines Models innerhalb eines anderen Models zu verwenden. Der Operator kopiert hierbei alle Eigenschaften des Quellmodels in das Zielmodel, ohne dabei eine Beziehung zwischen Quell- und Zielmodel herzustellen. Zusammengefasst erlaubt der Operator die Wiederverwendung von gemeinsamen Eigenschaften, ohne dabei der klassischen Vererbungsstruktur zu entsprechen. Da der Spread-Operator keine semantische Bedeutung hat, kann er auch mehrfach angewandt werden. Listing 7 zeigt ein Beispiel für die Verwendung des Spread-Operators. Das resultierende Model Pet ist identisch mit dem in Listing 1 definierten Model Pet.

model Pet {
  ...HasId;
  ...NamedEntity;
  type?: "cat" | "dog";
}

model HasId {
  id: int64;
}

model NamedEntity {
  name: string;
}

Listing 7: Verwendung des Spread-Operators

Zusammenhängendes Beispiel für API-First-Design mit TypeSpec

Ein zusammenhängendes Beispiel für das Hello-World des API-First-Designs ist in Listing 8 gegeben. Die generierte OAS umfasst stolze 106 Zeilen, während die TypeSpec-Spezifikation nur 34 Zeilen kurz ist.

import "@typespec/http";
using TypeSpec.Http;
 
@service({
  title: "Swagger Petstore",
  version: "1.0.0",
})
namespace DemoService;
 
model Pet {
  id: int64;
  name: string;
  type?: string;
}
 
@maxItems(100) 
model Pets is string[];
 
@error
model Error {
  code: int32;
  message: string;
}
 
@route("pets")
@tag("pets")
interface pets {
  @get list(@query limit?: int32): {
    @header("x-next") `x-next`: string;
    @body pets: Pets;
  } | Error;
  @get read(@path id: string): Pet | Error;
  @post create  (...Pet): OkResponse;
}

Listing 8: TypeSpec-Spezifikation für das PetStore-Beispiel

API-First-Design mit TypeSpec: ein Fazit

Mit TypeSpec stellt Microsoft ein mächtiges Werkzeug vor, das den theoretischen Nutzen des API-First-Designs endlich praktisch anwendbar macht. Auch wenn API-First nicht für jedes Projekt der beste Ansatz ist, so gibt es doch viele Bereiche, die eindeutig von API-First profitieren. Der Open-Source-Charakter von TypeSpec ermöglicht zudem eine einfache Erweiterung der Sprache. Vielleicht wird es neben OAS, JSON Schema und Protobuf bald noch weitere Emitter geben? Ich denke da zum Beispiel an GraphQL und erste Codegeneratoren für Programmiersprachen und Frameworks, die den Zwischenschritt über OAS - und den damit verbundenen Informationsverlust - überflüssig machen.


  1. Skill-Up Survey (2018) Bildquelle  ↩︎

  2. Eine gute Sammlung von Tools für OAS findet sich hier  ↩︎

  3. TypeSpec Github  ↩︎

  4. Norzagaray, H, Kistler, M, Weitzel, M (2023): Introducing TypeSpec Azure SDK Community Standup auf YouTube  ↩︎

  5. TypeSpec Documentation  ↩︎

Lernen Sie uns kennen

Das sind wir

Wir sind ein Software-Unternehmen mit Hauptsitz in Karlsruhe und auf die Umsetzung von Digitalstrategien durch vernetzte Cloud-Anwendungen spezialisiert. Wir sind Microsoft-Partner und erweitern Standard-Anwendungen bei Bedarf – egal ob Modernisierung, Integration, Implementierung von CRM- oder ERP-Systemen, Cloud Security oder Identity- und Access-Management: Wir unterstützen Sie!

Mehr über uns

Der Objektkultur-Newsletter

Mit unserem Newsletter informieren wir Sie stets über die neuesten Blogbeiträge,
Webcasts und weiteren spannenden Themen rund um die Digitalisierung.

Newsletter abonnieren