npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pawells/nestjs-graphql

v2.1.1

Published

NestJS GraphQL module with guards, decorators, subscriptions, and DataLoaders

Readme

NestJS GraphQL Module

GitHub Release CI npm version Node License: MIT GitHub Sponsors

Enterprise-grade NestJS GraphQL module with Apollo Server 5.x, Redis caching, DataLoaders, WebSocket subscriptions, query complexity analysis, and comprehensive error handling.

Installation

yarn add @pawells/nestjs-graphql

Optional Dependencies

For authentication (guards, decorators, WebSocket auth):

yarn add @pawells/nestjs-auth

For WebSocket subscriptions:

yarn add ws

Note: Without @pawells/nestjs-auth, all WebSocket connections requiring authentication will be rejected (fail-closed for security).

Requirements

  • Node.js: >= 22.0.0
  • NestJS: >= 10.0.0
  • GraphQL: >= 16.0.0
  • @pawells/nestjs-shared: same version
  • Redis: For caching and pub/sub (optional but recommended)

Peer Dependencies

{
  "@nestjs/apollo": ">=13.0.0",
  "@nestjs/cache-manager": ">=3.0.0",
  "@nestjs/common": ">=10.0.0",
  "@nestjs/config": ">=3.0.0",
  "@nestjs/core": ">=10.0.0",
  "@nestjs/graphql": ">=13.0.0",
  "@nestjs/terminus": ">=10.0.0",
  "@nestjs/throttler": ">=5.0.0",
  "@opentelemetry/api": ">=1.0.0",
  "@pawells/nestjs-auth": "^2.0.1" (optional - see below),
  "cache-manager": ">=7.0.0",
  "class-transformer": ">=0.5.0",
  "class-validator": ">=0.14.0",
  "compression": ">=1.0.0",
  "csrf-csrf": ">=4.0.0",
  "dataloader": ">=2.0.0",
  "express": ">=5.0.0",
  "graphql": ">=16.0.0",
  "graphql-query-complexity": ">=0.12.0",
  "graphql-redis-subscriptions": ">=2.0.0",
  "helmet": ">=7.0.0",
  "ioredis": ">=5.0.0",
  "joi": ">=18.0.0",
  "keyv": ">=5",
  "prom-client": ">=15.0.0",
  "rxjs": ">=7.0.0",
  "uuid": ">=9.0.0",
  "ws": ">=8.0.0" (optional - required for subscriptions),
  "xss": ">=1.0.0"
}

Optional Dependencies Details

The following peer dependencies are optional:

  • @pawells/nestjs-auth: Required only if using authentication features:

    • GraphQLAuthGuard — JWT validation and protected resolvers
    • GraphQLRolesGuard — Role-based access control with @Roles()
    • WebSocketAuthService — JWT validation for WebSocket connections (fails closed without it)
    • Decorators: @Public(), @Roles(), @CurrentUser(), @AuthToken()
    • Without this package, all WebSocket authentication will fail.
  • ws: Required only for WebSocket subscriptions. If omitted, the subscription and WebSocket infrastructure will not be available.

Quick Start

Basic Module Setup

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@pawells/nestjs-graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      playground: true,
      debug: true,
    }),
  ],
})
export class AppModule {}

With Redis Caching

import { Module } from '@nestjs/common';
import { CacheModule, GraphQLModule } from '@pawells/nestjs-graphql';

@Module({
  imports: [
    CacheModule.forRoot(),
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      playground: true,
    }),
  ],
})
export class AppModule {}

Asynchronous Configuration

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@pawells/nestjs-graphql';

