Startseite / Artikel / Erstellen Sie eine typsichere GraphQL-API mit Prisma und Nexus in Node.js

Erstellen Sie eine typsichere GraphQL-API mit Prisma und Nexus in Node.js

Befolgen Sie eine siebenstufige Anleitung zur Erstellung einer Node.js GraphQL-API, die das Datenmodell von Prisma mit von Nexus generierten Typen und Resolvern vereint.

2244 Wörter

Erfahren Sie, wie Sie Prisma Nexus in ein Node.js-Projekt integrieren können, um type-sichere GraphQL-APIs zu erstellen – mit Schwerpunkt auf Schema-Design, Resolver-Logik und einem laufenden Server.

Stellen Sie sich ein GraphQL-Projekt vor, in dem derselbe „User“-Typ an vier verschiedenen Stellen definiert ist: in einem SDL-Dokument, einer manuell geschriebenen TypeScript-Schnittstelle, einem Prisma-Modell sowie in einem Zod-Validator, den ein Teamkollege Monate nach dem Start hinzugefügt hat. Jedes Mal, wenn sich eine dieser Definitionen ändert, gerät mindestens eine der anderen aus dem Gleichklang. Eine Korrektur wird veröffentlicht – plötzlich gehen die TypeScript-Typen weiterhin davon aus, dass phone erforderlich ist, obwohl die Spalte bereits Wochen zuvor aus der Datenbank verschwunden war.

Diese Art von Abweichungen ist genau das, was die Kombination von Prisma und Nexus verhindern soll. Nexus erstellt Ihr GraphQL-Schema sowie Ihre TypeScript-Typen direkt aus demselben Datenmodell, das Sie bereits in Prisma definiert haben. Es gibt eine einzige Quelle der Wahrheit, von der alles Weitere abgeleitet wird. Ändern Sie die Definition einmal – dann passen sich die Typen, das Schema sowie die Signaturen der Resolver automatisch an. Das klingt nach gesundem Menschenverstand, sobald man es laut ausspricht – die eigentliche Lektion ergibt sich jedoch daraus, ohne dieses Konzept zu arbeiten und zu spüren, wie teuer diese Lücke werden kann.

Dieser Leitfaden führt Sie Schritt für Schritt durch den Aufbau einer Node.js GraphQL-API von Grund auf mit Prisma und Nexus – in sieben Schritten, inklusive vollständigem Code ohne Auslassungen. Am Ende werden Sie einen funktionierenden Server haben, der mit PostgreSQL verbunden ist – etwas, das Sie selbstständig ausführen, erweitern und weiterentwickeln können. Er dient als solide Grundlage für einen echten E-Commerce-Backend in der Produktion, nicht als Demo, die bereits beim Hinzufügen eines zweiten Modells zusammenbricht.

Was Sie vor Schritt 1 bereithalten müssen

Ihnen werden folgende Dinge benötigt:

  • Node.js installiert – holen Sie sich die aktuelle LTS-Version von nodejs.org, falls Sie sie noch nicht haben.
  • Die Prisma CLI, die global verfügbar ist:
npm install -g prisma
  • Eine laufende PostgreSQL-Datenbank, auf die Sie zugreifen können. Ein lokaler Docker-Container, Supabases kostenlose Stufe oder Railway – die Hosting-Lösung spielt keine Rolle, solange Sie eine Verbindungszeichenkette zur Hand haben.

Eine Anmerkung für alle, die dies auf eine bestehende Codebasis anwenden statt auf ein neues Projekt: Während der ersten Migration versucht Prisma, schema.prisma mit dem zu vereinbaren, was bereits in der Datenbank vorhanden ist. Bei einem unübersichtlichen Legacy-Schema kann dieser Vereinbarungsprozess einen großen, überwältigenden Diff erzeugen. Lesen Sie ihn sorgfältig durch, bevor Sie ihn anwenden, und testen Sie immer zunächst in einer Entwicklungsumgebung. Wenn Sie von vorne anfangen, gilt das alles noch nicht für Sie.

Schritt 1: Das Projekt in Betrieb nehmen

Das ist der schnellste Schritt im gesamten Prozess. Erstellen Sie eine Dateiordner und holen Sie alle Abhängigkeiten auf einmal herunter:

mkdir prisma-nexus-graphql
cd prisma-nexus-graphql
# Initialize your project
npm init -y# Install required dependencies
npm install graphql nexus prisma express apollo-server-express path

Diese einzige Anweisung lädt alle sieben Pakete auf einmal herunter: den GraphQL-Runtime, Nexus zur Erstellung von Schemata basierend auf Code, Prisma selbst sowie das Apollo/Express-Paar, das den Server ausführt. Die gemeinsame Installation ist nicht nur praktisch – sie ermöglicht es npm, alle Abhängigkeiten in einer einzigen Schritt zu lösen, anstatt das Risiko unvereinbarer Minerversionen einzugehen, wenn man die Pakete nacheinander installiert.

