Connecting NextJS with NestJS via SocketIO

Introduction

SocketIO is a powerful library that enables bidirectional, real-time, event-based communication between the server and the browser.

The key advantages include:

  • Low Latency: Instant data transmission instead of having to send continuous requests.
  • Reliability: Automatic reconnection on connection loss and fallback support to HTTP long-polling if WebSocket is unavailable.
  • Broadcasting: Easily send data to one or multiple clients simultaneously.

In this article, I will guide you on using NextJS and NestJS to communicate with each other via SocketIO, simulating a chatbot app and the AI Agent's workflow.

Detail

On NestJS project you need to install these packages:

yarn add @nestjs/websockets @nestjs/platform-socket.io socket.io


Create the controller/conversations.controller.ts file returning the conversation list as follows:

import { Controller, Get } from "@nestjs/common";

@Controller("conversations")
export class ConversationsController {
@Get()
getConversations() {
return [
{
id: "1",
name: "NestJS System",
lastMsg: "Socket.io is ready",
time: "10:00",
online: true,
},
{
id: "2",
name: "React Squad",
lastMsg: "New components added",
time: "Yesterday",
online: false,
},
];
}
}


Create the socket/websocket.gateway.ts file:

import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
import { Server } from "socket.io";

@WebSocketGateway({
cors: { origin: "*" },
path: "/socket-io-ai",
})
export class ChatGateway {
@WebSocketServer()
server!: Server;

@SubscribeMessage("sendMessage")
async handleMessage(@MessageBody() data: { sender: string; text: string }) {
const steps = ["🔍 Analyzing user intent...", "📚 Querying internal knowledge base...", "🤖 Running inference with Llama-3...", "✨ Synthesizing final response..."];

for (const step of steps) {
await this.delay(600);
this.server.emit("ai_processing", { step });
}

const responseTemplates = [
`I've analyzed your message: "${data.text}". Based on my processing, everything looks optimal.`,
`Regarding "${data.text}", I've cross-referenced our database and found the relevant context for you.`,
`Processing "${data.text}" complete. I've utilized specialized logic to give you this insight.`,
`Interesting point about "${data.text}". My neural network suggests focusing on the core parameters mentioned.`,
`Task finished! I've successfully routed "${data.text}" through the NestJS Gateway and synthesized this result.`,
];

const randomIndex = Math.floor(Math.random() * responseTemplates.length);
const finalResponse = responseTemplates[randomIndex];

this.server.emit("ai_response", {
id: `ai-${Date.now()}`,
role: "ai",
content: finalResponse,
});
}

private delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

The code above initializes a WebSocket Gateway in NestJS, serving as an intermediary server. When receiving the "sendMessage" event, the server simulates the AI's Chain of Thought steps by sending consecutive "ai_processing" events with a delay, then returns the final result via the "ai_response" event.


Update the app.module.ts file:

import { Module } from "@nestjs/common";
import { ConversationsController } from "./controller/conversations.controller";
import { ChatGateway } from "./socket/websocket.gateway";

@Module({
controllers: [ConversationsController],
providers: [ChatGateway],
})
export class AppModule {}


On NextJS project you need to install the package:

yarn add socket.io-client


Create the app/chatbot/page.tsx file:

"use client";

import { LoadingOutlined, RobotOutlined, SendOutlined, UserOutlined } from "@ant-design/icons";
import { Avatar, Button, Input, Spin, Tag } from "antd";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";

const NEST_BACKEND_URL = process.env.NEXT_PUBLIC_SOCKET_URL;

interface Message {
id: string;
role: "user" | "ai";
content: string;
steps?: string[];
}

export default function ChatPage() {
const [conversations, setConversations] = useState([]);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(false);
const [currentStep, setCurrentStep] = useState("");

const tempSteps = useRef<string[]>([]);
const socketRef = useRef<Socket | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);

useEffect(() => {
fetch(`${NEST_BACKEND_URL}/conversations`)
.then(res => res.json())
.then(setConversations)
.catch(err => console.error("Fetch error:", err));

socketRef.current = io(NEST_BACKEND_URL, {
path: "/socket-io-ai",
transports: ["websocket"],
});

socketRef.current.on("ai_processing", (data: { step: string }) => {
setIsTyping(true);
setCurrentStep(data.step);
tempSteps.current.push(data.step);
});

socketRef.current.on("ai_response", (data: Message) => {
const finalMsg: Message = {
...data,
steps: [...tempSteps.current],
};
setMessages(prev => [...prev, finalMsg]);
setIsTyping(false);
setCurrentStep("");
tempSteps.current = [];
});

return () => {
socketRef.current?.disconnect();
};
}, []);

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}
}, [messages, isTyping, currentStep]);

const handleSend = () => {
if (!inputValue.trim()) return;
const userMsg: Message = {
id: Date.now().toString(),
role: "user",
content: inputValue,
};
setMessages(prev => [...prev, userMsg]);
socketRef.current?.emit("sendMessage", { sender: "User", text: inputValue });
setInputValue("");
tempSteps.current = [];
};

