If you have ever tried to build a football odds dashboard from scratch, you know the first challenge is not the UI — it is the data shape. Fixtures, bookmakers, markets, and kickoff times all arrive with enough optionality to make a simple prototype feel fragile fast. I wanted to build something practical: a small tracker that compares upcoming matches and odds without turning into a pile of ad hoc fetch calls.
That is what led me to this project, a React + TypeScript app that uses Sportmicro as the data source for football odds and match data. The result is intentionally modest, but that is the point. It shows the path from a typed frontend to a usable odds comparison interface, while keeping the implementation clean enough to extend later.
What I set out to solve
The app is built around a simple question: how do I present football odds in a way that is readable, resilient, and easy to refresh?
A lot of sports data demos stop at rendering a list of fixtures. That is fine for a proof of concept, but the more useful version is one that also shows bookmaker odds, handles missing values gracefully, and gives the user a way to filter the noise. I wanted the experience to feel like a lightweight sportsbook-style dashboard rather than a static API sample.
So the project focuses on a few specific outcomes:
- upcoming football matches
- league and competition details
- home and away teams
- kickoff times
- bookmaker odds for the full-time result market
- a simple league filter
- a manual refresh action
- loading and error states
- fallback sample data when live requests fail
That scope is narrow on purpose. It makes the app easier to reason about, and it matches the idea of using Sportmicro as the backend for football analytics, betting, or sportsbook interfaces.
Why Sportmicro fits the architecture
The project uses the Sportmicro football API directly from the frontend, with a dedicated client in src/apiClient.ts. The app only relies on two documented endpoints:
GET /matchesGET /odds/full-time-results
That was enough to build the core experience. The match endpoint provides upcoming fixtures, and the odds endpoint supplies bookmaker prices for the full-time result market. Those two data sources complement each other well: one gives structure, the other gives context.
I also like the fact that the app treats the API as an external dependency rather than assuming it will always be available. The code expects data to be incomplete or temporarily unavailable, which is exactly how sports data behaves in real projects. The UI therefore leans on local sample data when requests fail, rather than leaving the page blank.
The API base URL used in the project is https://football.sportmicro.com, and the repository also points to the Sportmicro docs as the source of truth for endpoint behavior. That discipline matters. When you build on a sports betting API, the safest approach is to request only what you need and model only the fields you actually render.
The project structure I worked with
The repository is intentionally compact. The core files are easy to map mentally:
src/
App.tsx
apiClient.ts
main.tsx
mockData.ts
styles.css
types.ts
test/
That structure is simple, but it covers the main layers of the app:
-
apiClient.tsisolates Sportmicro requests -
types.tsdefines the local data model -
mockData.tsprovides sample matches, leagues, and odds -
App.tsxhandles the user interface and data flow -
main.tsxmounts the React app -
test/contains tests for the client and shell rendering
This separation is what keeps the project understandable. I do not need to hunt through UI code to see how requests work, and I do not need to inspect fetch logic to understand how the screen behaves.
The implementation flow
The app starts in src/main.tsx, where React renders the root App component and loads styles.css. Nothing unusual there, which is a good sign. The important work happens inside App.tsx.
1) I defined the data model first
The TypeScript interfaces in src/types.ts keep the app grounded in the fields the UI actually needs:
-
Leaguehas anidandname, with a couple of optional metadata fields -
Matchincludes league ID, league name, team names, start time, status, and bookmaker count -
FullTimeResultOddsrepresents odds for a single match and bookmaker - each outcome can carry a name and an optional price
That design keeps the UI flexible. It does not assume every field will be present, and it lets the interface degrade gracefully if the API returns partial data.
2) I centralized requests in one helper
The request helper in src/apiClient.ts is small, but it does the important things:
- it builds URLs against the Sportmicro football API base
- it appends query parameters in a reusable way
- it sends an
Accept: application/jsonheader - it adds a Bearer token when
SPORTMICRO_API_KEYis available - it throws a typed
SportmicroApiErrorwhen the response is not OK
That error class is useful because the UI can distinguish a known API failure from some other unexpected issue. In practice, that means the app can give a helpful message and fall back to sample data instead of crashing.
3) I used local sample data as a safety net
The mock data in src/mockData.ts includes a few leagues, matches, and odds records. This is not a fake product layer; it is a fallback layer.
That fallback is important for two reasons:
- it keeps the app useful when live API requests fail
- it makes the interface easier to understand before a real API key is configured
I think that is the right tradeoff for a public demo. A sports app without data is not very convincing, and a sports app that breaks when the backend is offline is not very trustworthy.
4) I built the refresh flow around live requests
The refreshData function in App.tsx is the main data-loading path. It sets a loading state, clears previous errors, and then requests both /matches and /odds/full-time-results.
If the requests succeed:
- live matches replace the current match list
- live odds replace the current odds list
- empty responses fall back to sample data
If the requests fail:
- the app shows a clear message
- sample data is restored
- the interface stays usable
That behavior is probably the most practical part of the project. It lets the app act as both a demo and a real-time tracker without forcing the user into a broken state.
5) I added filtering and visual separation
The interface has a simple league filter powered by local state. It filters the displayed matches by league_id, with an all option for the full list. It is a small interaction, but it makes the screen more useful immediately.
The odds are rendered in a separate section via a dedicated OddsCard component. I like this split because fixtures and market data are related, but they are not the same thing. Separate cards make that distinction clearer.
The render logic also deals with partial values carefully:
-
Time unavailablewhen kickoff time is missing -
Competition unavailablewhen the league name is absent -
N/Afor missing odds prices -
upcomingwhen a status is not provided
That kind of defensive rendering is easy to overlook, but it is exactly what makes a tracker feel reliable.
What the UI is doing well
The app is not trying to imitate a full sportsbook. Instead, it stays focused on a few useful views:
- a header that explains the purpose
- a manual refresh button
- a league filter sidebar
- match cards for upcoming fixtures
- odds cards for the full-time result market
- an API usage note at the bottom
The layout is responsive, and the styling is handled with Tailwind CSS. That keeps the code concise while still making the interface readable on different screen sizes.
I also appreciate that the app surfaces its data source clearly. The “Powered by Sportmicro” text and the docs link are a nice reminder that this is a live-data application, not a static mock.
Testing the core pieces
The repository includes tests for the API client and for the app shell.
The API client test checks two important behaviors:
- it builds the correct request URL
- it throws a typed error when the response is not OK
That gives confidence that the Sportmicro integration layer behaves as expected.
The app test renders the shell and verifies that the main structure is present. That is a sensible lightweight test for this kind of project. It does not overreach, but it protects the core user-facing layout.
Local setup, based on the repository
The repository includes a standard Vite setup and documents a straightforward local workflow:
npm install
npm run dev
npm run build
npm run test
It also expects a local .env file based on .env.example, with SPORTMICRO_API_KEY set for authenticated requests.
That is enough to run the project locally, and the README makes it clear that the app can still operate with sample data if the API is unavailable. I did not need anything more elaborate than that to understand how to get started.
Sensible next improvements
Because the project is already small and focused, the next improvements should stay grounded in the same design.
A few directions make sense:
- add more documented Sportmicro views, such as leagues, standings, teams, or matches by date
- expand the odds display beyond the full-time result market if the documentation supports it
- make the filter controls richer without adding clutter
- improve empty-state messaging for fixtures and odds separately
- keep extending the test coverage around request handling and rendering paths
The important thing is to keep the same pattern: typed data model, centralized API client, and UI that assumes data can be partial or unavailable.
Takeaway
What I like about this project is that it treats a football odds app as a real engineering exercise, not just a data display exercise. The combination of React, TypeScript, Vite, and Sportmicro gives you a clean path to build a useful sportsbook-style interface, but the real lesson is architectural: isolate the API, type the data you consume, and never assume live sports data will be complete.
If you are building a football odds tracker, that mindset matters more than any individual component. Start with the smallest useful slice, keep the integration clean, and let the UI fail gracefully. That is what makes a demo worth keeping around after the first run.
Top comments (0)