Schritt 2: Prisma mit Ihrer Datenbank verbinden

npx prisma init

Beantworten Sie die Fragen und wählen Sie PostgreSQL aus. Nach Abschluss der Anweisung erscheinen zwei neue Dateien, die zuvor nicht vorhanden waren:

  • prisma/schema.prisma – hier befindet sich Ihr Datenmodell
  • .env – hier wird die Verbindungszeichenkette DATABASE_URL gespeichert, und sie sollte unverzüglich dorthin gelegt werden

Das ist keine Übertreibung. Bevor Sie das Schema anfassen, bevor Sie eine Migration ausführen oder irgendetwas anderes öffnen, fügen Sie Ihre Verbindungszeichenkette in .env ein. Ab diesem Zeitpunkt versucht im Grunde jeder Prisma-Befehl, auf die Datenbank zuzugreifen, und die Fehler, die auftreten, wenn die Zeichenkette fehlt oder fehlerhaft ist, sind äußerst unhilfreich. Anstelle einer klaren Meldung „Ungültige Verbindungszeichenkette“ erhalten Sie eine vage Beschwerde darüber, dass der Client nicht initialisiert wurde – und Sie können leicht fünfzehn Minuten damit verbringen, der falschen Ursache nachzugehen.

Schritt 3: Schreiben Sie das Prisma-Schema – Das ist nicht Ihr GraphQL-Schema

Falls Sie bereits mit GraphQL gearbeitet haben, aber noch nie zusammen mit Prisma, sollten Sie vermeiden, schema.prisma als Ort zu betrachten, an dem Sie die Schnittstelle Ihrer API entwerfen. Das ist nicht der Fall – es handelt sich dabei um eine Darstellung Ihrer Datenbankstruktur: Tabellen, Spalten, Beziehungen und Einschränkungen. Die tatsächliche Form der API wird später über Nexus daraus abgeleitet. Behalten Sie diesen Unterschied im Hinterkopf, denn er sorgt dafür, dass das gesamte mentale Modell kohärent bleibt.

// schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
}

Sobald Ihr Modell erstellt ist, führen Sie die Migration aus:

npx prisma migrate dev

This single command does two things nothing else in the setup does: it creates the actual table in your database, and it regenerates Prisma Client with TypeScript types that exactly match your current schema. Skip it, and Prisma Client simply won't recognize that a User model exists. What you get instead are type errors buried in generated files you don't control, with call stacks that lead nowhere useful — there's no clever shortcut around that. Run the migration every time your schema changes, without exception.

Step 4: Nexus — Why One More File Is Worth It

Zu diesem Zeitpunkt der Einrichtung ist es berechtigt, sich zu fragen, ob Nexus wirklich seinen Beitrag leistet. Es gibt nichts, was Sie daran hindert, einen GraphQL-Server ohne ihn zu bauen – schreiben Sie das SDL von Hand, definieren Sie Ihre TypeScript-Interfacen selbst und verbinden Sie alles manuell mit den Resolvern. Viele Codebasen machen genau das. Das Problem ist jedoch, dass dieser Ansatz einem bestimmten Typ von Fehler Tür und Tor öffnet: Das SDL beschreibt eine Form, die TypeScript-Typen eine leicht andere, und der Resolver gibt etwas völlig anderes zurück. herauszufinden, welche der drei Versionen die „echte“ ist, kostet oft mehr Zeit als die ursprüngliche Implementierung der Funktion selbst.

Nexus umgeht dieses Problem, indem er das SDL als generierten Ausgabeinhalt behandelt und nicht als etwas, das man manuell erstellt. Sie beschreiben Ihre Typen in TypeScript, und Nexus leitet sowohl das SDL als auch die entsprechenden Typdefinitionen aus dieser einzigen Quelle ab. Die drei Komponenten, die früher auseinanderdrifteten, bilden nun ein einziges Artefakt, das strukturell nicht mit sich selbst im Widerspruch sein kann. So sieht schema.ts aus:

// schema.ts
import { makeSchema } from 'nexus';
import path from 'path';
import * as resolvers from './resolvers';const schema = makeSchema({
  types: [resolvers],
  outputs: {
    schema: path.join(__dirname, './generated/schema.graphql'),
    typegen: path.join(__dirname, './generated/nexus.ts'),
  },
});export default schema;

