@pawells/nestjs-graphql
v2.1.1
Published
NestJS GraphQL module with guards, decorators, subscriptions, and DataLoaders
Maintainers
Readme
NestJS GraphQL Module
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-graphqlOptional Dependencies
For authentication (guards, decorators, WebSocket auth):
yarn add @pawells/nestjs-authFor WebSocket subscriptions:
yarn add wsNote: 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 resolversGraphQLRolesGuard— 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:
QueryComplexityGuardperforms static AST analysis (no I/O) — register firstGraphQLAuthGuardvalidates JWT signatures (moderate cost) — register secondGraphQLRateLimitGuardchecks 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
ObjectIdfields 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-pyroscopeis 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-prometheusor 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:
GraphQLWebSocketServerusesonApplicationBootstrap— notonModuleInit. The WebSocket server must be configured after all modules have fully initialized and the Apollo schema has been built. Callingconfigure()duringonModuleInitrisks 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:
GraphQLAuthGuardandGraphQLRolesGuardrequire@pawells/nestjs-authto 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:
QueryComplexityGuard→GraphQLAuthGuard→GraphQLRateLimitGuard. 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:
QueryComplexityGuard→GraphQLAuthGuard→GraphQLRateLimitGuard. 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:
QueryComplexityGuard→GraphQLAuthGuard→GraphQLRateLimitGuard. 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
- Always enable authentication for production GraphQL endpoints
- Use Redis for caching in distributed deployments
- Set realistic query complexity limits for your schema
- Configure BSON if handling large payloads
- Monitor rate limit metrics for abuse patterns
- 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=developmentComplete Example
See examples/ directory for a complete working application.
Troubleshooting
Cache not working
- Ensure Redis is running and accessible
- Check
REDIS_HOSTandREDIS_PORTenvironment variables - Verify cache keys are deterministic
WebSocket subscriptions not connecting
- Check
/graphql/subscriptionspath is accessible - Verify JWT token is valid (if auth enabled)
- Check
ws://protocol in client connection - Verify
JwtServiceis available (required for auth)
Query complexity errors
- Increase
maxQueryComplexityif legitimate queries fail - Check query structure for nested selections
- Use DataLoaders to batch load related data
Memory issues
- Reduce
maxBatchSizein DataLoader options - Implement cache key strategy to avoid unbounded growth
- Monitor cache hit rates and adjust TTLs
Related Packages
- @pawells/nestjs-shared - Foundation library with guards, filters, and utilities
- @pawells/nestjs-auth - Keycloak integration: token validation, guards, decorators, Admin API client
- @pawells/nestjs-open-telemetry - Distributed tracing
- @pawells/nestjs-prometheus - Prometheus metrics
- @pawells/nestjs-pyroscope - Continuous profiling
License
MIT
