Building a Realtime Chat App with NestJs, GraphQL Subscriptions, and WebSockets: A Practical Guide
October 6, 2026 · 5 min read

Realtime communication is a fundamental feature in modern applications, especially chat apps where updates need to flow instantly between users. NestJs provides a powerful backend framework for building scalable server-side apps, and when combined with GraphQL subscriptions over WebSockets, it enables elegant realtime capabilities. This article walks you through the process of creating a realtime chat backend with NestJs, GraphQL subscriptions, and WebSockets, covering key implementation details, common pitfalls, and best practices.
Why GraphQL Subscriptions with NestJs?
WebSocket-based subscriptions allow clients to maintain a persistent connection to a GraphQL server and receive push notifications when data changes, perfect for chat and notifications. NestJs's integration with Apollo and its modular architecture simplifies wiring up GraphQL subscriptions alongside regular queries and mutations.
By the end of this guide, you'll understand how to:
- Set up WebSocket transport in a NestJs GraphQL server
- Define subscription resolvers and triggers
- Handle PubSub mechanisms securely and scalably
- Troubleshoot common WebSocket pitfalls
Setting up the NestJs Project with GraphQL
First, initialize a NestJs app if you haven't already:
npm i -g @nestjs/cli
nest new chat-backend
Install GraphQL and related packages:
npm install --save @nestjs/graphql graphql apollo-server-express graphql-subscriptions
Additionally, for WebSocket support ensure subscriptions-transport-ws (for Apollo v2) or use Apollo Server 3+ which has built-in support. Here, we use the Apollo driver bundled with NestJs.
Modify app.module.ts to set up GraphQL with subscriptions enabled:
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { ChatModule } from './chat/chat.module';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
installSubscriptionHandlers: true, // Critical for subscriptions
playground: true,
}),
ChatModule,
],
})
export class AppModule {}
Designing the Chat Schema with Subscriptions
Use the GraphQL schema-first or code-first approach (NestJs supports both), here is a simple chat schema:
type Message {
id: ID!
content: String!
sender: String!
timestamp: String!
}
type Query {
messages: [Message!]!
}
type Mutation {
sendMessage(content: String!, sender: String!): Message!
}
type Subscription {
messageSent: Message!
}
The messageSent subscription emits new messages whenever they are sent.
Implementing PubSub with graphql-subscriptions
GraphQL subscriptions rely on a PubSub mechanism. For prototyping and small scale, the in-memory PubSub from graphql-subscriptions suffices:
npm install graphql-subscriptions
Create a shared service:
import { Injectable } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
@Injectable()
export class PubSubService {
public pubSub = new PubSub();
}
This service is injected into your chat resolver to publish and subscribe to events.
Writing the Chat Resolver
In chat.resolver.ts:
import { Resolver, Query, Mutation, Args, Subscription } from '@nestjs/graphql';
import { PubSubService } from '../pubsub/pubsub.service';
import { Message } from './message.model';
import { UseFilters } from '@nestjs/common';
const MESSAGE_SENT_EVENT = 'messageSent';
@Resolver(() => Message)
export class ChatResolver {
private messages: Message[] = [];
constructor(private readonly pubSubService: PubSubService) {}
@Query(() => [Message])
messages() {
return this.messages;
}
@Mutation(() => Message)
sendMessage(
@Args('content') content: string,
@Args('sender') sender: string,
) {
const message: Message = {
id: Date.now().toString(),
content,
sender,
timestamp: new Date().toISOString(),
};
this.messages.push(message);
this.pubSubService.pubSub.publish(MESSAGE_SENT_EVENT, { messageSent: message });
return message;
}
@Subscription(() => Message, {
resolve: (value) => value,
})
messageSent() {
return this.pubSubService.pubSub.asyncIterator(MESSAGE_SENT_EVENT);
}
}
Common Pitfalls and How to Avoid Them
installSubscriptionHandlersmissing: Make sure this option is true inforRoot()to enable WebSocket subscriptions.- PubSub usage: The in-memory PubSub isn't suitable for distributed systems because it doesn't share events across multiple instances.
- WebSocket connection issues: If clients can't receive subscription data, verify network proxies or firewalls aren't blocking WebSocket traffic.
- Subscription filtering: Subscriptions can be filtered to only send relevant data to clients. Without filtering, all subscribers may get irrelevant messages causing unnecessary load.
For filtering example:
@Subscription(() => Message, {
filter: (payload, variables) => {
return payload.messageSent.sender === variables.sender;
},
})
messageSent(@Args('sender') sender: string) {
return this.pubSubService.pubSub.asyncIterator(MESSAGE_SENT_EVENT);
}
Scaling Subscriptions
When scaling horizontally (multiple app instances), the in-memory PubSub causes message events to be local per instance, resulting in inconsistent subscription events. Consider using:
- Redis-based PubSub implementations (
graphql-redis-subscriptions) - Message queues (RabbitMQ, Kafka) for event propagation
Example with Redis PubSub:
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const options = {
host: 'localhost',
port: 6379,
};
const pubSub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
});
Integrate this PubSub in your NestJs service for scaled deployments.
Testing Your Subscriptions
Use GraphQL Playground (enabled in our setup) or tools like Insomnia and Altair to:
- Run a query subscription:
subscription {
messageSent {
id
content
sender
}
}
- Send mutations:
mutation {
sendMessage(content: "Hello", sender: "Alice") {
id
content
sender
}
}
Ensure subscription clients receive real-time data push.
Security Considerations
- Authentication: Authenticate WebSocket connections using context or
onConnecthandlers. Avoid including sensitive tokens in subscription payloads. - Authorization: Implement schema-level guards or middleware to restrict subscription access.
Example of passing auth token during connection:
GraphQLModule.forRoot({
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
onConnect: (connectionParams) => {
if (!connectionParams.authToken) {
throw new Error('Missing auth token!');
}
return { user: getUserFromToken(connectionParams.authToken) };
},
},
},
});
Access authenticated user in resolvers via context.
Key Takeaways
- NestJs simplifies GraphQL and WebSocket integration, making realtime subscription features accessible.
- Enable
installSubscriptionHandlersand use a PubSub system for pushing events. - The built-in in-memory PubSub is fine for demos/small apps but replace with Redis or other systems for production scaling.
- Test carefully with appropriate clients and handle authentication/authorization for subscription connections.
- Applying filters on subscriptions can optimize network bandwidth and user experience.
Building a chat backend with realtime subscriptions in NestJs equips you with a powerful pattern for other realtime features. With attention to scaling and security, you can extend this foundation to robust production-grade applications.