NestJS und MongoDB 2026: Mongoose-Integration, Aggregationen und Interview-Fragen

Praxisleitfaden für NestJS 12 mit MongoDB und Mongoose 9: Schema-Design, Aggregation-Pipelines, Transaktionen und technische Interview-Fragen für Backend-Entwickler.

NestJS und MongoDB 2026: Mongoose-Integration, Aggregationen und Interview-Fragen

NestJS 12 in Kombination mit MongoDB über Mongoose 9 bietet einen produktionsreifen Stack für skalierbare Node.js-Backends. Dieser Leitfaden behandelt Schema-Design-Patterns, Aggregation-Pipelines und die Interview-Fragen, die Senior-Kandidaten von Junioren unterscheiden.

Kurzreferenz

Mongoose 9.9.4 erfordert Node.js 18+ und unterstützt MongoDB 6.0 bis 8.0. NestJS 12 liefert ESM-fähige Pakete, bleibt aber abwärtskompatibel mit CommonJS-Projekten.

Mongoose in einer NestJS 12-Anwendung einrichten

Das Paket @nestjs/mongoose integriert Mongoose mit der NestJS-Dependency-Injection. Die Installation erfolgt über npm:

bash
# Install Mongoose integration
npm install @nestjs/mongoose mongoose

Die Verbindung wird im Root-Modul registriert:

app.module.tstypescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot(process.env.MONGODB_URI, {
      // Connection pool size for production workloads
      maxPoolSize: 10,
      // Timeout after 10 seconds if connection fails
      serverSelectionTimeoutMS: 10000,
    }),
  ],
})
export class AppModule {}

Die forRoot-Methode akzeptiert alle Mongoose-Verbindungsoptionen. Die Einstellung maxPoolSize verhindert Connection-Erschöpfung unter Last – ein häufiges Produktionsproblem.

Schema-Design mit TypeScript-Decorators

Mongoose-Schemas in NestJS verwenden Decorators aus @nestjs/mongoose. Jedes Schema wird einer MongoDB-Collection zugeordnet.

user.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';

// Document type for TypeScript autocomplete
export type UserDocument = HydratedDocument<User>;

@Schema({
  timestamps: true, // Adds createdAt and updatedAt
  collection: 'users', // Explicit collection name
})
export class User {
  // MongoDB ObjectId, auto-generated
  _id: Types.ObjectId;

  @Prop({ required: true, unique: true, index: true })
  email: string;

  @Prop({ required: true })
  passwordHash: string;

  @Prop({ type: String, enum: ['admin', 'user', 'guest'], default: 'user' })
  role: string;

  @Prop({ type: [String], default: [] })
  permissions: string[];
}

export const UserSchema = SchemaFactory.createForClass(User);

Der @Prop-Decorator definiert Feld-Constraints. Das Setzen von index: true auf häufig abgefragten Feldern verbessert die Leseleistung auf Kosten langsamerer Schreiboperationen.

Interview-Einblick

Interviewer fragen nach dem Trade-off zwischen eingebetteten Dokumenten und Referenzen. Eingebettete Dokumente eignen sich für gemeinsam abgerufene Daten (Benutzerprofil + Einstellungen). Referenzen passen zu Daten mit unbegrenztem Wachstum oder unabhängigen Abfrageanforderungen (Benutzer + Bestellungen).

Repository-Pattern mit Injectable Services

NestJS empfiehlt die Trennung der Datenbanklogik in Services. Der @InjectModel-Decorator ermöglicht den Zugriff auf das Mongoose-Model.

user.service.tstypescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User, UserDocument } from './user.schema';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
  ) {}

  async findById(id: string): Promise<UserDocument> {
    // Validate ObjectId format before querying
    if (!Types.ObjectId.isValid(id)) {
      throw new NotFoundException('Invalid user ID format');
    }
    const user = await this.userModel.findById(id).exec();
    if (!user) {
      throw new NotFoundException(`User ${id} not found`);
    }
    return user;
  }

  async findByEmail(email: string): Promise<UserDocument | null> {
    // Case-insensitive email lookup
    return this.userModel.findOne({ 
      email: { $regex: new RegExp(`^${email}$`, 'i') } 
    }).exec();
  }

  async create(data: Partial<User>): Promise<UserDocument> {
    const user = new this.userModel(data);
    return user.save();
  }
}

Der Aufruf von .exec() gibt ein echtes Promise anstelle eines Mongoose-Query-Objekts zurück. Dies ist wichtig für korrektes async/await-Verhalten und Fehler-Stack-Traces.

Aggregation-Pipelines für komplexe Abfragen

MongoDB-Aggregationen bewältigen Reporting, Analytics und Datentransformationen, die SQL-Datenbanken mit Joins und GROUP BY lösen.

analytics.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { Order, OrderDocument } from './order.schema';

@Injectable()
export class AnalyticsService {
  constructor(
    @InjectModel(Order.name) private orderModel: Model<OrderDocument>,
  ) {}

