NestJS GraphQL Module: Code-First-Resolver mit Decorators
AI generated
{ }
type
GraphQL · NestJS · TypeScript
NestJS GraphQL Module
Code-First-Resolver mit Decorators bauen

Das NestJS GraphQL Module generiert das komplette GraphQL-Schema automatisch aus TypeScript-Klassen und Decorators, statt eine separate SDL-Datei zu pflegen. ObjectType, Field, Resolver und Args-Decorator bilden zusammen einen Code-First-Ansatz, bei dem Typsicherheit vom Datenbankmodell bis zur GraphQL-Response durchgängig erhalten bleibt, inklusive Autovervollständigung und Refactoring-Unterstützung in der IDE.

19 Min. Lesezeit NestJS · Apollo Driver · Decorators · DataLoader TypeScript · class-validator · PubSub

1. Code-First vs. Schema-First in NestJS

Das NestJS GraphQL Module unterstützt zwei Ansätze: Schema-First, bei dem SDL-Dateien separat gepflegt und per Codegenerierung mit TypeScript-Interfaces synchronisiert werden, und Code-First, bei dem TypeScript-Klassen mit Decorators die alleinige Quelle der Wahrheit sind und das Schema zur Laufzeit automatisch daraus generiert wird. In der Praxis hat sich Code-First als Standardansatz für NestJS-Projekte durchgesetzt, weil er die doppelte Pflege von SDL-Datei und TypeScript-Typen eliminiert.

Der entscheidende Vorteil von Code-First mit Decorators: Ändert sich ein Feld in der TypeScript-Klasse, etwa ein umbenanntes Property, ändert sich das generierte GraphQL-Schema automatisch mit, ohne manuellen Synchronisationsschritt. Der TypeScript-Compiler und die IDE erkennen Inkonsistenzen sofort, weil Schema und Anwendungscode dieselbe Quelle sind. Dieser Artikel konzentriert sich konsequent auf den Code-First-Ansatz, da er für neue NestJS-Projekte die empfohlene und in der Community dominierende Wahl ist.

2. GraphQLModule mit Apollo Driver einrichten

Die Grundlage jeder NestJS-GraphQL-API ist das GraphQLModule, das über einen Treiber, meist ApolloDriver, mit der eigentlichen GraphQL-Ausführungsengine verbunden wird. Bei autoSchemaFile generiert NestJS die SDL-Repräsentation automatisch aus den Decorators und schreibt sie optional als Datei ins Projekt, praktisch als lesbares Referenzdokument, ohne dass diese Datei manuell gepflegt werden müsste.

Wichtige Konfigurationsoptionen betreffen den context-Callback, der Request-spezifische Daten wie den authentifizierten Nutzer oder DataLoader-Instanzen für alle Resolver verfügbar macht, und playground beziehungsweise Apollo Sandbox für die interaktive Entwicklung. Für produktive Deployments sollte die Introspection und der Playground standardmäßig deaktiviert werden, außer für interne, kontrollierte APIs, bei denen Schema-Transparenz gewünscht ist.


// app.module.ts — GraphQLModule with Apollo Driver, code-first schema generation
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { join } from "path";
import { ProductModule } from "./product/product.module";

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), "src/schema.gql"),
      sortSchema: true,
      playground: process.env.NODE_ENV !== "production",
      context: ({ req }: { req: Request }) => ({ req }),
    }),
    ProductModule,
  ],
})
export class AppModule {}

3. ObjectType und Field Decorators

Mit dem @ObjectType()-Decorator wird eine TypeScript-Klasse zu einem GraphQL-Typ, und jedes mit @Field() annotierte Property wird zu einem GraphQL-Feld. NestJS liest dabei den TypeScript-Typ per Reflection aus und leitet daraus den passenden GraphQL-Skalar oder -Typ ab, allerdings nur zuverlässig bei primitiven Typen. Für komplexere Fälle, etwa Arrays oder explizite Nullable-Angaben, muss der GraphQL-Typ im Decorator explizit angegeben werden, weil TypeScripts Typinformationen zur Laufzeit nicht vollständig erhalten bleiben.

Ein häufiger Stolperstein: @Field() ohne explizite Typangabe funktioniert nur für string, number, boolean und Enums zuverlässig. Bei Arrays von Objekttypen, etwa einer Liste verknüpfter Kategorien, muss der Typ als Callback-Funktion übergeben werden, @Field(() => [Category]), um zirkuläre Import-Probleme zwischen wechselseitig referenzierenden Typen zu vermeiden, ein Pattern, das direkt aus Angulars Dependency-Injection-System übernommen wurde.


