When I started this project, the problem I wanted to solve was narrow but realistic: build a Premier League live scores dashboard that behaves like a real product, not a toy demo. Sports data is messy in ways that generic API examples often ignore. Matches can be live, finished, postponed, or interrupted. Fields can be missing. A response can be empty and still be perfectly valid.
So the goal of this build was not “show some football cards.” It was to create a clean, production-style dashboard that demonstrates how to use Sportmicro in a practical Next.js application, with a data layer that stays reusable and a UI that makes state changes explicit.
If you want to inspect the source, View the repository.
The shape of the app
I kept the architecture intentionally small. The repository is a Next.js app using React, TypeScript, and Tailwind CSS, but the important part is how the responsibilities are split:
-
src/lib/sportmicro.tsowns Sportmicro-specific data access -
src/app/page.tsxfocuses on rendering the dashboard -
src/app/layout.tsxprovides the app metadata and document shell -
src/app/page.test.tsandsrc/lib/sportmicro.test.tscover the score normalization behavior
That separation matters because sports API code tends to accumulate tiny decisions: how to authenticate, what to do when the API is missing, how to display nullable scores, and how to present status values. I wanted all of that to live in one place instead of leaking into the page component.
The app currently centers on one documented live-data workflow: fetching live Premier League matches and rendering them as cards with status, teams, scores, and kickoff time. The page also includes clear handling for empty and error states, which is where these dashboards often become useful or frustrating.
Sportmicro integration: keep the boundary small
The integration is built around a small client module. That module uses the default base URL https://football.sportmicro.com, reads SPORTMICRO_API_KEY, and exposes a couple of fetch helpers. The live dashboard uses the documented /matches-live endpoint, and the client also includes a getMatchesByDateLeague helper for the /matches-by-date-league endpoint.
That client is doing more than just wrapping fetch. It is the boundary between the UI and the API.
A simplified excerpt shows the pattern:
async function requestJson<T>(path: string, params: Record<string, string | undefined> = {}): Promise<T> {
const apiKey = process.env.SPORTMICRO_API_KEY;
if (!apiKey) {
throw new Error('SPORTMICRO_API_KEY is not configured.');
}
const response = await fetch(buildUrl(path, params), {
headers: {
Authorization: `Bearer ${apiKey}`
},
next: { revalidate: 10 }
});
if (!response.ok) {
throw new Error(`Sportmicro request failed with status ${response.status}.`);
}
return (await response.json()) as T;
}
The practical value here is not the syntax. It is the discipline:
- the UI never needs to know how authentication works
- the UI never needs to know the base URL
- missing credentials fail clearly
- upstream failures surface as readable errors
That makes the dashboard easier to reason about and easier to extend later. If I add another Sportmicro endpoint, I can do it in the same client without changing the rest of the app’s structure.
Implementation flow: from API response to dashboard state
The app flow is straightforward, but each step exists for a reason.
First, the page calls getLiveMatches() inside the server component. The page starts with an empty array and a nullable error string, then swaps those values depending on the outcome of the request. That gives the UI three meaningful branches: error, empty, and data.
Second, each match is rendered as a card with a status pill, team names, scores, and kickoff time. The page reads fields defensively with fallback text like Home team, Away team, and unknown, which is important because live sports responses are rarely perfectly complete.
Third, scores are normalized before rendering. The project keeps this deliberately boring: null, undefined, and empty string become —, while real values are stringified.
That logic is small enough to be tested directly, which is exactly what I wanted. The tests in the repository confirm two things:
- missing values become a placeholder
-
0is preserved as a legitimate display value
That second point is subtle but important. In sports interfaces, zero is data. It should never disappear just because it is falsy.
The dashboard UI: simple, responsive, and honest
The dashboard page uses Tailwind CSS for a responsive layout, but I was careful not to let styling distract from the data model. The UI has a headline, a “Live matches” section, and a grid of match cards. There is also a search input in the layout, though the current code shows it as interface scaffolding rather than a fully wired filter.
The most useful part of the UI is not the cards themselves; it is how clearly the page communicates state.
If Sportmicro returns nothing, the dashboard explains that no live matches were returned and that this can be expected when the Premier League is not active or when the configured account has no data available. If the request fails, the page shows an alert-like error message and points the user toward environment variables and API reachability.
That distinction is what makes the app feel like a real integration. Empty is not error. Error is not empty. And a missing API key is neither of those; it is a configuration problem that should be obvious immediately.
I also appreciated the way the app keeps the data visualization restrained. Each card shows:
- a status pill
- team names
- home and away scores
- kickoff time
- a stable identifier when available
That is enough to demonstrate a usable live scores interface without over-committing to a design that would be hard to evolve.
Local setup and environment configuration
The repository supports a normal local setup, and the evidence in the project makes that part clear.
The README documents the expected workflow:
npm install
cp .env.example .env.local
npm run dev
The app expects SPORTMICRO_API_KEY, and src/types/global.d.ts also defines SPORTMICRO_BASE_URL as an optional environment variable. The client defaults to https://football.sportmicro.com, so local setup stays simple unless you explicitly want to override the base URL.
There are also scripts for build and test:
npm run buildnpm testnpm run typecheck
I appreciate that the project keeps setup visible instead of hiding it behind helper scripts. For a developer learning an API integration, that is usually the right trade-off.
Challenges and trade-offs
Because this is a build-focused example, the important constraints were mostly design constraints rather than documented failure stories.
The first trade-off was scope. The repository intentionally focuses on live matches rather than trying to model every possible Sportmicro football endpoint. That keeps the integration honest and easier to understand.
The second trade-off is data handling. The code favors explicit fallbacks over assumptions, which means the UI stays predictable when the API returns missing names, nullable scores, or no matches at all. That is less flashy than complex client logic, but it is much safer for a sports dashboard.
The third trade-off is test coverage. The tests in the repo focus on score normalization, which is a sensible place to start because formatting mistakes are easy to miss and easy to reuse elsewhere.
Project tree
Here is the concise shape of the project:
src/
app/
globals.css
layout.tsx
page.tsx
page.test.ts
lib/
sportmicro.ts
sportmicro.test.ts
types/
global.d.ts
That tree tells the story pretty well. The app is small, the client is isolated, and the tests sit close to the code they verify.
What I would build next
There are a few natural next steps that fit the current structure without changing the project’s direction:
- Expand the dashboard with more documented Sportmicro endpoints, such as fixtures or standings.
- Add richer runtime validation for API responses so the app can distinguish more match field shapes safely.
- Wire the existing search input into an actual filter model.
- Broaden the tests beyond score normalization to cover page states and client behavior.
- Add a dedicated section for upcoming matches so the app covers more of the match lifecycle.
I would treat those as future improvements, not as missing pieces. The current project already demonstrates the core integration pattern: fetch live football data, normalize it carefully, and present it in a UI that makes the data state obvious.
Takeaway
The main lesson from this build is that a useful sports dashboard is less about clever UI and more about clear boundaries. Keep the API client separate, treat empty data as a first-class state, normalize the awkward values early, and let the page stay focused on rendering.
That approach made this Sportmicro example feel like a real application rather than a demo glued to an endpoint. If you are building your own football API project, start with one narrow workflow and make that workflow robust before you widen the scope.
Top comments (0)