DEV Community

Cover image for WebSocket Implementation with Next.js (Node.js + React in One App)

WebSocket Implementation with Next.js (Node.js + React in One App)

“A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.” : Leslie Lamport

Real-time communication is a core requirement for modern applications - chat systems, live dashboards, notifications, and collaborative tools all depend on it.

Many developers assume they need separate backend and frontend services to implement WebSockets. That’s not true.
In this article, we’ll build a fully working WebSocket system inside a single Next.js application, using:

  • A custom Node.js server
  • The ws WebSocket library
  • A clean, event-based messaging protocol

We’ll fully break down the server implementation and explain how a React frontend consumes it - without turning the article into a UI tutorial.

Why WebSockets (and Not API Polling)?
HTTP-based polling:

  • Wastes bandwidth
  • Introduces latency
  • Scales poorly

WebSockets:

  • Keep a persistent connection
  • Support bi-directional communication
  • Allow the server to push updates instantly

If your app needs live updates, WebSockets are the correct tool. Anything else is a workaround.

Why Use WebSockets Inside Next.js?
Because:

  • You already have a Node runtime
  • You can serve UI and real-time logic together
  • You avoid managing multiple repos and deployments

Important constraint (we’ll be honest about it):
This approach requires a Node-based deployment (Docker, VM, Railway, Render).
It does not work on serverless platforms like Vercel.

Index

  1. Architecture Overview
  2. Why a Custom Server Is Required
  3. Setting Up the Custom Next.js Server
  4. Designing the WebSocket Message Protocol
  5. Tracking Connected Users
  6. Creating the HTTP Server
  7. Creating the WebSocket Server
  8. Handling HTTP → WebSocket Upgrade
  9. Broadcasting Messages
  10. Managing User Presence
  11. Handling Client Connections
  12. Error Handling and Cleanup
  13. How the Frontend Uses This Server
  14. Why This Architecture Makes Sense
  15. Watch Out For
  16. Interesting Facts
  17. Next Steps You Can Take
  18. FAQ
  19. Conclusion

1. Architecture Overview

Browser (React / Next.js)
   ↕ WebSocket (/ws)
Custom Next.js Server (Node.js)
   ├─ Next.js request handler
   ├─ WebSocket upgrade handler
   └─ WebSocket message broker
Enter fullscreen mode Exit fullscreen mode

One process. One app. Real-time enabled.

2. Why a Custom Server Is Required

Next.js API routes are serverless by default:

  • No persistent process
  • No long-lived connections
  • WebSockets break immediately

To use WebSockets, we must:

  • Run Next.js inside a custom Node HTTP server
  • Manually handle WebSocket upgrades

This is not a hack - it’s the officially supported approach for advanced use cases.

3. Setting Up the Custom Server

import { createServer } from 'http';
import next from 'next';
import { WebSocketServer, WebSocket } from 'ws';
import { parse } from 'url';
Enter fullscreen mode Exit fullscreen mode

What’s happening here:

  • http → creates the base Node server
  • next → boots Next.js manually
  • ws → low-level WebSocket implementation
  • url → used to inspect upgrade requests
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
Enter fullscreen mode Exit fullscreen mode

This initializes Next.js exactly as it would run internally, but gives us control over the server lifecycle.

4. Defining the Message Protocol

interface ChatMessage {
  type: 'message' | 'join' | 'leave' | 'userList';
  username?: string;
  content?: string;
  users?: string[];
  timestamp: number;
}
Enter fullscreen mode Exit fullscreen mode

This is critical.

Instead of sending raw strings, we use a typed message protocol:

  • Predictable
  • Extensible
  • Easy to validate
  • Frontend-friendly

Every message has:

  • A type
  • A timestamp
  • Optional payload fields

This is how real systems are built.

5. Tracking Connected Users

const users = new Map<WebSocket, string>();
Enter fullscreen mode Exit fullscreen mode

Why a Map?

  • Each WebSocket connection is unique
  • We can associate metadata (username) with it
  • Easy cleanup on disconnect

This structure represents in-memory presence tracking.

6. Creating the HTTP Server

const server = createServer((req, res) => {
  handle(req, res);
});
Enter fullscreen mode Exit fullscreen mode

All normal HTTP requests:

  • Pages
  • Assets
  • API routes

Are still handled by Next.js.

7. Creating the WebSocket Server (Correctly)

const wss = new WebSocketServer({ noServer: true });
Enter fullscreen mode Exit fullscreen mode

This is an important design decision.
Why noServer: true?

  • We don’t want to hijack all connections
  • Next.js itself uses WebSockets (HMR)
  • We want full control over which requests upgrade

“Simplicity is hard work, but it pays off.” : Rich Hickey

8. Handling the WebSocket Upgrade Manually

