DEV Community

Gobe34
Gobe34

Posted on Originally published at github.com

How to Build a Tennis Live Score Tracker with Next.js with Sportmicro

Building a tennis live score tracker sounded simple at first: fetch matches, show scores, done. But as soon as I looked at the shape of real sports data, the problem became more interesting. Live feeds are often incomplete, statuses change constantly, and tournament context matters just as much as the scoreline. I wanted a small app that could surface live, upcoming, and completed tennis matches without turning into a giant data-mapping exercise.

That is what led me to this project: a compact Next.js app that uses Sportmicro as the tennis data source and keeps the interface readable even when the API returns partial records. The result is a good example of how to build a practical dashboard around a [Tennis Live Score API] without overengineering the first version.

View the repository

What I set out to build

The goal was not to create a complete tennis platform. I wanted a focused tracker that demonstrates a clean integration with Sportmicro and shows how to present tennis data in a modern UI.

The app is built around a few core ideas:

  • show live tennis matches
  • show upcoming matches
  • show completed matches
  • display tournament names when available
  • handle missing fields safely
  • keep the layout responsive on mobile and desktop

That scope is intentionally narrow. It makes the app easy to reason about, while still being useful as a starting point for a tennis API-powered product.

The architecture I used

The repository keeps the structure small and easy to follow. I like that because sports feeds are already dynamic enough; the code around them should stay predictable.

At a high level, the app is split into three layers:

  • data access in src/lib/sportmicro.ts
  • presentation components in src/components/MatchCard.tsx
  • page composition in src/app/page.tsx

That separation is doing most of the heavy lifting here.

Data access and normalization

The src/lib/sportmicro.ts file is where the Sportmicro integration lives. It defines the data shapes used by the app, fetches data from the Sportmicro tennis base URL, and converts raw match objects into a UI-friendly format.

A few details stand out:

  • fetchMatches() requests match data from /matches
  • fetchTournaments() requests tournament data from /tournaments
  • getMatchStatusCategory() groups status strings into live, upcoming, completed, or unknown
  • toDisplayMatch() normalizes incomplete records into something the UI can render safely

This last part is especially important. Sports APIs often return partial data, so the app avoids assuming that fields like player names, scores, or tournament names will always be present.

For example, the normalization logic falls back to values like:

  • TBD for missing team names
  • for missing score values
  • Tournament details unavailable for missing tournament names
  • Start time unavailable when there is no timestamp

That keeps the interface stable and prevents the page from breaking just because a match record is incomplete.

UI composition

The page component in src/app/page.tsx is an async server component that fetches matches and tournaments together. It then transforms the raw match list into display data and groups it by status.

The page uses a small amount of orchestration logic:

  • load matches from Sportmicro
  • load tournaments for sidebar context
  • normalize the match feed
  • split the results into live, upcoming, and completed sections
  • limit each section to a manageable number of cards

That approach keeps the UI code simple. The page is mostly about composition, while the data transformation stays in the library layer.

Reusable match cards

src/components/MatchCard.tsx handles the visual representation of one match. I like this as a separate component because it keeps the match layout consistent across sections.

Each card shows:

  • tournament name
  • player or team names
  • match status
  • start time
  • score
  • set scores

The status badge is styled by match category, so live matches, upcoming matches, and completed matches are visually distinct. That small detail helps the page scan well at a glance.

How Sportmicro fits into the build

This project uses Sportmicro as the tennis data source, and the integration is straightforward by design.

The app reads from two documented endpoints:

  • /matches
  • /tournaments

That is enough to create a useful live-score experience while staying within a focused API surface. The match endpoint drives the main dashboard, and the tournament endpoint adds context in the sidebar.

One thing I appreciate about this setup is that the app does not rely on a large number of requests or a complicated backend. It is a clean “fetch, normalize, display” flow.

The base URL used in the code is https://tennis.sportmicro.com, which keeps the integration explicit and easy to locate in the source. The project also includes SPORTMICRO_API_KEY in .env.example, which signals that local credentials should be handled through environment variables rather than hardcoded values.

Even though the repository context does not show a security scheme in the OpenAPI document, the code and docs still treat credentials as something to keep out of source control. That is the right pattern for any API-backed app.

The implementation flow

If I were walking through the build from scratch, I would think about it in this order.

1. Define the data shapes

The first step is to model the data coming back from Sportmicro. In src/lib/sportmicro.ts, the project defines interfaces for:

  • SportmicroMatch
  • SportmicroTournament
  • MatchDisplayData

That gives the rest of the app a predictable contract. Instead of passing raw API objects all the way through the UI, the app normalizes them once and then works with display-ready data.

2. Fetch only the data needed for the dashboard