Die outputs-Konfiguration weist Nexus an, wo die von ihm erzeugten Dateien platziert werden sollen: generated/schema.graphql enthält die SDL-Datei, und generated/nexus.ts enthält die entsprechenden TypeScript-Definitionen. Beide werden bei jedem Ausführungsvorgang neu erstellt, daher sollten Sie sie niemals manuell bearbeiten. Wenn Sie generated/nexus.ts öffnen und feststellen, dass etwas korrigiert werden muss, widerstehen Sie dem Drang, es direkt zu ändern – suchen Sie stattdessen die Quelldefinition und ändern Sie diese dort. Das Anpassen einer generierten Datei ist etwas wie das Anbringen eines Patches an einem kompilierten Binärdatei: Es funktioniert, bis der nächste Build Ihre Änderung stillschweigend überschreibt.

Schritt 5: Resolver – Verbindung des Schemas zur Datenbank

// resolvers.ts
import { extendType, stringArg, nonNull, objectType } from 'nexus';
import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export const User = objectType({
  name: 'User',
  definition(t) {
    t.nonNull.id('id')
    t.string('name')
    t.string('email')
  },
})export const Query = extendType({
  type: 'Query',
  definition(t) {
    t.list.field('users', {
      type: 'User',
      resolve: async () => {
        return await prisma.user.findMany();
      },
    });
  },
});export const Mutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.field('createUser', {
      type: 'User',
      args: {
        name: nonNull(stringArg()),
        email: nonNull(stringArg()),
      },
      resolve: async (_, args) => {
        return await prisma.user.create({
          data: {
            name: args.name,
            email: args.email,
          },
        });
      },
    });
  },
});

Achten Sie darauf, dass PrismaClient nur einmal instanziert wird, auf der obersten Ebene des Moduls, außerhalb eines Funktionskörpers. Diese Platzierung ist wichtiger, als es auf den ersten Blick scheinen mag. Jeder Aufruf von new PrismaClient() eröffnet eine neue Verbindung zur Datenbank. Wenn man ihn hingegen innerhalb eines Resolvers erstellen würde, würde bei jeder Anfrage eine neue Verbindung hergestellt werden. Bei normaler lokaler Entwicklung, mit vielleicht einer oder zwei Anfragen pro Sekunde, bemerkt die Datenbank den Unterschied nicht einmal. Unter echtem konkurrierendem Traffic – stellen Sie sich vor, einige hundert Käufer rufen gleichzeitig /checkout während eines Sales auf – wird dieses Muster die Verbindungsbeschränkung von PostgreSQL übersteigen und unter Last Fehler verursachen.

Die Deklaration des Clients auf Modulebene bedeutet, dass der gesamte Prozess eine einzige Verbindung teilt. Die Anfragen versuchen nicht, jeweils eigene Datenbankverbindungen zu öffnen; sie werden in einer Warteschlange für einen gemeinsamen Client angeordnet, der intern seinen eigenen Verbindungspool verwaltet. Solche Details werden von erfahrenen Node.js-Entwicklern instinktiv angewandt, während weniger erfahrene Teams sie in der Regel erst auf die harte Weise mitten im Incident herausfinden. Sie können nun diesen Lernprozess überspringen.

Schritt 6: Der Server

// server.ts
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import schema from './schema';const app = express();
const server = new ApolloServer({ schema });const startServer = async () => {
  await server.start(); // Start Apollo Server  server.applyMiddleware({ app }); // Apply Apollo Server middleware to Express  const PORT = process.env.PORT || 4000;  app.listen(PORT, () => {
    console.log(`Server is running at http://localhost:${PORT}/graphql`);
  });
}startServer().catch((err) => {
  console.error('Error starting the server:', err);
});

Ein Detail, das man beachten sollte, bevor man damit konfrontiert wird: await server.start() muss vor server.applyMiddleware() ausgeführt werden. Diese Reihenfolge war im Apollo Server 2 nicht erforderlich – Apollo 3 führte eine explizite asynchrone Startphase ein, und jedes Beispielcode, das vor Ende 2021 geschrieben wurde, fehlt vermutlich völlig diese Aufruf. Wenn man ihn überspringt, erhält man den Fehler Server must be started before calling server.applyMiddleware, was zumindest klar macht, was schiefgelaufen ist, auch wenn es nicht erklärt, warum diese Regel existiert. Sobald man den Grund versteht, handelt es sich um eine zwei Sekunden dauernde Korrektur statt um einen verwirrenden Umweg.

Schritt 7: Starten. Stören. Vertrauen.

node server.ts

Gehen Sie zu http://localhost:4000/graphql. Dadurch gelangen Sie in den GraphQL Playground. Führen Sie zunächst die Mutation aus:

