I built this project because I wanted a clean, practical way to show how a sports app can feel live without turning the browser into a data-fetching liability.
That’s the core tension with real-time sports experiences: users expect scores and events to update instantly, but frontend code should never carry private API credentials. So the shape of the solution became clear pretty quickly. I needed a browser UI, a Node.js relay, and a sports data source that could feed live updates into the relay. That’s where Sportmicro fit naturally.
The result is a small but complete demo: a React client connects to a local WebSocket server, the server talks to Sportmicro on the backend, and the browser gets normalized snapshots of live matches and event-style updates. The UI never needs to know anything about the upstream authentication layer. If you want to inspect the code, View the repository.
The problem I was solving
I wanted the demo to answer a very specific question: how do you build a sports WebSocket API experience that feels real-time while keeping the architecture safe and understandable?
That immediately ruled out the simplest approach of calling a sports API directly from the browser. It might be convenient, but it puts secrets in the wrong place. Instead, I built the app around a backend relay pattern. The browser only opens a WebSocket to my local server. The server uses SPORTMICRO_API_KEY to fetch data from Sportmicro’s documented endpoints and then forwards the result back to the UI.
That architecture is small enough to understand in one sitting, but it still mirrors how I’d structure a real-time sports dashboard in production.
The architecture in plain English
There are three moving parts:
- React client — handles the UI, sport selection, and connection state.
- Node.js WebSocket server — accepts browser connections and relays data.
- Sportmicro — provides the sports data from documented REST endpoints.
The client and server communicate with a tiny message schema. The browser sends a subscribe message when a sport is selected. The server responds with connection, snapshot, or error messages. That keeps the flow predictable and makes the UI logic straightforward.
The supported sports in this demo are:
- football
- basketball
- tennis
In the repository, that shows up in the shared types and the sport option lists. I like this kind of setup because it keeps the interface honest: the UI only exposes what the backend and the data model actually support.
Why the relay pattern mattered
This is one of those cases where the architecture is the feature.
A browser app can absolutely open WebSockets, but it should not hold private API keys. The relay solves that by moving all upstream Sportmicro access into the Node server. That gives you a few useful properties:
- credentials stay server-side
- the upstream response can be normalized before the client sees it
- reconnect and error handling can live in one place
- the frontend stays focused on rendering
That last point matters more than people think. Once the browser is only responsible for UI state, the code becomes much easier to reason about. The app becomes a consumer of snapshots instead of a mini data platform.
How Sportmicro fits into the flow
The project documentation and README make one thing clear: this demo uses documented Sportmicro endpoints only. The backend demonstrates live match retrieval and live incident-style event retrieval, with tennis using a point-by-point style fallback for incident-like updates.
The important part is not the exact endpoint syntax. It’s the pattern: the Node server fetches live sports data from Sportmicro, transforms it into a small application-specific snapshot, and pushes that snapshot over WebSocket.
That snapshot contains:
- the selected sport
- a fetch timestamp
- a list of matches
- a list of incidents
The UI then renders those two lists in separate panels: one for live match cards and one for the event log. It’s a simple model, but it’s enough to demonstrate live scores WebSocket behavior without pretending the app is doing more than it is.
Implementation flow
I like to think of the implementation as four layers: types, transport, data shaping, and presentation.
1. Define the shared message contract
The src/types.ts file is doing a lot of quiet work. It defines the allowed sport values, connection states, match records, incident records, and the client/server message types.
That pays off immediately in a project like this. The frontend hook knows what a snapshot looks like, and the app component knows what it can expect from the data. That gives the demo a strong spine without any unnecessary complexity.
2. Build the WebSocket hook
The useSportStream hook in src/websocket/useSportStream.ts is the client’s communication layer.
Its responsibilities are intentionally narrow:
- connect to the WebSocket URL
- send a
subscribemessage when the socket opens - listen for
snapshotanderrormessages - handle reconnects after disconnects
I especially like the reconnection approach here because it’s practical but not overengineered. If the socket closes unexpectedly, the hook marks the state as reconnecting and tries again after a short delay. If the component unmounts, it stops reconnecting and closes the socket cleanly. That’s exactly the kind of behavior you want in a sports data streaming demo: resilient, but not noisy.
3. Shape the UI around live state
The App.tsx component is where the project becomes visible.
It keeps the selected sport in local state, passes that into useSportStream, and then renders three main areas:
- a hero section with the project framing
- a toolbar with sport selection and connection state
- a two-column content area with matches and events
The match rendering includes a few thoughtful details:
- a helper to build a human-readable title
- a helper to format score lines
- a fallback empty state when there are no live matches
That last point is important. Real sports APIs often return empty results depending on timing, match availability, or the selected sport. A demo should acknowledge that instead of faking content.
The event log follows the same principle. It renders recent incident records if they exist, otherwise it shows a clear empty state. That keeps the app truthful in local development.
4. Keep the server as the secure middle layer
The server files are the part of the app I’d expect another engineer to extend first.
The repository includes a Node server entry point and a Sportmicro integration module. The README describes the server’s job clearly: accept WebSocket connections, authenticate with Sportmicro using SPORTMICRO_API_KEY, fetch data from documented endpoints only, and stream normalized snapshots back to the browser.
That’s the right boundary. The frontend never knows about the API key, and the backend never leaks raw upstream details into the UI unless the app deliberately chooses to render them.
A quick look at the structure
Here’s the shape of the project as it stands:
.
├── server/
│ ├── index.ts
│ └── sportmicro.ts
├── src/
│ ├── App.tsx
│ ├── App.test.tsx
│ ├── main.tsx
│ ├── styles.css
│ ├── sportLabels.ts
│ ├── sports.ts
│ ├── types.ts
│ └── websocket/
│ └── useSportStream.ts
├── .env.example
├── README.md
├── package.json
└── vite.config.ts
That tree tells the whole story: a small server, a focused React app, and shared types that keep the transport contract explicit.
What I liked about the implementation choices
There are a few design decisions here that make the project feel well-scoped.
First, the app only supports three sports, and that’s enough. I’ve seen too many demos get cluttered by trying to support everything at once. A constrained set of options makes the architecture easier to understand.
Second, the client and server each have a single job. The React side renders state. The Node side manages the socket bridge and upstream access. That division keeps the code readable.
Third, the UI acknowledges uncertainty. If there are no live matches or no event records, the screen doesn’t pretend otherwise. That makes the demo more credible and a better foundation for future expansion.
Fourth, there’s test coverage for the app’s basic rendering. The included test checks that the headline, Sportmicro branding, and connected state appear as expected when the hook is mocked. That’s a sensible unit boundary for a project like this.
Local setup, based on the repository docs
The repository documents a straightforward local setup:
- Node.js 18+ is required
- an active Sportmicro API key is needed
- dependencies are installed with
npm install -
.env.exampleis copied to.env - the app is started with
npm run dev - the client can be built with
npm run build:client - the full production build uses
npm run build
The documented environment variables are:
SPORTMICRO_API_KEYPORTCLIENT_ORIGINVITE_WS_URL
I appreciate that the repository is explicit about these pieces. For a real-time demo, environment variables are not just config; they’re part of the architecture.
Sensible next improvements
If I were extending this project, I’d keep the same architecture and improve it incrementally.
A few natural next steps stand out:
- add clearer loading states for each sport transition
- surface richer metadata in the match cards if the upstream response supports it
- improve the event log grouping or ordering
- make the empty states more sport-aware
- add more tests around socket state changes and error rendering
I’d also keep an eye on the boundary between normalization and presentation. The backend should continue shaping upstream data into a consistent snapshot, so the frontend stays simple even if new Sportmicro endpoints are added later.
That matters because once a live sports app starts growing, the temptation is to let every new upstream field leak straight into the UI. This demo avoids that trap by starting with a stable message contract.
What I took away
The biggest lesson from this build is that real-time doesn’t have to mean complicated.
If you keep the data flow small, define a clear WebSocket message contract, and place the API boundary on the server, you can build a convincing live sports experience without overengineering it. The result is a practical pattern for a Sportmicro powered sports dashboard: the browser subscribes, the server relays, and the UI updates as soon as new snapshots arrive.
That’s the part I’d want another developer to remember. The magic of a sports WebSocket API app is not that every piece is advanced. It’s that each piece does one job well, and the whole system feels live because the flow is simple, secure, and explicit.
Top comments (0)