// product.type.ts — ObjectType decorator generates the GraphQL type automatically
import { ObjectType, Field, ID, Float } from "@nestjs/graphql";
import { Category } from "../category/category.type";

@ObjectType()
export class Product {
  @Field(() => ID)
  id: string;

  @Field()
  sku: string;

  @Field()
  name: string;

  @Field(() => Float)
  price: number;

  @Field(() => [Category])
  categories: Category[];

  @Field({ nullable: true })
  discountedPrice?: number;
}

4. Resolver-Klassen mit Query und Mutation

Resolver-Klassen tragen den @Resolver(() => Product)-Decorator und definieren einzelne Felder als Methoden mit @Query() für lesende und @Mutation() für schreibende Operationen. NestJS' Dependency-Injection-Container steht dabei vollständig zur Verfügung, sodass Repository- oder Service-Klassen ganz normal per Konstruktor injiziert werden, exakt wie in REST-Controllern. Diese Konsistenz zwischen REST- und GraphQL-Schicht ist einer der größten praktischen Vorteile des NestJS-Ansatzes gegenüber einem eigenständigen Apollo-Server-Setup.

Der Decorator @ResolveField() definiert zusätzliche, im ObjectType noch nicht direkt aufgelöste Felder, etwa berechnete Werte oder Beziehungen, die aus Performance-Gründen erst bei tatsächlicher Anfrage nachgeladen werden sollen. Kombiniert mit dem @Parent()-Decorator, der auf den bereits aufgelösten Elternwert zugreift, lässt sich so eine saubere Trennung zwischen einfachen Datenfeldern und teureren, abgeleiteten Feldern erreichen.


// product.resolver.ts — Resolver with injected service, query and mutation
import { Resolver, Query, Mutation, Args, ResolveField, Parent } from "@nestjs/graphql";
import { Product } from "./product.type";
import { CreateProductInput } from "./dto/create-product.input";
import { ProductService } from "./product.service";
import { CategoryService } from "../category/category.service";

@Resolver(() => Product)
export class ProductResolver {
  constructor(
    private readonly productService: ProductService,
    private readonly categoryService: CategoryService,
  ) {}

  @Query(() => [Product])
  products(): Promise<Product[]> {
    return this.productService.findAll();
  }

  @Query(() => Product, { nullable: true })
  product(@Args("sku") sku: string): Promise<Product | null> {
    return this.productService.findBySku(sku);
  }

  @Mutation(() => Product)
  createProduct(@Args("input") input: CreateProductInput): Promise<Product> {
    return this.productService.create(input);
  }

  @ResolveField(() => Number, { nullable: true })
  discountedPrice(@Parent() product: Product): number | undefined {
    return this.productService.applyActiveDiscount(product);
  }
}

5. Input-Types und Validation mit class-validator

Für Mutation-Argumente definiert NestJS eigene @InputType()-Klassen, die strukturell den ObjectTypes ähneln, aber semantisch eingehende Daten repräsentieren. Kombiniert mit class-validator-Decorators wie @IsString(), @Min() oder @IsEmail() direkt auf den Input-Type-Properties, validiert eine global registrierte ValidationPipe jede eingehende Mutation automatisch, bevor der Resolver überhaupt aufgerufen wird, exakt wie bei REST-Controllern in NestJS.

Dieses Pattern spart erheblichen Code gegenüber manueller Validierung im Resolver-Body und sorgt für konsistente Fehlerformate über die gesamte API hinweg. Schlägt die Validierung fehl, wandelt NestJS die geworfene BadRequestException automatisch in einen GraphQL-Fehler mit passendem Format um, inklusive einer Liste der einzelnen Validierungsfehler pro Feld im extensions-Objekt.


// create-product.input.ts — InputType with class-validator decorators
import { InputType, Field, Float } from "@nestjs/graphql";
import { IsString, IsNotEmpty, Min, MaxLength } from "class-validator";

@InputType()
export class CreateProductInput {
  @Field()
  @IsString()
  @IsNotEmpty()
  @MaxLength(64)
  sku: string;

  @Field()
  @IsString()
  @IsNotEmpty()
  name: string;

  @Field(() => Float)
  @Min(0)
  price: number;
}

6. DataLoader-Integration gegen N+1

