A full-stack real-time weather application with WebSocket-powered live updates and topic-based pub/sub messaging.
Browser ──→ Nginx (static files + reverse proxy)
│
├── /api/* ──→ Express (REST API)
└── /socket.io/* ──→ Socket.IO (WebSocket)
│
├── Topic-based Pub/Sub
└── Weather Polling ──→ OpenWeatherMap API
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Tailwind CSS, Vite |
| Backend | Express 5, TypeScript, Socket.IO, JWT |
| Realtime | Socket.IO with topic-based pub/sub |
| Weather | OpenWeatherMap API (free tier) |
| Deploy | Docker, Docker Compose, Nginx |
- User Authentication — JWT-based login with protected routes
- City Weather Query — Real-time weather data from OpenWeatherMap (temperature, humidity, wind speed, icon)
- Live Weather Updates — Backend polls weather every 60s, pushes changes via WebSocket
- Topic-based Pub/Sub — Extensible messaging system with topic validation and subscription management
- Real-time Message Push — Toast notifications delivered to subscribed clients
- Socket Authentication — JWT verification on WebSocket connections
- Health Check —
GET /api/v1/healthendpoint for monitoring - Docker Support — One-command startup with
docker-compose
# 1. Clone the repository
git clone <repo-url> && cd NuraSpaceCode-Challenge
# 2. Create server/.env
cp server/.env.example server/.env
# Edit server/.env and add your OpenWeatherMap API key
# 3. Start everything
docker-compose up --build
# 4. Open http://localhost:3000# Backend
cd server
npm install
cp .env.example .env # Add your API key
npm run dev # Starts on port 3001
# Frontend (in another terminal)
cd client
npm install
npm run dev # Starts on port 5173 with proxy to 3001| Variable | Description | Required |
|---|---|---|
OPENWEATHER_API_KEY |
OpenWeatherMap API key | Yes |
JWT_SECRET |
Secret for signing JWT tokens | Yes |
PORT |
Server port (default: 3001) | No |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/auth/login |
User login | No |
| GET | /api/v1/weather/:city |
Get weather data | Yes |
| POST | /api/v1/messages |
Push message to topic | No |
| GET | /api/v1/health |
Health check | No |
| Event | Direction | Payload | Description |
|---|---|---|---|
subscribe |
Client → Server | ['city:Sydney'] |
Subscribe to topics |
message |
Server → Client | { topic, message, timestamp } |
Live message notification |
weather:update |
Server → Client | { city, temp, humidity, ... } |
Real-time weather update |
├── docker-compose.yml
├── client/ # React frontend
│ ├── Dockerfile
│ ├── nginx.conf # Production reverse proxy
│ └── src/
│ ├── pages/
│ │ ├── Login/ # Login page
│ │ └── Home/ # Weather dashboard
│ │ ├── components/ # CitySelector, WeatherCard, Toast
│ │ └── hooks/ # useSocket (WebSocket hook)
│ ├── services/ # API client, Socket.IO client
│ └── context/ # Auth state management
├── server/ # Express backend
│ ├── Dockerfile
│ └── src/
│ ├── routes/v1/ # Versioned API routes
│ ├── controllers/ # Request handlers
│ ├── services/ # Weather API, Weather Poller
│ ├── infrastructure/ # Socket.IO setup (pub/sub + auth)
│ ├── middleware/ # JWT auth middleware
│ └── data/ # In-memory user store
The app automatically pushes weather updates to clients when data changes, without users having to refresh. This is implemented via server-side polling (every 60s) + WebSocket push.
Why not use a push-based weather API?
OpenWeatherMap (and most weather APIs) don't offer WebSocket or webhook-based real-time feeds on their free tier. Services like Xweather or WeatherFlow do provide streaming APIs, but they are paid, proprietary, and would tightly couple the app to a specific vendor.
Server-side polling is the standard approach because:
- Vendor-agnostic — The poller wraps any HTTP-based weather API. Switching providers requires changing one service file, not the entire real-time architecture.
- Efficient — Only polls cities with active subscribers. No subscribers = no API calls. Change detection (
hasChanged) ensures we only push when data actually differs, avoiding unnecessary WebSocket traffic. - Scalable — In production, the polling interval and concurrency can be tuned. The same pattern works with Redis pub/sub for multi-instance deployments.
The messaging layer uses a topic-based pub/sub model (city:Sydney, city:Melbourne) instead of raw Socket.IO rooms.
- Extensible — Adding new topic types (e.g.
alert:storm,region:oceania) requires only adding a prefix to the whitelist, no structural changes. - Secure — Topic validation (prefix whitelist, max count, max length) prevents abuse. Raw rooms accept any string.
- Observable — Dual-map tracking (
topicSubscribers+connTopics) makes it easy to inspect who is listening to what, and ensures clean disconnection handling.
| Username | Password |
|---|---|
| admin | admin |
| user | 123456 |