@Module({
  imports: [
    ConfigModule.forRoot(),
    GraphQLModule.forRootAsync({
      useFactory: (configService: ConfigService) => ({
        autoSchemaFile: 'schema.gql',
        playground: configService.get('NODE_ENV') !== 'production',
        debug: configService.get('NODE_ENV') === 'development',
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Module Features

Guard Registration Order

When using multiple guards, register them in this order for optimal security and performance:

import { UseGuards } from '@nestjs/common';
import { QueryComplexityGuard, GraphQLAuthGuard, GraphQLRateLimitGuard } from '@pawells/nestjs-graphql';

@UseGuards(
  QueryComplexityGuard,     // Static analysis first (fastest, cheap)
  GraphQLAuthGuard,         // JWT verification second
  GraphQLRateLimitGuard,    // Rate limiting last
)
@Query(() => [Post], { name: 'Posts' })
async posts(): Promise<Post[]> {
  return this.postService.findMany();
}

This order is critical. Incorrect order can degrade performance or bypass authentication:

  • QueryComplexityGuard performs static AST analysis (no I/O) — register first
  • GraphQLAuthGuard validates JWT signatures (moderate cost) — register second
  • GraphQLRateLimitGuard checks Redis for rate limits (I/O required) — register last

BSON Serialization (Optional)

BSON serialization is disabled by default. Enable it only if returning MongoDB BSON types to clients:

import { GraphQLModule } from '@pawells/nestjs-graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      bson: {
        enabled: true,
        maxPayloadSize: 10 * 1024 * 1024, // 10MB default
      },
    }),
  ],
})
export class AppModule {}

When to use:

  • Returning ObjectId fields directly in GraphQL responses
  • Handling large binary payloads from MongoDB
  • Reducing JSON serialization overhead

Client side: Clients must include Accept: application/bson header to receive BSON responses. Without this header, responses are JSON serialized.

Performance Interceptors

The module registers two performance monitoring interceptors that serve complementary purposes:

GraphQLPerformanceInterceptor

Local performance tracking and integration with profiling services (Pyroscope):

@UseInterceptors(GraphQLPerformanceInterceptor)
@Query(() => [Post], { name: 'Posts' })
async posts(): Promise<Post[]> {
  // Execution time is tracked and logged locally
  // Slow queries are logged with a configurable threshold
  return this.postService.findMany();
}

Behavior:

  • Tracks resolver execution time
  • Logs queries exceeding the slow query threshold
  • Integrates with Pyroscope for continuous profiling
  • Enabled by default when @pawells/nestjs-pyroscope is available

GraphQLPerformanceMonitoringInterceptor

Exports metrics to external monitoring services (Prometheus, Datadog):

@UseInterceptors(GraphQLPerformanceMonitoringInterceptor)
@Query(() => [Post], { name: 'Expensive' })
async expensive(): Promise<Post[]> {
  // Metrics are exported to monitoring backends
  return this.postService.expensive();
}

Metrics exported:

  • Request count and latency histograms
  • Error rates per resolver
  • DataLoader batch sizes
  • Query complexity scores
  • Cache hit/miss ratios

Integration:

  • Enabled by default when @pawells/nestjs-prometheus or similar is available
  • Integrates with OpenTelemetry for distributed tracing

Cache Module

The Cache Module provides Redis-backed caching with automatic storage management, TTL support, and metrics tracking.

Basic Usage

import { Injectable } from '@nestjs/common';
import { CacheService } from '@pawells/nestjs-graphql';

@Injectable()
export class UserService {
  constructor(private cacheService: CacheService) {}

  async getUser(id: string) {
    const cached = await this.cacheService.Get(`user:${id}`);
    if (cached) return cached;

    const user = await this.db.user.findUnique({ where: { id } });
    await this.cacheService.Set(`user:${id}`, user, 300000); // 5 minutes

    return user;
  }
}

@Cacheable Decorator

Automatically cache method results with configurable TTL and cache keys:

@Cacheable({ key: 'users:all', ttl: 300000 })
async getAllUsers(): Promise<User[]> {
  return this.db.user.findMany();
}

// Dynamic cache key based on arguments
@Cacheable({
  key: (id: string) => `user:${id}`,
  ttl: 600000,
  condition: (id: string) => id !== 'system', // Skip caching for system user
})
async getUserById(id: string): Promise<User> {
  return this.db.user.findUnique({ where: { id } });
}

@CacheInvalidate Decorator

Invalidate specific cache keys after mutation:

@CacheInvalidate({ keys: 'users:all' })
async createUser(input: CreateUserInput): Promise<User> {
  return this.db.user.create({ data: input });
}

// Multiple keys with dynamic generation
@CacheInvalidate({
  keys: (userId: string) => [
    'users:all',
    `user:${userId}`,
    `user:${userId}:profile`,
  ],
})
async updateUser(userId: string, input: UpdateUserInput): Promise<User> {
  return this.db.user.update({ where: { id: userId }, data: input });
}

@CacheEvict Decorator

Pattern-based cache invalidation (requires Redis):

// Evict all keys matching the pattern
@CacheEvict({ pattern: 'post:*' })
async deleteAllPosts(): Promise<void> {
  return this.db.post.deleteMany();
}

Cache Service API

// Get value from cache
const value = await cacheService.Get('key');

// Set value with TTL (milliseconds)
await cacheService.Set('key', value, 60000); // 1 minute

// Delete specific key or array of keys
await cacheService.Del('key');
await cacheService.Del(['key1', 'key2']);

// Clear all cache
await cacheService.Clear();

// Check whether a key exists without returning its value
const exists = await cacheService.Exists('key');

// Get or set using cache-aside pattern — calls factory only on cache miss
const user = await cacheService.GetOrSet(
  `user:${id}`,
  () => this.db.user.findUnique({ where: { id } }),
  300000, // TTL in milliseconds (optional)
);

// Invalidate all keys matching a glob pattern (requires Redis)
// Returns the number of keys deleted
const removed = await cacheService.InvalidatePattern('user:*');

// Get cache statistics
const stats = cacheService.GetStats();

// Reset all cache statistics counters back to zero
cacheService.ResetStats();

Exists(key: string): Promise<boolean> — Returns true if the key is present in the cache, false otherwise. On Redis errors the method returns false rather than throwing.

GetOrSet<T>(key, factory, ttl?): Promise<T> — Implements the cache-aside pattern. If the key is present the cached value is returned immediately. On a miss, factory() is awaited, the result is stored under key with the optional ttl (milliseconds), and the value is returned. Throws if factory() throws.

InvalidatePattern(pattern: string): Promise<number> — Deletes every key matching pattern using a Redis SCAN-based key lookup. The pattern follows Redis glob syntax (e.g. user:*, session:?, cache:[abc]). Returns the number of keys deleted. Returns 0 if no keys match or if the backing store does not support pattern-based key enumeration. Does not throw on errors — logs them and returns 0.

ResetStats(): void — Resets all statistics counters (hits, misses, sets, deletes, clears, errors, hitRate, evictions, etc.) back to zero and clears the in-memory operation timing buffers.

GraphQL Configuration

Module.forRoot() Options

interface GraphQLConfigOptions {
  // Schema file configuration
  autoSchemaFile?: string | boolean;    // Path or true for temp file
  sortSchema?: boolean;                 // Alphabetically sort schema

  // Server options
  playground?: boolean;                 // GraphQL Playground UI
  introspection?: boolean;              // Allow introspection queries
  debug?: boolean;                      // Enable Apollo debug mode

  // Custom context and CORS
  context?: (ctx) => object;
  cors?: CorsOptions | boolean;

  // Error handling
  formatError?: (error) => object;
  errorHandling?: {
    includeStackTrace?: boolean;        // Dev-only stack traces
    errorCodes?: Record<string, string>; // Custom error codes
  };

  // Query complexity limits
  maxQueryComplexity?: number;

  // BSON serialization
  bson?: {
    enabled?: boolean;
    maxPayloadSize?: number;            // Default: 10MB
  };
}

Custom Scalars

ObjectId Scalar

For MongoDB ObjectId support:

import { ObjectType, Field, ObjectId as GraphQLObjectId } from '@pawells/nestjs-graphql';

@ObjectType()
export class Post {
  @Field(() => ObjectIdScalar)
  _id: ObjectId;

  @Field()
  title: string;
}

Query with ObjectId:

@Query(() => Post, { name: 'GetPost' })
async getPost(
  @Args('id', { type: () => ObjectIdScalar }) id: ObjectId
): Promise<Post> {
  return this.postService.findById(id);
}

DateTime Scalar

ISO 8601 DateTime strings (automatically used by NestJS):

import { DateTimeScalar } from '@pawells/nestjs-graphql';

@ObjectType()
export class User {
  @Field()
  id: string;

  @Field()
  name: string;

  @Field(() => DateTimeScalar)
  createdAt: Date;

  @Field(() => DateTimeScalar)
  updatedAt: Date;
}

JSON Scalar

For arbitrary JSON objects:

import { JSONScalar } from '@pawells/nestjs-graphql';

@ObjectType()
export class Post {
  @Field()
  id: string;

  @Field(() => JSONScalar)
  metadata: Record<string, any>;
}

Cursor-Based Pagination

Relay-style cursor-based pagination with PageInfo:

import { Connection, Edge, PageInfo, CursorUtils } from '@pawells/nestjs-graphql';

@ObjectType()
export class UserConnection {
  @Field(() => [UserEdge])
  edges: UserEdge[];

  @Field()
  pageInfo: PageInfo;

  @Field()
  totalCount: number;
}

@ObjectType()
export class UserEdge {
  @Field()
  cursor: string;

  @Field(() => User)
  node: User;
}

// Usage in resolver
@Query(() => UserConnection, { name: 'GetUsers' })
async getUsers(
  @Args('first', { type: () => Int, nullable: true }) first?: number,
  @Args('after', { nullable: true }) after?: string,
): Promise<UserConnection> {
  const users = await this.userService.findMany();

  const { edges, pageInfo } = this.graphQLService.paginateItems(
    users,
    first,
    after,
  );

  return {
    edges: edges.map((edge) => ({
      cursor: edge.cursor,
      node: edge.node,
    })),
    pageInfo,
    totalCount: users.length,
  };
}

// Manual cursor operations
const cursor = CursorUtils.encodeCursor('user:123', Date.now());
const decoded = CursorUtils.decodeCursor(cursor); // { id: 'user:123', timestamp: ... }

DataLoaders

Prevent N+1 queries with batch loading:

import { Injectable } from '@nestjs/common';
import { DataLoaderRegistry } from '@pawells/nestjs-graphql';

@Injectable()
export class UserLoader {
  constructor(
    private dataloaderRegistry: DataLoaderRegistry,
    private userService: UserService,
  ) {}

  // Use in request scope
  loadUser(userId: string) {
    return this.dataloaderRegistry.GetOrCreate(
      'users',
      {
        batchLoadFn: async (userIds: readonly string[]) => {
          const users = await this.userService.findByIds(userIds);
          // Return users in same order as userIds
          return userIds.map(id => users.find(u => u.id === id));
        },
      },
    ).load(userId);
  }
}

// Usage in resolver
@Resolver(() => Post)
export class PostResolver {
  @ResolveField(() => User)
  async author(@Parent() post: Post) {
    return this.userLoader.loadUser(post.authorId);
  }
}

Query Complexity Analysis

Prevent expensive queries that could impact performance:

import { QueryComplexityGuard } from '@pawells/nestjs-graphql';

@UseGuards(QueryComplexityGuard)
@Query(() => [Post], { name: 'Posts' })
async posts(
  @Args('limit', { type: () => Int, nullable: true }) limit?: number,
): Promise<Post[]> {
  // Query complexity is calculated and validated
  return this.postService.findMany();
}

Configure limits in module setup:

GraphQLModule.forRoot({
  // ... other options
  maxQueryComplexity: 500, // Complexity score limit
})

WebSocket Subscriptions

Real-time subscriptions with WebSocket support:

Basic Setup

import { Resolver, Subscription, Mutation, Args } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';

const pubSub = new PubSub();

@Resolver()
export class NotificationResolver {
  @Subscription(() => String, { name: 'NotificationAdded' })
  notificationAdded() {
    return pubSub.asyncIterator('notification.added');
  }

  @Mutation(() => String, { name: 'SendNotification' })
  async sendNotification(@Args('message') message: string) {
    pubSub.publish('notification.added', { notificationAdded: message });
    return message;
  }
}

With Redis PubSub (Distributed)

For multi-instance deployments:

import { Module } from '@nestjs/common';
import { Redis } from 'ioredis';
import { RedisPubSub } from 'graphql-redis-subscriptions';

@Module({
  providers: [
    {
      provide: 'PUB_SUB',
      useFactory: () => {
        return new RedisPubSub({
          connection: {
            host: 'localhost',
            port: 6379,
          },
        });
      },
    },
  ],
  exports: ['PUB_SUB'],
})
export class SubscriptionModule {}

Authentication with WebSocket

WebSocket connections require JwtService for signature verification (fails closed for security):

// Client side - send token in connection params
const client = new WebSocketLink({
  uri: 'ws://localhost:3000/graphql/subscriptions',
  connectionParams: () => ({
    authorization: `Bearer ${token}`,
  }),
});

// Server side - WebSocketAuthService validates token cryptographically
// If JwtService is unavailable, all WebSocket auth fails (fail-closed)

WebSocket Server Configuration

Configure subscriptions in GraphQL module:

import { GraphQLWebSocketServer } from '@pawells/nestjs-graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      // ... other options
    }),
  ],
  providers: [GraphQLWebSocketServer],
})
export class AppModule implements OnApplicationBootstrap {
  constructor(private wsServer: GraphQLWebSocketServer) {}