Wie bei jeder GraphQL-Implementierung führen verschachtelte Felder ohne Batching zum N+1-Problem: eine Liste von Produkten mit jeweils aufgelösten Kategorien erzeugt eine Datenbankabfrage pro Produkt statt einer gebündelten Abfrage. In NestJS wird das DataLoader-Pattern typischerweise als Request-scoped Provider implementiert, der pro Request eine neue DataLoader-Instanz erzeugt und über den context-Callback des GraphQLModule allen Resolvern zur Verfügung stellt.

Wichtig ist die Request-Scope, weil DataLoader-Instanzen zwischen Requests niemals geteilt werden dürfen, sonst mischen sich Daten unterschiedlicher Nutzer oder es entstehen veraltete gecachte Werte, die über die eigentliche Anfrage hinaus bestehen bleiben. Der DataLoader selbst sammelt alle angefragten IDs innerhalb eines Event-Loop-Ticks und löst sie in einer einzigen findByIds-Abfrage auf, bevor die Ergebnisse den ursprünglichen Promises zugeordnet werden.


// category.loader.ts — request-scoped DataLoader batching category lookups
import { Injectable, Scope } from "@nestjs/common";
import DataLoader from "dataloader";
import { CategoryService } from "./category.service";
import { Category } from "./category.type";

@Injectable({ scope: Scope.REQUEST })
export class CategoryLoader {
  private readonly loader: DataLoader<string, Category[]>;

  constructor(private readonly categoryService: CategoryService) {
    this.loader = new DataLoader(async (productIds: readonly string[]) => {
      const byProduct = await this.categoryService.findByProductIds([...productIds]);
      return productIds.map((id) => byProduct[id] ?? []);
    });
  }

  load(productId: string): Promise<Category[]> {
    return this.loader.load(productId);
  }
}

7. Guards und Interceptors für Auth in Resolvern

NestJS' Guard-System funktioniert für GraphQL-Resolver identisch wie für REST-Controller, mit einem Detail: Der ExecutionContext muss über GqlExecutionContext.create() in den GraphQL-spezifischen Kontext umgewandelt werden, um an Request, Args oder den authentifizierten Nutzer zu gelangen. Ein @UseGuards(GqlAuthGuard)-Decorator auf Resolver-Ebene schützt so ganze Klassen oder einzelne Methoden, exakt nach demselben Muster wie in der REST-Schicht.

Interceptors eignen sich gut für Cross-Cutting Concerns wie Logging, Performance-Messung oder das automatische Anreichern von Response-Daten, unabhängig davon, ob der Request über REST oder GraphQL hereinkam. Diese Wiederverwendbarkeit von NestJS' Kernkonzepten über beide Protokolle hinweg ist ein zentraler Grund, warum viele Teams NestJS gegenüber einem eigenständigen Apollo-Server-Setup bevorzugen, wenn GraphQL und REST im selben Projekt koexistieren.

8. Subscriptions mit PubSub

Für Echtzeit-Funktionalität nutzt das NestJS GraphQL Module das @Subscription()-Decorator-Pattern in Kombination mit einer PubSub-Implementierung, standardmäßig eine einfache In-Memory-Variante für Entwicklung, produktiv meist graphql-redis-subscriptions für Verteilung über mehrere Server-Instanzen. Ein Resolver publiziert Events über pubSub.publish(), während die Subscription-Methode einen Async-Iterator zurückgibt, den NestJS automatisch über WebSockets an verbundene Clients weiterleitet.

Für produktive Setups mit mehreren horizontal skalierten Server-Instanzen ist die In-Memory-PubSub-Variante ungeeignet, weil Events nur innerhalb derselben Node.js-Instanz sichtbar sind. Redis als geteilter Message-Broker löst dieses Problem, indem alle Instanzen dasselbe Pub/Sub-Topic abonnieren und Events instanzübergreifend verteilt werden, unabhängig davon, welche Instanz das ursprüngliche Mutation-Event ausgelöst hat.

9. NestJS GraphQL vs. Apollo Server standalone

Ob sich der Umweg über NestJS lohnt oder ein direktes Apollo-Server-Setup ausreicht, hängt stark davon ab, wie viel Struktur ein Projekt bereits braucht.

Kriterium NestJS GraphQL Module Apollo Server standalone
Dependency Injection Eingebaut, konsistent mit REST Manuell zu konfigurieren
Schema-Generierung Automatisch aus Decorators Manuell (SDL oder Code-First-Bibliothek)
REST + GraphQL im selben Projekt Nativ unterstützt Zusätzlicher Framework-Layer nötig
Einstiegsaufwand Höher, mehr Konzepte Niedriger für reine GraphQL-Projekte
Guards, Interceptors, Pipes Wiederverwendbar über Protokolle Muss selbst gebaut werden

