Node.js, TypeScript und Datenbanken: Typsichere Repositories mit Prisma

Mein bewährtes Tandem ist die Nutzung von Prisma als Datenbankabstraktion in Kombination mit TypeScript. Ich skizziere, weshalb ich mich für diesen Ansatz entschieden habe und welche Vorteile sich für mich daraus ergeben.

4 Minuten

Welches Problem löst Prisma für mich?

Das Speichern und Lesen von Applikationsdaten ist eine Herausforderung. Qualitätsmerkmale wie Skalierbarkeit, Performance und Wartbarkeit hängen direkt damit zusammen. Entsprechend genau ist in der Anforderungsanalyse darauf zu achten, wie die Datenhaltung organisiert ist. Meine Erfahrung zeigt, dass in der Regel eine NoSQL-Datenbank wie MongoDB im Spiel ist. Entsprechend lohnt sich ein Ansatz, der die Daten von Node.js einfach hinpackt und wieder herausholt.

Genau an diesem Punkt kommt Prisma ins Spiel. Es ist eine Abstraktionsschicht (ORM = Object-Relational Mapping), die deutlich mehr bietet als nur einfaches Datenlesen und Schreiben. Jedoch erfüllt sie als ein Baustein genau diesen Aspekt mit dem Versprechen, eine gute Entwicklererfahrung zu bieten, und besonders wichtig: typsicher zu sein.

Warum ich das Architekturmuster „Repository“ einsetze

So gut heute Prisma in meinen Tech-Stack passt, so schnell kann sich das ändern. Damit ich dafür gewappnet bin – und aus einigen anderen Gründen – verwende ich das Architekturmuster-Repository. Einen besonders gut geschriebenen Artikel dazu findest du auf Albert Hernández’ Blog. Falls dich die anderen Gründe interessieren, findest du ebenfalls detaillierte Antworten darauf: Testbarkeit, Trennung von Verantwortlichkeiten, Flexibilität.

Zurück zu meinem Punkt: Austauschbarkeit. Neben Prisma gibt es weitere Tools, die das Problem lösen. Ein Beispiel wäre Drizzle. Wenngleich ich heute oder morgen nicht wechseln möchte, möchte ich mir diese Option offenhalten. Die Allzweckwaffe dafür ist eine Abstraktionsschicht: in diesem Fall das genannte Architekturmuster-Repository.

Wenn ich den Austausch vollziehe, beschränkt sich meine Integration in den bestehenden Code genau auf dieses Konstrukt. Mein sonstiger Code verlässt sich auf das Repository und die definierten Typen als Vertrag in der Kommunikation. Dank TypeScript habe ich Typsicherheit.

Wie spielt die REST-API damit zusammen?

In der bisherigen Betrachtung kann ich Daten lesen und schreiben. Damit kann ich jede Menge tun, nur fehlt ein wichtiges Puzzlestück: die Verbindung nach draußen. Wie in meinem Artikel zum allgemeinen Setup angedeutet, bevorzuge ich die Anbindung an die Außenwelt über eine REST-Schnittstelle. Deren Definition erfolgt ebenfalls typsicher. Eine wichtige Frage lautet: Wie hängen diese Typdefinitionen zusammen?

In meinen ersten Versuchen habe ich, getrieben vom DRY-Prinzip (Don't repeat yourself) den Ansatz gewählt, diese Typen miteinander zu verheiraten. Davon kann ich dir nur dringend abraten. Der vermeintliche Vorteil, weniger Code zu schreiben, führt zu Problemen, sobald Ausnahmen auftreten. Anfangs mag es wie Ausnahmen zu scheinen, doch werden die schnell zur Regel – ab dem Moment habe ich nichts gewonnen. Schlimmer noch, ich habe die Definition der REST-Schnittstelle implizit mit meinem Datenbankmodell verknüpft. Das ist eine furchtbare Idee.

Stattdessen empfehle ich die folgende Betrachtungsweise. Beim Controller definierst du die Typen der Schnittstelle. Das Repository definiert seine eigene Schnittstelle als internen Vertrag zur Abstraktion des Datenzugriffs. Die Datenhaltung modellierst du in deinem ORM-Tool, das idealerweise, wie bei Prisma, einen typisierten Zugriff gewährleistet.

export type PatientUpdateBody = {
  /**
   * Anrede des Klienten (Herr, Frau, Divers)
   * @minLength 1 Bitte eine Anrede wählen
   */
  salutation: Salutation
  /**
   * Vorname des Klienten
   * @minLength 1 Bitte einen Vornamen angeben
   */
  firstName: string
  /**
   * Nachname des Klienten
   * @minLength 1 Bitte einen Nachnamen angeben
   */
  lastName: string
  // ...
}

Im Controller die TSOA-konforme Definition des Requestbody beim Aktualisieren

export type PatientDetails = Prisma.PatientGetPayload<{ select: typeof patientDetails }>

const patientDetails = Prisma.validator<Prisma.PatientSelect>()({
  id: true,
  salutation: true,
  firstName: true,
  lastName: true,
  //...
})

Im Repository definiere ich meine Typen mithilfe von Prisma – damit weiche ich meine strikte Trennung auf!

model Patient {
  id                  String                     @id @map("_id")
  salutation          Salutation
  firstName           String
  lastName            String
  //...
}

Die Prisma-DSL zur Definition einer MongoDB-Collection

Abschließender Gedanke

In den Codebeispielen wird deutlich, wie ich die strikte Trennung zwischen Controller- und Repository-Typen aufweiche. Damit mache ich mir einen potenziellen Wechsel von Prisma zu Dizzle schwerer. Meine Abwägung dabei ist folgende:

Die Wahrscheinlichkeit, dass ich auf ein Problem bei Prisma stoße, bei dem ich in diesem Projekt auf das ORM-Tool wechseln muss, schätze ich als gering ein. Die Menge an zusätzlichem Code, den ich schreiben muss, wenn ich den Kompromiss in der Typdefinition nicht eingehe, ist enorm groß.

Folglich heißt das: Wenn ich das Entwicklungsmuster beibehalte und in einem zukünftigen Projekt von Beginn an Drizzle nutze, kann ich selbst Jahre später zu diesem Projekt mit Prisma zurückkehren. Ich werde mich schnell zurechtfinden, weil ich mein bewährtes Vorgehen angewandt habe und es konsequent in meinen Projekten anwende.

Diese Art zu denken und Entscheidungen auf lange Sicht zu treffen unterscheidet den Anfänger vom Profi

Wie siehst du das?
call to action background image

Abonniere meinen Newsletter

Erhalte einmal im Monat Nachrichten aus den Bereichen Softwareentwicklung und Kommunikation gespikt mit Buch- und Linkempfehlungen.