  onApplicationBootstrap() {
    this.wsServer.configure({
      path: '/graphql/subscriptions',
      maxPayloadSize: 102400,  // 100KB
      keepalive: 30000,        // 30 seconds
      connectionTimeout: 60000, // 60 seconds
      maxConnections: 1000,
    });
  }
}

Note: GraphQLWebSocketServer uses onApplicationBootstrap — not onModuleInit. The WebSocket server must be configured after all modules have fully initialized and the Apollo schema has been built. Calling configure() during onModuleInit risks running before the schema is ready, which causes the server to silently skip initialization.

Error Handling

GraphQLErrorFormatter

Structured error responses with standardized codes:

import { GraphQLErrorCode, GraphQLErrorFormatter } from '@pawells/nestjs-graphql';

// Errors are automatically formatted by the module
// Clients receive consistent error structures:
{
  "errors": [{
    "message": "User not found",
    "extensions": {
      "code": "NOT_FOUND",
      "timestamp": "2024-03-14T12:00:00.000Z",
      "details": {}
    }
  }]
}

Standard Error Codes

enum GraphQLErrorCode {
  // Authentication & Authorization
  UNAUTHENTICATED = 'UNAUTHENTICATED',
  FORBIDDEN = 'FORBIDDEN',

  // Input Validation
  BAD_USER_INPUT = 'BAD_USER_INPUT',
  VALIDATION_ERROR = 'VALIDATION_ERROR',

