Modern trading platforms like Groww or Kite handle thousands of price updates per second.
To make the UI smooth, fast, and scalable, they combine real-time streaming (via NATS) and REST APIs (for static data).
Let’s walk through how this architecture works, why NATS.js shines here, and how to handle market hours vs post-market hours seamlessly on the frontend.
🧠 1. Where REST and NATS Are Used
A trading app needs both static and live data. Each page uses them differently.
| Page | Data via NATS (Live) | Data via REST (Static / Snapshot) |
|---|---|---|
| Nifty 50 / Top Gainers / Losers | Real-time prices, percentage change | Company names, symbols, sectors |
| Holdings / Positions | LTP (Last Traded Price), live PnL | Stock metadata, historical cost |
| Stock Detail Page | Price ticks, volume, OHLC updates | Company info, news, fundamentals |
| Dashboard | Portfolio value refresh (live PnL) | Profile info, investment summary |
| User Profile | — | Purely REST |
So —
🟢 NATS → live market data
🔵 REST → static or slowly changing data
⚙️ 2. Using NATS.js in the Frontend
NATS.js is an official JavaScript client for the NATS messaging system — a high-performance publish/subscribe platform built for cloud apps.
✨ Why NATS.js Shines Here
-
Subject-based Pub/Sub: You can subscribe to fine-grained topics like
stocks.NIFTY.NSororders.user123.status. - Lightweight: Binary encoding, minimal overhead — ideal for streaming prices at millisecond frequency.
- Automatic Reconnects: Recovers gracefully from disconnections.
- Load balancing & clustering: Distributes updates efficiently across thousands of clients.
- Streaming + Queue Groups: Handles fan-out for large-scale market tick distribution.
📦 Installing NATS.js
npm install nats.ws
🔌 Basic Connection Example
import { connect } from 'nats.ws';
const nc = await connect({ servers: 'wss://pulse.tradingapp.com' });
const sub = nc.subscribe('stocks.*');
for await (const m of sub) {
const { symbol, price } = JSON.parse(m.data);
updateStock(symbol, price);
}
🏗️ 3. State Management — A Generic Store Design
Instead of keeping fixed slices like nifty50, topGainers, or holdings, a generic symbol-based store is more scalable.
Example (Zustand-like):
import { create } from "zustand";
const useStockStore = create((set, get) => ({
stocks: {}, // symbol → details
updateStocks: (updates) =>
set((state) => {
const newStocks = { ...state.stocks };
updates.forEach((u) => {
newStocks[u.symbol] = { ...newStocks[u.symbol], ...u };
});
return { stocks: newStocks };
}),
}));
Then whenever new data arrives:
pulse.on("priceUpdate", (data) => {
useStockStore.getState().updateStocks(data);
});
This design allows bulk updates — even if 100 stocks are subscribed (like on a holdings page), updates are applied efficiently with a single re-render.
🔄 4. How Data Flows on the UI (Static + Live)
Let’s see how different pages use both REST and NATS in harmony 👇
🔹 Nifty50 / Top Gainers Page
- REST → Initial stock list + metadata
- NATS → Continuous tick updates (LTP, change%)
- Bulk updates applied to store
// REST snapshot
const snapshot = await fetch('/api/stocks/nifty50').then(r => r.json());
useStockStore.setState({ stocks: snapshot });
// Live updates
pulse.subscribe('stocks.nifty50.*', handleUpdates);
🔹 Stock Details Page
- REST → Company name, description, news, financials
- NATS → Real-time price chart and volume updates Both layers merge at the UI level — static info from REST, live info from NATS.
🔹 Holdings / Portfolio Page
- REST → Your bought quantities, avg cost
- NATS → Live LTP → Recalculate PnL dynamically
Example:
const pnl = (ltp - avgCost) * qty;
🔹 Dashboard
- REST → Total invested value
- NATS → Current value → Instant portfolio refresh
🕓 5. After Market Hours — Switching to REST
After 3:30 PM, the exchanges stop sending live ticks.
The app automatically switches from NATS to REST mode.
| Time | Data Source | Behavior |
|---|---|---|
| During Market Hours | NATS | Live ticks, streaming updates |
| After Market Close | REST | EOD snapshots, no new subscriptions |
Implementation idea:
if (marketStatus === "OPEN") {
pulse.subscribe("stocks.*");
} else {
pulse.unsubscribe("stocks.*");
fetch("/api/eod-snapshot").then(updateStocks);
}
This hybrid design saves bandwidth and keeps the UI consistent — users still see final market prices even when data feed stops.
⚡ Why Not Just WebSockets?
| Feature | WebSocket | NATS.js |
|---|---|---|
| Pub/Sub Routing | Manual | Built-in subjects |
| Clustering | Custom infra | Native |
| Reconnect Handling | Manual | Automatic |
| Binary Support | Yes | Yes (optimized) |
| Fan-out Efficiency | Poor at scale | Excellent |
| Server-to-Server bridge | Not native | Built-in JetStream |
In short, NATS is WebSocket++ — faster, scalable, and designed for real-time microservices, not just peer connections.
🧩 6. Bringing It All Together
Here’s what happens under the hood:
┌──────────────┐
│ Exchange Feed│
└──────┬───────┘
│
(via NATS)
│
┌────────────────────┐
│ Trading App Backend│
└────────────────────┘
│
┌─────────────┐
│ NATS.js │ <— Subscribes to live ticks
└─────────────┘
│
┌──────────────┐
│ Zustand Store│ <— Bulk updates by symbol
└──────────────┘
│
┌──────────────┐
│ React UI │ <— Auto re-renders only affected stocks
└──────────────┘
💡 Key Takeaways
- REST + NATS = perfect hybrid for trading apps
- NATS.js gives reliable real-time streaming with lightweight reconnects
- Generic key–value store (symbol → data) handles high-frequency updates efficiently
- Bulk update pattern ensures smooth UI even with 100+ stocks
- Fallback to REST keeps app stable after market close
🔚 Final Thoughts
Frontend performance in trading apps isn’t just about fancy charts —
it’s about data orchestration: balancing live vs static updates, avoiding over-renders, and using protocols designed for scale.
So the next time you build a dashboard or live feed —
think beyond WebSocket, think NATS 🚀
Top comments (0)