server.on('upgrade', (request, socket, head) => {
  const { pathname } = parse(request.url || '');

  if (pathname === '/ws') {
    wss.handleUpgrade(request, socket, head, (ws) => {
      wss.emit('connection', ws, request);
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Intercepts upgrade requests
  • Accepts only /ws
  • Leaves everything else untouched

This prevents breaking:

  • Next.js Hot Module Reloading
  • Other internal WebSocket usage
  • This is a production-grade pattern.

9. Broadcasting Messages

function broadcast(message: ChatMessage) {
  const data = JSON.stringify(message);
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

This turns the server into a real-time event hub:

  • One message in
  • Many clients updated instantly

10. Broadcasting User Presence

function broadcastUserList() {
  const userList = Array.from(users.values());
  broadcast({
    type: 'userList',
    users: userList,
    timestamp: Date.now(),
  });
}
Enter fullscreen mode Exit fullscreen mode

Presence is treated as state, not messages.
This keeps the frontend simple and consistent.

11. Handling Client Connections

wss.on('connection', (socket) => {
  socket.on('message', ...)
  socket.on('close', ...)
});
Enter fullscreen mode Exit fullscreen mode

Each connection:

  • Can join
  • Can send message
  • Will trigger leave on disconnect

This is the core event loop of the system.

Join Event

  • Assign username
  • Store in Map
  • Notify everyone
  • Broadcast updated user list

Message Event

  • Validate sender
  • Broadcast to all clients

Leave Event

  • Remove from Map
  • Notify users
  • Re-sync presence list

This is clean, explicit, and debuggable.

12. Error Handling and Cleanup

socket.on('error', ...)
socket.on('close', ...)
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • Prevents memory leaks
  • Ensures stale connections don’t linger
  • Keeps presence data accurate

Real-time systems fail silently if you ignore this.

13. How the Frontend Uses This Server (Conceptual)

The frontend does not need to know server internals.
It only needs to:

  1. Open a WebSocket connection to /ws
  2. Send a join message once connected
  3. Listen for incoming messages
  4. Update UI based on type

Typical Flow

  • User opens page
  • Browser connects to ws(s)://your-domain/ws

Frontend sends:

 { "type": "join", "username": "Ankit" }
Enter fullscreen mode Exit fullscreen mode
  • Server broadcasts join + user list
  • Messages flow both ways in real time

The UI is just a consumer of events.

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” : Martin Fowler

14. Why This Architecture Makes Sense

  • One deployment
  • One codebase
  • Explicit protocol
  • Clear separation of concerns
  • Easy to extend with auth, rooms, or persistence

This is how real chat systems start.

15. Watch Out For

  • Serverless platforms
  • No reconnection logic on client
  • No message validation
  • No scaling strategy (Redis needed for multi-instance)

WebSockets are powerful - but unforgiving.

16. Interesting Facts

17. Next Steps You Can Take

  • Add authentication (JWT on join)
  • Implement rooms or channels
  • Persist messages to DB
  • Add Redis Pub/Sub for scaling
  • Introduce rate limiting

18. FAQ

1. Can this WebSocket setup run on Vercel?
No. Vercel’s serverless functions do not support long-lived connections. This setup requires a persistent Node.js process running on a VM or container-based platform.

2. Do I need a separate backend for WebSockets?
Not necessarily. As shown in this article, WebSockets can live inside the same Next.js application using a custom server.

3. Is this approach production-ready?
Yes. This architecture is production-safe when deployed on a Node-based platform and enhanced with proper authentication, validation, and scaling strategies.

4. How do I scale this to multiple servers?
You need a shared pub/sub layer (Redis, NATS, Kafka). Without it, each server instance will only broadcast to its own connected clients.

5. Should I use Socket.IO instead of ws?
Socket.IO adds reconnection, fallback transports, and rooms - but also extra overhead.
If you want maximum control and performance, ws is the better choice.

6. How do I secure WebSocket connections?

  • Use wss:// behind HTTPS
  • Authenticate users during the join event
  • Validate every incoming message
  • Apply rate limiting to prevent abuse

7. Can this be used for more than chat?
Absolutely. The same architecture works for:

  • Live notifications
  • Activity feeds
  • Collaborative editing
  • Real-time dashboards
  • Multiplayer game state sync

19. Conclusion

You can build a clean, scalable WebSocket system inside a single Next.js application - if you respect the constraints and design it properly.
By using a custom Node server, explicit upgrade handling, and a structured message protocol, you get:

  • Real-time performance
  • Predictable behavior
  • Production-ready architecture

If your app needs live updates, this approach is not a workaround - it’s the right tool.

About the Author: Ankit is a full-stack developer at AddWebSolution and AI enthusiast who crafts intelligent web solutions with PHP, Laravel, and modern frontend tools.

Top comments (0)