  // Business Logic
  CONFLICT = 'CONFLICT',
  NOT_FOUND = 'NOT_FOUND',

  // Rate Limiting
  RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',

  // System Errors
  INTERNAL_ERROR = 'INTERNAL_ERROR',
  SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
}

Validation Error Handling

Class-validator integration provides structured validation errors:

import { IsEmail, IsNotEmpty } from 'class-validator';
import { InputType, Field } from '@nestjs/graphql';

@InputType()
export class CreateUserInput {
  @Field()
  @IsNotEmpty()
  name: string;

  @Field()
  @IsEmail()
  email: string;
}

// Validation errors are formatted as:
{
  "errors": [{
    "message": "Validation failed",
    "extensions": {
      "code": "BAD_USER_INPUT",
      "validationErrors": [
        {
          "field": "email",
          "constraints": { "isEmail": "email must be an email" }
        }
      ]
    }
  }]
}

Guards

Note: GraphQLAuthGuard and GraphQLRolesGuard require @pawells/nestjs-auth to be installed and imported as a module. Without it, these guards will not function.

GraphQLAuthGuard

Requires authentication for resolvers (requires @pawells/nestjs-auth):

import { UseGuards } from '@nestjs/common';
import { GraphQLAuthGuard } from '@pawells/nestjs-graphql';