// Fetch Users
query {
  users {
    id
    name
    email
  }
}
// Create Users
mutation {
  createUser(name: "John Doe", email: "john@example.com") {
    id
    name
    email
  }
}

Führen Sie die Mutation vor der Abfrage aus, damit tatsächlich Daten geladen werden können. Beobachten Sie, wie die gerade eingefügte Zeile in der Abfrageantwort zurückkommt. Führen Sie anschließend etwas aus, was die meisten Anleitungen überspringen: Öffnen Sie einen Datenbankklienten – psql, TablePlus, DBeaver oder was auch immer Sie haben – und inspizieren Sie die User-Tabelle direkt. Nicht das JSON, das die API zurückgegeben hat, sondern die Tabelle in ihrer Rohform.

Ihre Zeile befindet sich dort – sie wurde durch eine GraphQL-Mutation erstellt, die Sie in TypeScript mit Nexus-Typen definiert haben, über Prisma ausgeführt und in PostgreSQL gespeichert wurde. Jeder Schritt in dieser Kette hat funktioniert. Sie können genau den Punkt anzeigen, an dem Ihr Anwendungscode auf die Datenbank zugreift. Für Personen mit jahrelanger Erfahrung in REST-Endpunkten und handgeschriebenem SQL ist das in der Regel der Moment, in dem dieser Stack nicht mehr wie ein Diagramm wirkt, sondern wie etwas Reales.

Was Sie bereits gebaut haben und was Sie noch hinzufügen müssen

Was Sie jetzt haben, ist eine funktionsfähige Backend-Grundlage – kein bloßes Beispiel zum Ausprobieren. Das Muster, dem Sie gerade gefolgt sind – ein Prisma-Modell definieren, eine Migration ausführen, einen Nexus objectType hinzufügen, den Resolver schreiben und ihn mit dem Apollo/Express-Server verbinden – ist genau das, was Sie für jedes weitere Modell wiederholen werden. Egal ob es sich um Product, Order oder Cart handelt: Die Schritte bleiben unverändert, genauso wie die Garantien. Fügen Sie eine Beziehung in schema.prisma hinzu, führen Sie migrate dev aus und implementieren anschließend den Resolver – dann werden Ihre Typen automatisch aktualisiert. Diese automatische Synchronisierung ist tatsächlich der wesentliche Vorteil dieser Konfiguration: Sie müssen sich nicht mehr darauf verlassen, dass das Gedächtnis Ihr Schema, Ihre Typen und Resolver in Einklang hält, denn die Tools sorgen dafür.

Was bislang auffällig fehlt, sind Authentifizierung, Autorisierung, Rate Limiting sowie Validierung der Eingaben. Nexus garantiert lediglich, dass Ihre Typen korrekt sind. Er sagt nichts darüber aus, wer welche Operation aufrufen darf. Derzeit reagiert die createUser-Mutation gerne auf jeden, der auf Port 4000 zugreifen kann. Das ist beim lokalen Entwickeln akzeptabel. Sobald die API über eine echte URL erreichbar ist, wird das unzulässig. Bevor die API in ein Umfeld gelangt, auf das andere Personen Zugriff haben können, muss ein Authentifizierungs-Middleware hinzugefügt werden.

Für eine ausführlichere Darstellung sollten Sie sich die Prisma-Dokumentation zu Beziehungen, Filtern und Paginierung sowie die Nexus-Dokumentation zu Berechtigungen auf Feldebene und benutzerdefinierten Skalaren ansehen. Beide Dokumentationssets sind gut strukturiert, sodass man sie von Anfang bis Ende durchlesen kann, anstatt nur nach einer Lösung zu suchen, wenn etwas nicht funktioniert – eine Eigenschaft, die in technischen Dokumentationen seltener vorkommen sollte.

Zusätzliche Literatur

  • Jest durch Node’s eingebauten Testlaufzeitmechanismus in Node 24 ersetzen — Eine Praxisbeispiel-Migration zeigt, wie Node 24’s integrierter Testlaufzeitmechanismus sowie die native TypeScript-Unterstützung die CI-Zeit verkürzen und gleichzeitig vier Abhängigkeiten beseitigen.
  • Behebung des Fehlers „libssl.so.1.1 fehlt“ bei Prisma in Alpine Docker — Erfahren Sie, warum Prismas Abfragemotor in auf Alpine basierenden Docker-Images wegen eines fehlenden libssl-Fehlers abstürzt, und wie dieser dauerhaft behoben werden kann.
  • Von Prisma zu Drizzle migrieren: Eine Rückblick nach sechs Monaten — Ein Entwickler teilt tatsächliche Benchmarks sowie die Vor- und Nachteile beim Umstieg eines PostgreSQL- mit TypeScript-Stacks von Prisma auf den Drizzle ORM.