DEV Community

Cover image for Ditch REST: How React + SQL + WebSockets Can Power Real-Time Apps Without the Overhead
Yevhen Kozachenko πŸ‡ΊπŸ‡¦
Yevhen Kozachenko πŸ‡ΊπŸ‡¦

Posted on • Originally published at ekwoster.dev

Ditch REST: How React + SQL + WebSockets Can Power Real-Time Apps Without the Overhead

Ditch REST: How React + SQL + WebSockets Can Power Real-Time Apps Without the Overhead

Tired of polling? Hate Redux overload? Want snappy real-time updates without diving into Firebase lock-in? Here's a bold new way to build full-duplex apps with SQL + React + WebSockets that actually scales.

🚨 Why REST Doesn’t Cut It Anymore

We’ve been building web apps the β€œRESTful” way for the last two decades: client requests -> server responds. While it works for a lot of CRUD-style apps, the internet is changing. Take a look at:

  • Real-time collaboration (e.g. Google Docs)
  • Multiplayer experiences (e.g. Figma, Trello)
  • Dashboards that auto-update (e.g. Trading apps)

Polling every few seconds is a waste of resources and bad UX. You might consider Firebase, Hasura, or GraphQL subscriptions β€” but what if you could get real-time capabilities with tools you already know?

Enter React + SQL + WebSockets.


🧠 The Stack

We'll build a basic chat app (yes, revolutionary πŸ™ƒ) but the idea scales:

  • Frontend: React with Hooks and WebSocket connection
  • Backend: Node.js with Express and WebSocket server
  • Database: PostgreSQL (because SQL still rocks)
  • Real-Time Sync: PostgreSQL triggers + WebSocket pushing delta

πŸ› οΈ Pre-requisites

Before diving in, make sure you’ve got:

  • Node.js β‰₯ 18
  • PostgreSQL β‰₯ 12
  • Basic knowledge of React
  • Familiarity with Express

πŸ§ͺ Step 1 – PostgreSQL Setup with Trigger

Let’s enable PostgreSQL to notify our backend when new data comes in.

-- Create messages table
CREATE TABLE messages (
  id SERIAL PRIMARY KEY,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Function to notify
CREATE OR REPLACE FUNCTION notify_new_message()
RETURNS trigger AS $$
DECLARE
BEGIN
  PERFORM pg_notify('new_message', row_to_json(NEW)::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Trigger
CREATE TRIGGER trigger_new_message
AFTER INSERT ON messages
FOR EACH ROW EXECUTE FUNCTION notify_new_message();
Enter fullscreen mode Exit fullscreen mode

Now, every time you INSERT a message, PostgreSQL sends a NOTIFY event.


πŸ‘‚ Step 2 – Node.js WebSocket Listening on PostgreSQL

Let’s write the backend that pipes SQL NOTIFY messages to WebSocket clients.

const express = require('express');
const { Pool } = require('pg');
const WebSocket = require('ws');
const http = require('http');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

const pgPool = new Pool({
  connectionString: process.env.DATABASE_URL // or use config
});

(async () => {
  const client = await pgPool.connect();
  await client.query('LISTEN new_message');

  client.on('notification', msg => {
    const payload = JSON.parse(msg.payload);
    console.log('πŸ’¬ New message:', payload);

    wss.clients.forEach(ws => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(payload));
      }
    });
  });
})();

app.use(express.json());
app.post('/messages', async (req, res) => {
  const { content } = req.body;
  const { rows } = await pgPool.query(
    'INSERT INTO messages (content) VALUES ($1) RETURNING *',
    [content]
  );
  res.json(rows[0]);
});

server.listen(3001, () => console.log('πŸš€ Server listening on port 3001'));
Enter fullscreen mode Exit fullscreen mode

Now the server acts like a bridge from PostgreSQL -> WebSocket.


βš›οΈ Step 3 – React Hook for WebSocket Subscription

On the React side, we set up a WebSocket hook to connect to the backend and render messages.

import React, { useEffect, useRef, useState } from 'react';

function useWebSocketMessages(url) {
  const [messages, setMessages] = useState([]);
  const ws = useRef(null);

  useEffect(() => {
    ws.current = new WebSocket(url);

    ws.current.onmessage = (e) => {
      const data = JSON.parse(e.data);
      setMessages((prev) => [...prev, data]);
    };

    return () => ws.current.close();
  }, [url]);

  return messages;
}

function Chat() {
  const messages = useWebSocketMessages('ws://localhost:3001');

  return (
    <div>
      <h2>Real-time Chat</h2>
      <ul>
        {messages.map((msg, idx) => (
          <li key={idx}>{msg.content}</li>
        ))}
      </ul>
    </div>
  );
}

export default Chat;
Enter fullscreen mode Exit fullscreen mode

🧬 Architectural Benefits

This architecture is surprisingly powerful:

  • πŸ”₯ Realtime β€” updates as they happen, no polling
  • πŸͺ React hooks fit naturally with WebSocket streams
  • πŸ“¦ Lightweight backend β€” no need for GraphQL or Firebase
  • 🧠 Declarative updates from SQL without Redux
  • πŸ” Full control over data & auth

⏭️ Where to Go From Here?

This pattern isn’t limited to a chat app. You can extend it to:

  • Real-time stock tickers
  • IoT sensor visualization
  • Collaborative whiteboards
  • Multiplayer boards (Kanban, Trello clones)

Bonus: Add authentication, debounce updates, or event sourcing with PostgreSQL logical replication.


🏁 Conclusion

Imagine a future where:

  • Your server speaks SQL + WS natively
  • Your client only fetches once
  • You skip Redux, polling, and stale data

With a small upgrade to your architecture, you can get real-time SQL in the browser β€” and you didn’t even need Firebase.

REST is slow. GraphQL is heavy. WebSockets + SQL is underrated.

Try it.


πŸ”— Resources:


🧠 If you enjoyed this article, follow the blog for deeper real-time explorations, async patterns, and minimalist web architectures that scale.

Happy hacking πŸ––


πŸ‘‰ If you need this done – we offer fullstack-development services.

Top comments (0)