@UseGuards(GraphQLAuthGuard)
@Query(() => String, { name: 'GetCurrentUser' })
async getCurrentUser(): Promise<string> {
  // Only authenticated users can access
  return 'user-data';
}

IMPORTANT: When combining multiple guards, see the Guard Registration Order section above. The mandatory order is: QueryComplexityGuardGraphQLAuthGuardGraphQLRateLimitGuard. Incorrect order can bypass authentication entirely.

GraphQLPublicGuard

Marks resolvers as publicly accessible:

import { GraphQLPublic } from '@pawells/nestjs-auth';

@GraphQLPublic()
@Query(() => String, { name: 'Health' })
async health(): Promise<string> {
  return 'OK';
}

GraphQLRolesGuard

Role-based access control (requires @pawells/nestjs-auth):

import { UseGuards } from '@nestjs/common';
import { Roles } from '@pawells/nestjs-auth';
import { GraphQLRolesGuard } from '@pawells/nestjs-graphql';

@UseGuards(GraphQLRolesGuard)
@Roles('admin', 'moderator')
@Query(() => [User], { name: 'AllUsers' })
async allUsers(): Promise<User[]> {
  // Only admin or moderator roles can access
  return this.userService.findMany();
}

QueryComplexityGuard

Enforces query complexity limits:

import { UseGuards } from '@nestjs/common';
import { QueryComplexityGuard } from '@pawells/nestjs-graphql';

@UseGuards(QueryComplexityGuard)
@Query(() => [Post], { name: 'ExpensiveQuery' })
async expensiveQuery(): Promise<Post[]> {
  // Query complexity is validated before execution
  return this.postService.findExpensive();
}

IMPORTANT: When combining multiple guards, see the Guard Registration Order section above. The mandatory order is: QueryComplexityGuardGraphQLAuthGuardGraphQLRateLimitGuard. Incorrect order can bypass authentication entirely.

GraphQLRateLimitGuard

Rate limiting per user or IP:

import { UseGuards } from '@nestjs/common';
import { GraphQLRateLimitGuard } from '@pawells/nestjs-graphql';

@UseGuards(GraphQLRateLimitGuard)
@Mutation(() => String, { name: 'CreatePost' })
async createPost(@Args() input: CreatePostInput): Promise<string> {
  // Rate limited to default: 100 requests per 15 minutes
  return this.postService.create(input);
}

IMPORTANT: When combining multiple guards, see the Guard Registration Order section above. The mandatory order is: QueryComplexityGuardGraphQLAuthGuardGraphQLRateLimitGuard. Incorrect order can bypass authentication entirely.

Configure custom limits:

import { RateLimitService } from '@pawells/nestjs-graphql';

@Injectable()
export class AppService implements OnModuleInit {
  constructor(private rateLimitService: RateLimitService) {}

  onModuleInit() {
    // 50 requests per minute for mutations
    this.rateLimitService.SetOperationConfig('mutation', {
      maxRequests: 50,
      windowMs: 60000, // 1 minute
    });
  }
}

Interceptors

GraphQLLoggingInterceptor

Automatic GraphQL operation logging:

@Injectable()
@UseInterceptors(GraphQLLoggingInterceptor)
export class PostResolver {
  @Query(() => [Post], { name: 'Posts' })
  async posts(): Promise<Post[]> {
    // Automatically logged with operation name, arguments, and execution time
    return this.postService.findMany();
  }
}

GraphQLErrorInterceptor

Consistent error formatting and logging:

@UseInterceptors(GraphQLErrorInterceptor)
@Query(() => String, { name: 'RiskyOperation' })
async riskyOperation(): Promise<string> {
  // Errors are automatically formatted and logged
  return this.service.risky();
}

GraphQLPerformanceInterceptor

Track and report resolver performance:

@UseInterceptors(GraphQLPerformanceInterceptor)
@Query(() => [Post], { name: 'SlowQuery' })
async slowQuery(): Promise<Post[]> {
  // Execution time is tracked and logged
  return this.postService.slowQuery();
}

Context Handling

Access GraphQL context with typed decorators (re-exported from @pawells/nestjs-auth for convenience):

import { GraphQLContextParam, GraphQLCurrentUser } from '@pawells/nestjs-graphql';

@Query(() => User, { name: 'Me' })
async me(
  @GraphQLContextParam() context: any,
  @GraphQLCurrentUser() user: User,
): Promise<User> {
  // context contains req, res, and other GraphQL context
  // user is extracted from context.req.user
  return user;
}

Note: GraphQLContextParam and GraphQLCurrentUser are re-exported from @pawells/nestjs-auth for convenience. See the auth package documentation for additional GraphQL decorator variants.

BSON Serialization

Opt-in BSON support for binary serialization:

GraphQLModule.forRoot({
  autoSchemaFile: 'schema.gql',
  bson: {
    enabled: true,
    maxPayloadSize: 10485760, // 10MB
  },
})

Benefits:

  • Binary serialization reduces payload size
  • More efficient for large datasets
  • Direct MongoDB BSON compatibility

Performance Monitoring

Metrics Collection

The module provides built-in metrics for:

  • Cache hit/miss rates
  • Query complexity scores
  • Resolver execution times
  • Rate limit statistics
  • DataLoader batch sizes

Access metrics programmatically:

import { CacheService, RateLimitService } from '@pawells/nestjs-graphql';

@Injectable()
export class MetricsService {
  constructor(
    private cacheService: CacheService,
    private rateLimitService: RateLimitService,
  ) {}

  async getMetrics() {
    const cacheStats = this.cacheService.GetStats();
    const rateLimitStats = this.rateLimitService.GetStats();

    return {
      cache: cacheStats,
      rateLimit: rateLimitStats,
    };
  }
}

Security

Default Behaviors

  • WebSocket Auth: JwtService required for signature verification — fails closed
  • Query Complexity: Enforced by default to prevent DoS attacks
  • Token Blacklist: Tokens treated as blacklisted when cache unavailable
  • CORS: Strict localhost matching to prevent subdomain bypass
  • Rate Limiting: Default 100 requests per 15 minutes per client

Best Practices

  1. Always enable authentication for production GraphQL endpoints
  2. Use Redis for caching in distributed deployments
  3. Set realistic query complexity limits for your schema
  4. Configure BSON if handling large payloads
  5. Monitor rate limit metrics for abuse patterns
  6. Enable introspection in development only

Configuration Reference

Environment Variables

# Redis connection
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=optional
REDIS_DB=0

# GraphQL
GRAPHQL_PLAYGROUND=true
GRAPHQL_DEBUG=true
GRAPHQL_INTROSPECTION=true

# Node
NODE_ENV=development

Complete Example

See examples/ directory for a complete working application.

Troubleshooting

Cache not working

  • Ensure Redis is running and accessible
  • Check REDIS_HOST and REDIS_PORT environment variables
  • Verify cache keys are deterministic

WebSocket subscriptions not connecting

  • Check /graphql/subscriptions path is accessible
  • Verify JWT token is valid (if auth enabled)
  • Check ws:// protocol in client connection
  • Verify JwtService is available (required for auth)

Query complexity errors

  • Increase maxQueryComplexity if legitimate queries fail
  • Check query structure for nested selections
  • Use DataLoaders to batch load related data

Memory issues

  • Reduce maxBatchSize in DataLoader options
  • Implement cache key strategy to avoid unbounded growth
  • Monitor cache hit rates and adjust TTLs

Related Packages

License

MIT