return (
<div className="flex h-screen bg-slate-50 p-4 gap-4 overflow-hidden font-sans text-slate-900">
<div className="w-80 bg-white rounded-2xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
<div className="p-5 font-bold text-lg border-b border-slate-100 text-blue-600 bg-white uppercase tracking-wider">Conversations</div>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{conversations.map((item: any) => (
<div key={item.id} className="p-4 hover:bg-blue-50 cursor-pointer border-b border-slate-50 flex gap-3 items-center group transition-colors">
<div className="flex-shrink-0">
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.id}`} size={45} className="border border-slate-100" />
</div>
<div className="flex-1 overflow-hidden">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-slate-700 truncate">{item.name}</span>
<span className="text-[10px] text-slate-400 font-medium">{item.time}</span>
</div>
<div className="text-xs text-slate-500 truncate group-hover:text-blue-500 transition-colors">{item.lastMsg}</div>
</div>
</div>
))}
</div>
</div>

<div className="flex-1 bg-white rounded-2xl shadow-md border border-slate-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-white/80 backdrop-blur-sm z-10">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<Avatar icon={<RobotOutlined />} className="bg-blue-600 shadow-sm" size={40} />
</div>
<div>
<div className="font-bold text-slate-800 leading-tight italic">AI Agentic Node</div>
<div className="text-[10px] text-blue-500 font-bold uppercase tracking-widest">Processing Online</div>
</div>
</div>
<Tag color="blue" className="rounded-full px-3 border-none bg-blue-50 text-blue-600 font-medium uppercase text-[10px]">
Socket.io Stream
</Tag>
</div>

<div ref={scrollRef} className="flex-1 overflow-y-auto p-6 space-y-8 bg-slate-50/50">
{messages.map(msg => (
<div key={msg.id} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"} animate-in fade-in duration-300`}>
<div className={`flex max-w-[85%] gap-3 ${msg.role === "user" ? "flex-row-reverse" : "flex-row"}`}>
<div className="flex-shrink-0 self-end mb-1">
<Avatar icon={msg.role === "user" ? <UserOutlined /> : <RobotOutlined />} className={`${msg.role === "user" ? "bg-slate-800" : "bg-blue-600"} shadow-sm`} size={32} />
</div>
<div className="flex flex-col gap-4">
{msg.role === "ai" && msg.steps && msg.steps.length > 0 && (
<div className="flex flex-col gap-2 ml-1">
<div className="flex items-center gap-2 opacity-60">
<div className="h-[1px] w-4 bg-slate-300"></div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Chain of Thought</span>
</div>
<div className="flex flex-col gap-1.5 border-l-2 border-slate-100 ml-1 pl-4">
{msg.steps.map((step, idx) => (
<div key={idx} className="flex items-center gap-3 py-0.5 group">
<div className="w-1.5 h-1.5 rounded-full bg-blue-400/50 group-last:bg-blue-500 shadow-[0_0_8px_rgba(96,165,250,0.5)]"></div>
<span className="text-[12px] text-slate-500 italic font-medium leading-none tracking-tight">{step}</span>
</div>
))}
</div>
</div>
)}

<div
className={`p-4 rounded-2xl shadow-sm text-[14.5px] leading-relaxed tracking-tight ${
msg.role === "user" ? "bg-blue-600 text-white rounded-br-none shadow-blue-100" : "bg-white text-slate-700 border border-slate-100 rounded-tl-none font-medium"
}`}
>
{msg.content}
</div>
</div>
</div>
</div>
))}

{isTyping && (
<div className="flex justify-start gap-3 pl-11">
<div className="bg-white/80 backdrop-blur-sm px-4 py-2 rounded-2xl border border-blue-100 flex items-center gap-3 shadow-sm ring-4 ring-blue-50/50">
<span className="text-[12px] text-blue-600 font-bold italic tracking-wide animate-pulse">{currentStep || "Analyzing..."}</span>
<Spin indicator={<LoadingOutlined className="text-blue-600 text-sm" spin />} />
</div>
</div>
)}
</div>

<div className="p-5 bg-white border-t border-slate-100">
<div className="flex gap-2 p-1.5 border border-slate-200 rounded-2xl bg-slate-50 focus-within:bg-white focus-within:border-blue-500 focus-within:ring-4 focus-within:ring-blue-100/50 transition-all shadow-inner">
<Input
variant="borderless"
placeholder="Ask AI anything..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onPressEnter={handleSend}
disabled={isTyping}
className="py-2 px-4 text-slate-700 placeholder:text-slate-400"
/>
<Button
type="primary"
size="large"
shape="circle"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim() || isTyping}
className="flex-shrink-0 shadow-lg shadow-blue-200 flex items-center justify-center bg-blue-600 hover:scale-105 active:scale-95 transition-transform"
/>
</div>
</div>
</div>
</div>
);
}

This is the user interface (Client) using socket.io-client to listen for events from NestJS. When "ai_processing" is received, it displays the processing status and stores the steps in a temporary array. When "ai_response" is received, it updates the full content and the list of thought steps into the chat window for the user to follow.


Update the .env file; you can change the value if your server is at a different address:

NEXT_PUBLIC_SOCKET_URL = http://localhost:4000


The result is as follows:


You can see that the socket connection has been successfully created.

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Sitemap

NodeJS Practice Series