Bordered avatar

Street Learner

Author
8 min read

Last Updated: a year ago

Mastering GraphQL in NestJS with MongoDB: Federation, DataLoader, and Solving the N+1 Problem

Mastering GraphQL in NestJS with MongoDB: Federation, DataLoader, and Solving the N+1 Problem

GraphQL has changed how modern APIs are built. Instead of fixed REST endpoints, GraphQL allows clients to ask for exactly the data they need — no more and no less. When combined with NestJS and MongoDB, GraphQL becomes extremely powerful, scalable, and production-ready.

In this guide, we will explore advanced GraphQL architecture in NestJS, including:

  • Schema-First vs Code-First
  • Federation
  • Federation Specification
  • Gateway
  • Entities
  • DataLoader
  • Solving the N+1 problem
  • MongoDB integration

This article is designed to be a complete reference for developers who want to build production-ready GraphQL APIs using NestJS.

Schema-First vs Code-First in NestJS

NestJS supports two main approaches:

Schema-First

You manually write your GraphQL schema in .graphql files:

type Product {
  id: ID!
  name: String!
  price: Float!
}

type Query {
  products: [Product!]!
}

You then write resolvers that match this schema.

Code-First

You use TypeScript decorators to build the schema automatically:

import { ObjectType, Field, ID } from '@nestjs/graphql';

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

  @Field()
  name: string;

  @Field()
  price: number;
}

In production apps, Code-First is preferred because:

  • You get type safety
  • IDE autocomplete works perfectly
  • Refactoring is easy

Entities in a GraphQL + MongoDB Application

Entities represent the real data structure of your database.

With MongoDB and Mongoose:

import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type ProductDocument = Product & Document;

@Schema()
export class Product {
  @Prop({ required: true })
  name: string;

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

  @Prop({ default: 0 })
  stock: number;
}

export const ProductSchema = SchemaFactory.createForClass(Product);

In production architecture:

  • The same entity is reused for GraphQL and database logic.
  • This guarantees consistency.
  • One model = one source of truth.

The N+1 Problem in GraphQL

The N+1 problem happens when your server executes too many database queries.

Example query:

query {
  orders {
    id
    products {
      name
      price
    }
  }
}

What actually happens in a bad implementation:

  1. One query loads all orders → 1 query
  2. Each order fetches its products → N queries

Total queries = 1 + N, which is slow and dangerous in production.

How DataLoader Solves the N+1 Problem

DataLoader batches multiple queries into one optimized query per request.

Here’s a production-ready DataLoader example:

import * as DataLoader from 'dataloader';
import { Injectable, Scope } from '@nestjs/common';
import { ProductService } from './product.service';

@Injectable({ scope: Scope.REQUEST })
export class ProductLoader {
  constructor(private productService: ProductService) {}

  loader = new DataLoader(async (ids: readonly string[]) => {
    const products = await this.productService.findManyByIds(ids as string[]);
    return ids.map(
      id => products.find(product => product.id.toString() === id),
    );
  });
}

Why this is production-ready:

  • Uses REQUEST scope so cache resets per request
  • Batches multiple product fetches
  • Prevents memory leaks
  • Improves performance drastically

MongoDB Service Example

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

@Injectable()
export class ProductService {
  constructor(
    @InjectModel(Product.name)
    private readonly productModel: Model<ProductDocument>,
  ) {}

  async findById(id: string) {
    return this.productModel.findById(id).exec();
  }

  async findManyByIds(ids: string[]) {
    return this.productModel.find({ _id: { $in: ids } }).exec();
  }
}

This is written exactly how real production services handle MongoDB in NestJS.

What is GraphQL Federation

GraphQL Federation allows you to split one large schema into multiple sub-schemas, also called subgraphs.

Instead of one huge server:

  • Products service owns product logic
  • Orders service owns order logic
  • Users service owns user logic

They are later combined by a Gateway.

Federation Specification (Core Concepts)

GraphQL Federation introduces special directives:

@key

Used to uniquely identify federated entities.

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';

@ObjectType()
@Directive('@key(fields: "id")')
export class Product {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  price: number;
}

Why it matters:

  • Allows other services to reference this entity
  • Enables stitching of schemas

ResolveReference in Federation

Federated services must be able to resolve references:

import { Resolver, ResolveReference } from '@nestjs/graphql';

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

  @ResolveReference()
  resolveReference(ref: { __typename: string; id: string }) {
    return this.productService.findById(ref.id);
  }
}

This is how different parts of the federated graph talk to each other.

GraphQL Gateway in Production

A Gateway acts as a central brain that combines multiple schemas.

Example:

import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from 'apollo-server';

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'products', url: 'http://localhost:3001/graphql' },
    { name: 'orders', url: 'http://localhost:3002/graphql' },
  ],
});

const server = new ApolloServer({
  gateway,
  subscriptions: false,
});

server.listen({ port: 4000 });

In production:

  • Gateway handles authentication
  • Gateway handles schema composition
  • Gateway routes queries to correct services

Real-World Production Use Cases

This architecture is used in:

  • E-commerce platforms
  • Large SaaS products
  • Banking dashboards
  • Multi-tenant enterprise systems

Common GraphQL features:

  • Fast product catalogs
  • Batched order lookups
  • User dashboards
  • Unified API gateways

Why This Architecture Is Production-Ready

This approach:

  • Is scalable
  • Avoids database overload
  • Allows independent schema evolution
  • Eliminates N+1 problems
  • Uses MongoDB efficiently
  • Works with future microservice architecture (without forcing it)

Senior-Level Best Practices

  • Always scope DataLoader per request
  • Keep GraphQL types and DB entities aligned
  • Use Federation even inside monoliths to future-proof architecture
  • Never ignore N+1 performance problems
  • Always batch database calls

Related Stories



Performance Optimization of NestJS Applications: A Complete Guide

Performance Optimization of NestJS Applications: A Complete Guide

Optimizing the performance of a NestJS application is critical for building scalable, fast, and production-ready APIs. Even though NestJS is a high-performance framework, improper coding practices, unoptimized database queries, and lack of caching can slow down your application.

Bordered avatar

Street Learner

21 Jan 2025

Mastering High-Performance Interceptors in NestJS for Scalable APIs

Mastering High-Performance Interceptors in NestJS for Scalable APIs

NestJS interceptors are one of the most powerful tools in the framework, enabling developers to transform responses, cache results, log performance, and optimize requests. For large-scale applications, building high-performance interceptors is essential to improve speed, maintainability, and scalability.

Bordered avatar

Street Learner

18 Jan 2025