The app fetches matches and tournaments in parallel using Promise.all(). That is a practical choice because both requests are independent, and the page benefits from getting them together.

The code also uses Next.js revalidation settings on fetch requests:

  • revalidate: 30 for matches
  • revalidate: 3600 for tournaments

That suggests different refresh expectations for live match data versus slower-changing tournament context, which matches the nature of the data itself.

3. Normalize before rendering

The toDisplayMatch() helper is the bridge between API data and UI data. It determines the title, score, status, tournament label, and formatted start time.

This is where the app becomes resilient. Instead of checking for missing fields in the component tree, the logic centralizes those decisions in one place.

4. Group by status

Once the page has display-ready matches, it filters them into sections:

  • live matches
  • upcoming matches
  • completed matches

That gives the dashboard a clear structure and helps the user scan the feed quickly.

5. Render reusable cards

The MatchCard component takes care of the repeated visual pattern. It is small, but it keeps the page clean and makes future changes easier. If I wanted to tweak the card layout later, I would only have to do it in one place.

A concise look at the project tree

Here is the core structure of the repository as it stands:

src/
  app/
    api/health/route.ts
    globals.css
    layout.tsx
    page.tsx
    page.test.ts
  components/
    MatchCard.tsx
  lib/
    sportmicro.ts
Enter fullscreen mode Exit fullscreen mode

Outside of src/, the project also includes the expected Next.js and TypeScript configuration files, plus .env.example for local API credentials.

That tree tells the story clearly: a simple Next.js app with a small service layer, one reusable component, and one page that brings everything together.

What the app already does well

There are a few things I think this repository gets right.

First, it treats incomplete sports data as normal. That matters more than it sounds like it does. A lot of dashboards look fine only when every field is filled in. In practice, good API consumers need to stay useful when fields are missing.

Second, it keeps the layout responsive without adding unnecessary complexity. The page uses Tailwind CSS and a simple two-column layout on larger screens, while still reading naturally on smaller devices.

Third, it shows a clear mental model for Sportmicro integration. If someone wanted to use the same approach for other tennis views, they would not need to untangle a lot of abstractions first.

Finally, the repository includes a small health route at src/app/api/health/route.ts, which returns { ok: true }. It is a tiny file, but it is useful as a basic application check and fits the lightweight nature of the project.

Local setup, based on the repository

The repository supports a straightforward local workflow.

The documented prerequisites are:

  • Node.js 18+
  • npm
  • a Sportmicro API key in your environment

The installation steps in the README are:

npm install
cp .env.example .env.local
SPORTMICRO_API_KEY=your_sportmicro_api_key_here
npm run dev
Enter fullscreen mode Exit fullscreen mode

The app should then be available at http://localhost:3000.

The project also includes two useful verification commands:

npm run build
npm test
Enter fullscreen mode Exit fullscreen mode

And the test script is backed by Node’s built-in test runner, while npm run lint runs TypeScript type checking with tsc --noEmit.

I am only calling out setup steps that are actually supported by the repository itself, because that is the safest way to describe a public project like this.

Tests and safety checks

The repository includes a small test file, src/app/page.test.ts, that exercises the status categorization and data normalization helpers.

That is a good sign. It means the project is not only rendering data, but also protecting the small logic layer that makes the UI resilient.

The tests cover:

  • live status categorization
  • completed status categorization
  • safe normalization of incomplete match data

Those checks are modest, but they target the parts of the app most likely to break if the API shape changes or if the mapping logic regresses.

Sensible next improvements

If I were extending this app, I would keep the same philosophy: small, typed, and focused on documented Sportmicro data.

A few next steps would make sense:

  • add more explicit loading states for the dashboard
  • expand the tournament sidebar into a real filter UI
  • add a dedicated match detail view
  • support more Sportmicro tennis data where it fits the existing structure
  • keep any additional data access logic inside src/lib/sportmicro.ts
  • preserve the current fallback behavior for incomplete records

The key is not to add features just because they are possible. I would only extend the app where it improves the experience of tracking live tennis data.

Takeaway

This project is a strong example of how to build a focused API-driven dashboard without overcomplicating the stack. The combination of Next.js, TypeScript, Tailwind CSS, and Sportmicro creates a clean path from raw tennis data to a readable live-score interface.

What I take away from it is simple: when you are integrating a sports API, the hard part is not rendering cards. The hard part is designing for incomplete data, changing status values, and a feed that needs to stay useful even when the response is sparse.

That is why the normalization layer matters so much here. It turns raw Sportmicro responses into a stable UI model, and everything else becomes easier after that.

If you want to explore the implementation and adapt it for your own tennis dashboard, start with the repository, study the data helper in src/lib/sportmicro.ts, and build outward from there.

Top comments (0)