NestJS — Enterprise Node.js

GraphQL Subscriptions & Advanced Patterns

16 min Lesson 34 of 48

GraphQL Subscriptions & Advanced Patterns

This lesson covers four production-grade GraphQL capabilities: real-time data via @Subscription and PubSub, resolving nested relationships with @ResolveField, eliminating the notorious N+1 problem with DataLoader, and returning structured errors through GraphQL error handling. Together they take a basic API to enterprise quality.

Real-Time with @Subscription and PubSub

Subscriptions push data to clients over a persistent WebSocket connection. NestJS wires them via @Subscription() on a resolver method and a PubSub instance that acts as the in-process event bus. For a single-server deployment the built-in PubSub from graphql-subscriptions is sufficient; in a multi-node environment replace it with a Redis-backed pub/sub.

// posts.resolver.ts import { Resolver, Mutation, Subscription, Args } from '@nestjs/graphql'; import { PubSub } from 'graphql-subscriptions'; import { Post } from './post.model'; import { CreatePostInput } from './dto/create-post.input'; const pubSub = new PubSub(); const POST_ADDED_EVENT = 'postAdded'; @Resolver(() => Post) export class PostsResolver { @Mutation(() => Post) async createPost(@Args('input') input: CreatePostInput): Promise<Post> { const post = await this.postsService.create(input); await pubSub.publish(POST_ADDED_EVENT, { postAdded: post }); return post; } @Subscription(() => Post) postAdded() { return pubSub.asyncIterator(POST_ADDED_EVENT); } }
WebSocket transport required. Subscriptions need installSubscriptionHandlers: true (Apollo Server 2) or the subscriptions-transport-ws / graphql-ws plugin configured on the GraphQLModule. HTTP alone cannot push data to clients.

Resolving Nested Types with @ResolveField

@ResolveField() teaches NestJS how to populate a field whose value is not directly on the parent entity — for example, fetching an author object whenever a Post is returned. The method receives the parent object as its first argument (injected with @Parent()) so it can issue a targeted lookup:

import { Resolver, ResolveField, Parent } from '@nestjs/graphql'; import { Post } from './post.model'; import { User } from '../users/user.model'; @Resolver(() => Post) export class PostsResolver { @ResolveField(() => User) async author(@Parent() post: Post): Promise<User> { return this.usersService.findOne(post.authorId); } }

The N+1 Problem and DataLoader

The N+1 problem occurs when fetching a list of N posts each triggers a separate findOne query for the author, resulting in 1 list query + N author queries. DataLoader solves this by batching all author IDs collected during a single GraphQL execution tick into one query, then distributing results back to each resolver.

  • Install: npm install dataloader
  • Create a loader that accepts an array of IDs and returns an array of entities in the same order.
  • Scope the loader to the request (use a REQUEST-scoped provider or a NestJS DataLoaderInterceptor) so batches are per-request, not global.
import DataLoader from 'dataloader'; import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) export class UserLoader { private loader = new DataLoader<number, User>(async (ids) => { const users = await this.usersService.findByIds(ids as number[]); // return results in the SAME ORDER as ids return ids.map(id => users.find(u => u.id === id) ?? new Error(`User ${id} not found`)); }); load(id: number): Promise<User> { return this.loader.load(id); } } // In PostsResolver — swap direct service call for the loader: @ResolveField(() => User) async author(@Parent() post: Post): Promise<User> { return this.userLoader.load(post.authorId); // batched automatically }
Always use REQUEST scope for DataLoader. A singleton loader would leak batched data across unrelated requests and cache stale results for the lifetime of the process.

GraphQL Error Handling

GraphQL errors should be meaningful and typed. NestJS propagates NestJS exceptions (e.g. NotFoundException) to the errors array in the response, but you can also throw GraphQLError directly for richer client-facing error codes:

  • Throw NestJS exceptions — automatically translated to GraphQL errors with the message preserved.
  • Custom error codes — use extensions.code on GraphQLError so clients can distinguish error types programmatically.
  • Format errors globally — configure formatError on GraphQLModule to strip stack traces in production.
Never expose internal stack traces or database error details in production GraphQL responses. Use formatError to return only a safe message and a machine-readable code. Log full details server-side.

Summary

@Subscription + PubSub delivers real-time events over WebSockets; swap in Redis PubSub for multi-node deployments. @ResolveField populates nested types cleanly. DataLoader (REQUEST-scoped) batches nested queries and eliminates the N+1 problem. Structured error handling with formatError protects clients from leaking internals while providing actionable codes. These four patterns are the foundation of production-ready NestJS GraphQL APIs.