DEV Community

Miheve
Miheve

Posted on

How to Build a NBA Game Odds Monitor with Sportmicro

I built this project because sports data demos often look fine in a screenshot and then fall apart the moment the API response is missing a field, the key is absent, or the data simply hasn’t arrived yet. The goal here was narrower and more useful: create a clean NBA game odds monitor that shows how to wire Sportmicro into a React + TypeScript app without hiding the messy parts of real API usage.

The repository is a small but practical example of a nba game odds monitor API workflow. Instead of trying to mimic a full sportsbook, I focused on a developer-facing dashboard that can fetch bookmaker odds, compare markets, refresh on demand, and make loading/error/empty states obvious. That makes it a better learning artifact than a “happy path only” demo.

View the repository

What I set out to build

The project is intentionally scoped around basketball odds data. In the current implementation, the app uses a dedicated Sportmicro client and a React UI to display moneyline odds and bookmaker metadata. The repository README and source show that the app currently centers on:

  • /odds/bookmakers
  • /odds/moneyline

That focus matters. A sports data API can expose a lot of surface area, but a useful tutorial usually does better when it shows one complete flow end to end rather than a half-dozen endpoints superficially.

I also wanted the structure to feel production-style even though the app is small. That means:

  • the UI does not build API URLs directly
  • the API key is read from environment variables
  • the app handles loading, empty, and error states explicitly
  • the response is normalized into a local UI-friendly shape

Those choices keep the demo honest. They also make it easier to extend later without rewriting the whole app.

Architecture: keep the Sportmicro boundary isolated

The cleanest part of the codebase is the separation between the UI and the provider-specific client. The data flow is straightforward:

UI -> app state -> Sportmicro client -> network request -> Sportmicro API
Enter fullscreen mode Exit fullscreen mode

That boundary is implemented in src/sportmicroClient.ts, while src/App.tsx stays focused on state and rendering. I think that split is the right default for any sports developer API integration because it prevents the component tree from becoming a pile of fetch logic and URL assembly.

The client module does three important things:

  1. It chooses a base URL, defaulting to https://basketball.sportmicro.com.
  2. It reads SPORTMICRO_API_KEY from the environment.
  3. It wraps fetch calls into typed helpers.

Here’s the core pattern:

const DEFAULT_BASE_URL = 'https://basketball.sportmicro.com';

function getBaseUrl() {
  return import.meta.env.VITE_SPORTMICRO_BASE_URL || DEFAULT_BASE_URL;
}

async function requestJson<T>(path: string, params?: Record<string, string | undefined>): Promise<T> {
  const apiKey = import.meta.env.SPORTMICRO_API_KEY as string | undefined;
  if (!apiKey) {
    throw { message: 'SPORTMICRO_API_KEY is not set. Add it to your environment before refreshing odds.' } satisfies ApiError;
  }

  const response = await fetch(buildUrl(path, params), {
    headers: { Authorization: `Bearer ${apiKey}` }
  });

  if (!response.ok) {
    throw { message: `Sportmicro request failed with HTTP ${response.status}.`, status: response.status } satisfies ApiError;
  }

  return (await response.json()) as T;
}
Enter fullscreen mode Exit fullscreen mode

The important part is not the syntax; it’s the decision to make the API client the only place that knows about auth headers, base URLs, and response checking. The React app only consumes fetchBookmakers() and fetchOddsForMatch().

That keeps the integration flexible. If I later wanted to add caching, a backend proxy, or a different provider-compatible base URL, the UI would not need to know.

Implementation flow in the UI

The main application logic lives in src/App.tsx, and the flow is deliberately simple.

First, the app loads bookmaker metadata on mount:

  • useEffect calls fetchBookmakers()
  • the result populates a local bookmakers state array
  • if that request fails, the app quietly leaves the filter list empty

Then the user enters a match ID and clicks refresh. The refresh handler checks whether a match ID exists before calling Sportmicro. If the field is empty, the app sets a visible error and stops early.

That guard is a small but important detail. It keeps the app from pretending it can load odds without the one input it actually needs.

After the fetch succeeds, the app normalizes each odds row into a presentation-friendly shape. The normalization happens in a local helper:

function normalizeMarket(row: OddsMarket, index: number): MarketRow {
  return {
    key: [row.bookmaker_id ?? row.bookmaker_name ?? 'unknown', row.market_name ?? 'moneyline', row.selection ?? 'selection', index].join(':'),
    bookmaker: row.bookmaker_name ?? row.bookmaker_id ?? 'Unknown bookmaker',
    market: row.market_name ?? 'Moneyline',
    selection: row.selection ?? 'Unknown selection',
    odd: typeof row.odd === 'number' ? row.odd.toFixed(2) : row.odd ?? '',
    live: row.is_live ? 'Live' : 'Pre-match',
    updatedAt: row.updated_at ?? 'Unknown'
  };
}
Enter fullscreen mode Exit fullscreen mode

I like this pattern because it does two jobs at once:

  • it shields the UI from missing API fields
  • it creates a consistent display model for the table

That’s especially useful with sports data, where the provider may return optional values or varying response shapes. By the time data reaches the table, the app has already decided what “missing” should look like: Unknown, , or a fallback label.

The UI then uses a single table to display bookmaker, market, selection, odd, state, and updated timestamp. A filter dropdown lets me narrow the view by bookmaker name. That filter is driven by the bookmaker list loaded from Sportmicro, which makes the UI feel connected to the same data source rather than acting like a static mockup.

Project structure at a glance

The tree is compact enough to understand quickly:

src/
  App.tsx             React UI and local state
  sportmicroClient.ts  Sportmicro API client and request helpers
  types.ts             Shared TypeScript types
  main.tsx             React entry point
  styles.css           Global styles
Enter fullscreen mode Exit fullscreen mode

The rest of the repository supports that core flow with Vite, TypeScript, Tailwind config, and test setup. Nothing in the tree feels decorative. Each file has a clear job.

The TypeScript models are intentionally small too:

  • ApiError
  • OddsMarket
  • Bookmaker
  • MarketRow

That is a sensible choice for a focused odds monitor. I don’t need to model every possible Sportmicro basketball field to make the UI useful. A smaller type surface also makes the tutorial easier to follow, because the app only carries the data it actually renders.

Local setup and how I verified the project shape

The repository supports local development with standard Node tooling. The README documents the expected flow:

  • npm install
  • copy .env.example to .env
  • set SPORTMICRO_API_KEY
  • run npm run dev
  • optionally run npm run build and npm run test

That matches the scripts in package.json, which include dev, build, preview, and test.

The setup details matter because the client expects SPORTMICRO_API_KEY to exist before odds can be refreshed. The repository also documents an optional VITE_SPORTMICRO_BASE_URL, which is useful if you want to point the client at a compatible base URL during development.

I also like that the test coverage is modest and honest. The existing test in src/app.test.tsx checks that the app renders the title and the empty state. That may sound small, but it proves the shell of the experience works before any data arrives. For a demo focused on API integration, that is a meaningful baseline.

Challenges and trade-offs

I didn’t treat this project as a place to simulate dramatic failures or invented incidents. The real design constraints are more interesting.

The first trade-off is scope. The app focuses on odds comparison and refresh, not on every Sportmicro basketball endpoint. That keeps the example readable, but it also means the UI is intentionally not a complete sports platform.

The second trade-off is client-side simplicity. The current implementation uses a browser-based client with an environment-provided API key and no server proxy. That makes the demo easy to run and understand, but it also leaves room for future work if stronger credential protection or caching is needed.

The third trade-off is normalization. The app maps provider fields into a smaller MarketRow shape. That keeps rendering predictable, though it means the UI is not trying to preserve every upstream field exactly as returned.

Those are all sensible decisions for an educational repository. They reduce complexity without pretending the problem is simpler than it is.

What I would improve next

A few next steps stand out naturally from the current codebase. I would treat these as future improvements, not existing behavior:

  • Add another documented odds endpoint and reuse the same client pattern.
  • Introduce stronger row normalization if more markets are displayed.
  • Add deduplication based on stable provider IDs if the response shape supports it.
  • Consider a backend proxy if the app ever needs stricter key handling.
  • Add caching or a simple refresh policy if repeated requests become expensive.
  • Expand tests around error handling and filtering behavior.

Those changes would fit the existing architecture well because the provider boundary is already isolated. The point is not to rewrite the app; it is to extend the same pattern carefully.

Takeaway

The main lesson from this build is that a useful sports API demo is less about flashy UI and more about boundaries, fallback states, and a small amount of disciplined normalization. By keeping Sportmicro integration in one client module, using a compact TypeScript model, and making the empty/error/loading states explicit, the app stays understandable even when the data is dynamic.

If I were building another sports developer API example, I’d keep this same recipe: one provider boundary, one clear user action, one narrow data shape, and a UI that tells the truth when the data is not ready yet. That is what makes a demo feel reusable instead of disposable.

Top comments (0)