  async getRevenueByMonth(year: number): Promise<MonthlyRevenue[]> {
    const pipeline: PipelineStage[] = [
      // Stage 1: Filter orders by year
      {
        $match: {
          createdAt: {
            $gte: new Date(`${year}-01-01`),
            $lt: new Date(`${year + 1}-01-01`),
          },
          status: 'completed',
        },
      },
      // Stage 2: Group by month, sum revenue
      {
        $group: {
          _id: { $month: '$createdAt' },
          totalRevenue: { $sum: '$amount' },
          orderCount: { $sum: 1 },
          avgOrderValue: { $avg: '$amount' },
        },
      },
      // Stage 3: Sort by month ascending
      { $sort: { _id: 1 } },
      // Stage 4: Reshape output
      {
        $project: {
          _id: 0,
          month: '$_id',
          totalRevenue: { $round: ['$totalRevenue', 2] },
          orderCount: 1,
          avgOrderValue: { $round: ['$avgOrderValue', 2] },
        },
      },
    ];

    return this.orderModel.aggregate(pipeline).exec();
  }
}

Aggregation-Pipelines verarbeiten Dokumente sequenziell durch Stufen. Jede Stufe transformiert die Ausgabe für die nächste. Die $match-Stufe filtert früh, um die Anzahl der in nachfolgenden Stufen verarbeiteten Dokumente zu reduzieren.

Bereit für deine Node.js / NestJS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Transaktionen für Multi-Dokument-Operationen

MongoDB 4.0+ unterstützt ACID-Transaktionen über mehrere Dokumente. Transaktionen werden eingesetzt, wenn mehrere Dokumente atomar aktualisiert werden müssen.

transfer.service.tstypescript
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
import { Connection, Model, ClientSession } from 'mongoose';
import { Account, AccountDocument } from './account.schema';

@Injectable()
export class TransferService {
  constructor(
    @InjectConnection() private connection: Connection,
    @InjectModel(Account.name) private accountModel: Model<AccountDocument>,
  ) {}

  async transfer(
    fromId: string,
    toId: string,
    amount: number,
  ): Promise<void> {
    // Start a session for the transaction
    const session: ClientSession = await this.connection.startSession();
    
    try {
      await session.withTransaction(async () => {
        // Debit source account
        const source = await this.accountModel.findOneAndUpdate(
          { _id: fromId, balance: { $gte: amount } },
          { $inc: { balance: -amount } },
          { session, new: true },
        );
        
        if (!source) {
          throw new BadRequestException('Insufficient balance or account not found');
        }

        // Credit destination account
        const dest = await this.accountModel.findByIdAndUpdate(
          toId,
          { $inc: { balance: amount } },
          { session, new: true },
        );

        if (!dest) {
          throw new BadRequestException('Destination account not found');
        }
      });
    } finally {
      await session.endSession();
    }
  }
}

Der session.withTransaction-Wrapper handhabt Commit und Rollback automatisch. Wenn eine Operation einen Fehler wirft, wird die gesamte Transaktion abgebrochen.

Produktionshinweis

Transaktionen erfordern ein MongoDB-Replica-Set oder Sharded-Cluster. Standalone-MongoDB-Instanzen unterstützen keine Transaktionen. Atlas M0/M2/M5-Tiers beinhalten standardmäßig Replica-Sets.

Indexierungsstrategien für Query-Performance

Indizes bestimmen die Abfrageleistung. Ohne geeignete Indizes durchsucht MongoDB ganze Collections.

product.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type ProductDocument = HydratedDocument<Product>;

@Schema({ timestamps: true })
export class Product {
  @Prop({ required: true, index: true })
  sku: string;

  @Prop({ required: true })
  name: string;

  @Prop({ required: true })
  category: string;

  @Prop({ required: true })
  price: number;

  @Prop({ default: true })
  inStock: boolean;
}

export const ProductSchema = SchemaFactory.createForClass(Product);

// Compound index for common query pattern
ProductSchema.index({ category: 1, price: -1 });

// Text index for search functionality
ProductSchema.index({ name: 'text', sku: 'text' });

Zusammengesetzte Indizes unterstützen Abfragen, die nach mehreren Feldern filtern oder sortieren. Der Index { category: 1, price: -1 } optimiert Abfragen wie find({ category }).sort({ price: -1 }).

Häufige Interview-Fragen zu NestJS und MongoDB

Technische Interviews testen sowohl konzeptionelles Verständnis als auch praktische Erfahrung. Diese Fragen erscheinen häufig bei Senior-Backend-Positionen.

Frage: Wie handhabt Mongoose Connection-Pooling?

Mongoose verwaltet intern einen Connection-Pool. Die Option maxPoolSize (Standard: 100) begrenzt gleichzeitige Verbindungen. Jede Operation bezieht eine Verbindung aus dem Pool, führt sie aus und gibt sie zurück. Connection-Pooling vermeidet den Overhead des Aufbaus neuer TCP-Verbindungen pro Abfrage.

Frage: Wann sollten eingebettete Dokumente statt Referenzen verwendet werden?