Für reine, kleine GraphQL-Microservices ohne REST-Anteil ist ein schlankes Apollo-Server-Setup oft schneller aufgesetzt. Für größere Projekte, die ohnehin NestJS als Backend-Framework nutzen oder REST und GraphQL parallel betreiben, überwiegt der Vorteil der konsistenten Architektur mit wiederverwendbaren Guards, Interceptors und Dependency Injection klar.

Mironsoft

TypeScript-Backends und GraphQL-Architektur mit NestJS

GraphQL-API mit NestJS strukturiert aufbauen?

Wir konzipieren Resolver-Architektur, Input-Validation und DataLoader-Strategie für euer NestJS-GraphQL-Backend, richten Guards für Auth sauber ein und sorgen für ein typsicheres Schema von der Datenbank bis zur Response.

Schema-Architektur

ObjectTypes, Resolver und Module sauber strukturieren und typisieren

Performance

Request-scoped DataLoader gegen N+1-Probleme in verschachtelten Queries

Auth & Realtime

Guards für Auth und Redis-gestützte Subscriptions für Echtzeit-Updates

10. Zusammenfassung

Das NestJS GraphQL Module im Code-First-Ansatz generiert das komplette Schema automatisch aus TypeScript-Klassen und Decorators wie @ObjectType(), @Field(), @Resolver() und @InputType(). Statt SDL und TypeScript-Typen manuell zu synchronisieren, bleibt eine einzige Quelle der Wahrheit erhalten, mit voller IDE-Unterstützung und automatischer Fehlererkennung durch den TypeScript-Compiler.

Request-scoped DataLoader-Provider lösen N+1-Probleme sauber innerhalb von NestJS' Dependency-Injection-System, Guards und Interceptors funktionieren identisch für REST und GraphQL, und class-validator-Decorators auf Input-Types sparen manuellen Validierungscode. Für Projekte, die bereits NestJS als Backend-Framework nutzen oder REST und GraphQL parallel betreiben, ist der Code-First-Ansatz mit Decorators die naheliegende, konsistente Wahl.

NestJS GraphQL Module — Das Wichtigste auf einen Blick

Code-First-Decorators

@ObjectType(), @Field(), @Resolver() generieren das Schema automatisch aus TypeScript-Klassen.

Validation

@InputType() plus class-validator-Decorators validieren Mutations automatisch vor dem Resolver-Aufruf.

DataLoader

Request-scoped Provider verhindern N+1-Datenbankabfragen bei verschachtelten Feldern.

Guards & Subscriptions

GqlExecutionContext für Auth-Guards, Redis-PubSub für Subscriptions über mehrere Instanzen.

11. FAQ: NestJS GraphQL Module

1Was ist Code-First im NestJS GraphQL Module?
TypeScript-Klassen mit Decorators sind die einzige Quelle der Wahrheit, das Schema wird automatisch generiert.
2Wann braucht @Field() eine Typangabe?
Bei Arrays von Objekttypen oder zirkulären Referenzen, weil TypeScripts Laufzeit-Typinformationen unvollständig sind.
3Wie funktioniert DI in Resolvern?
Exakt wie bei REST-Controllern, per Konstruktor-Injektion über NestJS' Dependency-Injection-Container.
4Wie werden Mutations validiert?
Mit InputType-Klassen und class-validator-Decorators, automatisch geprüft durch eine global registrierte ValidationPipe.
5Wie löst man N+1-Probleme?
Mit request-scoped DataLoader-Providern, verteilt über den context-Callback des GraphQLModule.
6Funktionieren Guards wie bei REST?
Fast identisch, nur mit GqlExecutionContext.create() für den Zugriff auf Request und Args.
7Wie funktionieren Subscriptions?
Mit @Subscription() und PubSub, produktiv mit graphql-redis-subscriptions statt In-Memory.
8Lohnt sich NestJS gegenüber Apollo Server?
Für kleine reine GraphQL-Dienste oft nicht nötig, für Projekte mit REST und GraphQL im selben Backend klar überlegen.
9Können Schema-First und Code-First gemischt werden?
Technisch möglich, aber unüblich, da Mischformen die jeweiligen Vorteile abschwächen.
10Wie testet man Resolver?
Mit Jest-Unit-Tests und gemockten Services, ergänzt durch Supertest-Integrationstests gegen das NestJS-Testing-Modul.