In this project, I demonstrate how to build a scalable, maintainable chat application using NestJS with a Clean-Architecture mindset. The goal is to show how Nest’s module system, built-in dependency injection, and clear separation of concerns let you compose robust real-time features with GraphQL and WebSockets.
- Real-time messaging
Users can send and receive text messages in real time via GraphQL subscriptions over WebSockets (using the graphql-ws package). - GraphQL API
I chose GraphQL over REST because:- Single endpoint: simplifies client-server communication.
- Type safety: strong typing of queries/mutations which will make sure fewer runtime errors.
- Subscriptions: first-class support for real-time data push.
- Authentication & Authorization
Users sign in via a local (email/password) strategy, receive a JWT in an HTTP-only cookie, and subsequent requests (including GraphQL and WebSocket connections) are guarded by JWT-based guards.
I follow a four-layer Clean Architecture:
- Domain Layer
- Entities (
User
,Chat
,Message
,JwtPayload
)
- Entities (
- Application Layer (Use Cases)
- Services (
UsersService
,ChatsService
,MessagesService
,AuthService
) encapsulate business rules and logic (e.g. hashing passwords, publishing events, composing MongoDB aggregation pipelines are a few to mention).
- Services (
- Interface Adapters
- Resolvers (GraphQL) and Controllers (REST endpoints for file upload, auth functionality and count APIs).
- Guards (
JwtAuthGuard
,GqlAuthGuard
,LocalAuthGuard
) and Passport Strategies (JwtStrategy
,LocalStrategy
) translate framework inputs into calls to the Application layer to authenticate and authorize the user to make the right API call.
- Infrastructure Layer
- Nest Modules (
UsersModule
,ChatsModule
,MessagesModule
,AuthModule
) wire everything together. - Repositories (
UserRepository
,ChatRepository
) extend an abstract MongoDB repository; Mongoose Schemas (UserDocument
,ChatDocument
,MessageDocument
) define persistence models. - Third-party services: AWS S3 for profile image storage (
S3Service
), in-memory PubSub for GraphQL subscriptions, and Nest’sJwtModule
/ConfigService
for secrets management.
- Nest Modules (
This separation makes sure that the core logic remains framework-agnostic and testable, while NestJS simply wires the pieces using dependency injection.
Chat Module
User Module
Auth Module
---
A directed acyclic graph (DAG) is a conceptual representation of a series of activities, as per https://hazelcast.com/foundations/distributed-computing/directed-acyclic-graph/
In Nest.js architecture, modules typically are designed in hierarchical structure. This is system works
in layers, ensuring that each module and its components get their dependencies from the closest
injector, be it module-specific or global (Feature or root module).
Nest’s module system ensures that each module is a single unit of responsibility. As modules become dependent on one another, they form a directed acyclic graph (DAG) that paints a clear picture of the application’s architecture.
It is a visualization aids in the following aspects:
-
Problem diagnosis: Easily identifying which modules might be affected when a single module encounters an issue.
-
Optimized refactoring: Recognizing which modules can be independently refactored without disturbing the application’s overall functionality.
3.Enhanced scalability: Strategically adding new modules or expanding existing ones based on the current module graph.
Note: This diagram was created using madge with the help of GraphViz
It is the situation where two classes, services or modules depend on each other. So they call each other at once.
In this particular app, i have came across the circular dependency between Chats and Messages modules where they depend on each other in object initialization.
In order to solve this issue, Nest.js provides the solution to this issue using Forward reference as per the Nest.js Docs
I utilized this solution in my code:
in Chats Module :
@Module({
imports: [
UsersModule,
MongooseModule.forFeature([
{
name: ChatDocument.name,
schema: ChatSchema,
},
]),
forwardRef(() => MessagesModule),
],
providers: [ChatsResolver, ChatsService, ChatRepository],
exports: [ChatRepository, ChatsService],
controllers: [ChatsController],
})
export class ChatsModule {}
in Messages Module :
@Module({
imports: [forwardRef(() => ChatsModule), PubSubModule, UsersModule],
providers: [MessagesResolver, MessagesService],
controllers: [MessagesController],
})
export class MessagesModule {}
-
MongoDB / Mongoose
- AbstractEntity: base schema with
_id: ObjectId
andcreatedAt
/updatedAt
fromAbstractEntity
. - UserDocument → persisted user data (email, hashed password) +
User
domain entity returned to clients. - ChatDocument → each chat has a
userId
owner, aname
, and an array of embeddedMessageDocument
subdocuments. - MessageDocument → stores
content
,sender
(ObjectId), andcreatedAt
; transformed into aMessage
GraphQL object type on the way out.
- AbstractEntity: base schema with
-
GraphQL Types & Inputs
- Object Types (
User
,Chat
,Message
) mirror the domain entities. - Input Types (
CreateUserInput
,UpdateUserInput
,CreateChatInput
,UpdateChatInput
,CreateMessageInput
) enforce validation viaclass-validator
. - Args Types (
PaginationArgs
,GetMessagesArgs
,MessageCreatedArgs
) support cursor-style pagination and subscription filters.
- Object Types (
Backend
- Framework: NestJS v11 (modules, DI, pipes, guards, interceptors)
- API:
- Apollo GraphQL (
@nestjs/graphql
,@apollo/server
) - WebSocket Subscriptions via
graphql-ws
&graphql-subscriptions
- Apollo GraphQL (
- Database: MongoDB with Mongoose (
@nestjs/mongoose
) - Auth: Passport.js strategies (
local
,jwt
), JWT in HTTP-only cookies - File Storage: AWS S3 (
@aws-sdk/client-s3
) for user profile images - Logging: Pino (
nestjs-pino
) - Validation:
class-validator
&joi
for request payloads - Migrations:
migrate-mongo
- Testing: Jest, Supertest
Frontend
-
Framework: React v19, Vite
-
GraphQL Client: Apollo Client (@apollo/client)
-
Real-time: graphql-ws for subscriptions over WebSockets
-
Styling: MUI (Material UI), styled-components
-
Routing: React Router v7
-
State & Caching: Apollo’s in-memory cache, localForage for offline caching
-
Tooling: ESLint, Prettier, GraphQL Codegen
# key NPM scripts to run the server:
npm run start:dev # start in watch mode
npm run build # compile to dist/
npm run test # run unit tests
npm run test:e2e # run end-to-end tests
# Key NPM scripts to run the React APP
npm run dev # start Vite development server
npm run build # production build
npm run codegen # generate GraphQL types/hooks