Daten einbetten, die zusammengehören und begrenztes Wachstum haben. Lieferadressen eines Benutzers (maximal 5-10) lassen sich gut einbetten. Bestellpositionen werden in das Bestelldokument eingebettet. Referenzen eignen sich für unbegrenzte Beziehungen: Bestellungen eines Benutzers über Jahre oder Produkte in einer Kategorie. Die Regel: Wenn die Daten zu 90% gemeinsam geladen werden und unter 16MB bleiben, einbetten.

Frage: Erklären Sie die $lookup-Stufe der Aggregation-Pipeline.

Die $lookup-Stufe führt einen Left-Outer-Join zwischen Collections durch. Sie matched Dokumente aus einer fremden Collection basierend auf Feldgleichheit oder einer benutzerdefinierten Pipeline. Anders als SQL-Joins wird $lookup während der Aggregation ausgeführt und kann zusätzliche Filterung und Projektion innerhalb des Joins enthalten.

typescript
// Example: Orders with customer details
const pipeline: PipelineStage[] = [
  {
    $lookup: {
      from: 'customers',
      localField: 'customerId',
      foreignField: '_id',
      as: 'customer',
    },
  },
  { $unwind: '$customer' },
];

Frage: Wie werden Schema-Migrationen in MongoDB gehandhabt?

MongoDB-Schemas entwickeln sich anders als SQL. Gängige Strategien:

  • Neue Felder mit Standardwerten hinzufügen (abwärtskompatibel)
  • Migrationsskripte ausführen, die bestehende Dokumente in Batches aktualisieren
  • Schema-Versionierung mit einem schemaVersion-Feld verwenden
  • Mongoose-Middleware (pre('save')) kann Dokumente beim Schreiben transformieren
Interview-Tipp

Senior-Kandidaten erklären Trade-offs. Junioren listen Features auf. Bei der Frage nach eingebetteten vs. referenzierten Dokumenten sollten Abfragemuster, Dokumentgrößenlimits (16MB) und Aktualisierungshäufigkeit diskutiert werden – nicht nur "es kommt darauf an".

Fehlerbehandlung und Validierungsmuster

Mongoose-Validierung läuft vor Save-Operationen. Benutzerdefinierte Validatoren behandeln Geschäftslogik.

order.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type OrderDocument = HydratedDocument<Order>;

@Schema({ timestamps: true })
export class Order {
  @Prop({
    required: true,
    validate: {
      validator: (v: number) => v > 0,
      message: 'Amount must be positive',
    },
  })
  amount: number;

  @Prop({
    type: String,
    enum: ['pending', 'processing', 'completed', 'cancelled'],
    default: 'pending',
  })
  status: string;

  @Prop({ required: true })
  items: OrderItem[];
}

export const OrderSchema = SchemaFactory.createForClass(Order);

// Pre-save hook for computed fields
OrderSchema.pre('save', function (next) {
  // Recalculate total from items
  if (this.isModified('items')) {
    this.amount = this.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0,
    );
  }
  next();
});

Die pre('save')-Middleware läuft vor jeder Save-Operation. Sie wird für berechnete Felder, Audit-Logging oder kaskadierende Updates verwendet.

Performance-Monitoring mit explain()

Die explain()-Methode zeigt Query-Ausführungspläne. Sie wird verwendet, um fehlende Indizes und langsame Abfragen zu identifizieren.

typescript
// Debug query performance
async analyzeQuery(category: string): Promise<void> {
  const explanation = await this.productModel
    .find({ category, inStock: true })
    .sort({ price: -1 })
    .explain('executionStats');

  console.log('Documents examined:', explanation.executionStats.totalDocsExamined);
  console.log('Documents returned:', explanation.executionStats.nReturned);
  console.log('Execution time (ms):', explanation.executionStats.executionTimeMillis);
  // If totalDocsExamined >> nReturned, add an index
}

Ein Verhältnis von totalDocsExamined zu nReturned nahe 1 zeigt effiziente Index-Nutzung an. Hohe Verhältnisse signalisieren fehlende Indizes oder nicht-selektive Abfragen.

Kernpunkte für NestJS MongoDB-Entwicklung

  • maxPoolSize basierend auf erwarteter Parallelität konfigurieren, Standard 100 passt für die meisten Workloads
  • HydratedDocument<T> für korrektes TypeScript-Typing von Mongoose-Dokumenten verwenden
  • .exec() bei Abfragen aufrufen, um native Promises mit akkuraten Stack-Traces zu erhalten
  • $match-Stufen früh in Aggregation-Pipelines platzieren, um verarbeitete Dokumente zu reduzieren
  • Transaktionen erfordern Replica-Sets, Deployment-Topologie vor Abhängigkeit verifizieren
  • Zusammengesetzte Indizes passend zu Abfragemustern erstellen: Filterfelder zuerst, Sortierfelder danach
  • Dokumente mit begrenztem Wachstum einbetten, unbegrenzt wachsende Dokumente referenzieren
  • Query-Performance mit explain('executionStats') während der Entwicklung überwachen

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tägliche Challenge

Findest du den Bug in Node.js / NestJS?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 28. August 2026

Teilen

Verwandte Artikel