<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Miheve</title>
    <description>The latest articles on DEV Community by Miheve (@mihailove123).</description>
    <link>https://dev.to/mihailove123</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3983989%2Fb10792bc-45b1-47a4-bc38-94f75057d695.jpg</url>
      <title>DEV Community: Miheve</title>
      <link>https://dev.to/mihailove123</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mihailove123"/>
    <language>en</language>
    <item>
      <title>How to Build a Sports Data FastAPI Gateway with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 20 Sep 2026 13:19:33 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-sports-data-fastapi-gateway-with-sportmicro-47bj</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-sports-data-fastapi-gateway-with-sportmicro-47bj</guid>
      <description>&lt;p&gt;When I started this project, I wanted a small but realistic way to show how a backend can sit in front of a sports data provider without turning into a thin, leaky proxy. The goal was to build a sports data FastAPI gateway that feels production-style: environment-based configuration, a typed client, explicit resource mapping, and predictable responses for consumers who want to work with Sportmicro data through a clean interface.&lt;/p&gt;

&lt;p&gt;The repository is an open-source example of that shape, powered by &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt;. It does not try to model the entire upstream API. Instead, it focuses on a practical subset and shows the integration boundary clearly. If you want to inspect the code while reading, &lt;a href="https://github.com/mihailove123/sports-data-sportmicro-fastapi-gateway" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a gateway instead of a direct proxy
&lt;/h2&gt;

&lt;p&gt;The first thing I wanted to avoid was spreading upstream details throughout the app. When a product talks directly to a sports data API, three things tend to happen quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;credentials appear in too many places,&lt;/li&gt;
&lt;li&gt;response shapes leak into every layer,&lt;/li&gt;
&lt;li&gt;error handling becomes inconsistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A gateway solves that by putting the upstream integration behind one service boundary.&lt;/p&gt;

&lt;p&gt;In this project, FastAPI is the public interface, but Sportmicro remains the provider behind the scenes. That matters because the application owns the contract it exposes to consumers. The gateway returns a stable, developer-facing payload with a source, endpoint, count, preview items, applied filters, and an optional note when no results are found.&lt;/p&gt;

&lt;p&gt;The other practical reason for this shape is that sports data is naturally filter-heavy. A consumer might search by league, season, team, player, match, date, or status type. A gateway gives us one place to define which filters are allowed and how they are translated to the upstream API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture I used
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally small, but the separation of responsibilities is clear.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/config.py&lt;/code&gt; reads settings from the environment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/sportmicro_client.py&lt;/code&gt; handles outbound HTTP calls to Sportmicro.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/services.py&lt;/code&gt; maps supported resources to known Sportmicro endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/main.py&lt;/code&gt; defines the FastAPI app and route behavior.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/models.py&lt;/code&gt; defines the query and response structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split is the core of the implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration stays at the edge
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Settings&lt;/code&gt; reads &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; plus optional base URLs and timeout settings from environment variables. The defaults in the code are already aligned to Sportmicro’s football, basketball, and tennis hosts, and &lt;code&gt;get_settings()&lt;/code&gt; is cached with &lt;code&gt;lru_cache(maxsize=1)&lt;/code&gt;. That means configuration is loaded once and reused.&lt;/p&gt;

&lt;p&gt;This is a simple choice, but it keeps secrets out of route handlers and makes the app easier to configure locally. It also gives the gateway a single place to adjust upstream hosts if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The client does one job
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;SportmicroClient&lt;/code&gt; class is deliberately narrow. It checks that an API key exists, expects an injected HTTP client in this environment, builds a query string, attaches the Bearer token, and raises a &lt;code&gt;SportmicroClientError&lt;/code&gt; when something goes wrong.&lt;/p&gt;

&lt;p&gt;A small excerpt shows the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the right level of responsibility for this layer. It does not try to interpret business rules. It just translates a request into an upstream call and turns low-level failures into a controlled error path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The service layer owns resource mapping
&lt;/h3&gt;

&lt;p&gt;The service layer is where the gateway becomes opinionated. &lt;code&gt;RESOURCE_MAP&lt;/code&gt; explicitly lists supported resource keys such as &lt;code&gt;football-teams&lt;/code&gt;, &lt;code&gt;football-players&lt;/code&gt;, &lt;code&gt;football-matches&lt;/code&gt;, &lt;code&gt;basketball-matches&lt;/code&gt;, and &lt;code&gt;tennis-rankings&lt;/code&gt;. Each key maps to a &lt;code&gt;SportmicroEndpoint&lt;/code&gt; with a source, base URL, and path.&lt;/p&gt;

&lt;p&gt;That explicit list is important. Rather than building a generic “forward any path” proxy, the app only exposes resources that are intentionally supported. That keeps the contract understandable and reduces the chance of accidentally promising something the upstream provider does not document.&lt;/p&gt;

&lt;p&gt;The service also translates the query object into upstream parameters. It uses &lt;code&gt;order&lt;/code&gt; for the sort value, which is a good example of where the gateway is allowed to adapt the API without exposing the upstream shape directly to every caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the request flow works
&lt;/h2&gt;

&lt;p&gt;The main request path is straightforward, and that is part of the point.&lt;/p&gt;

&lt;p&gt;The app uses a FastAPI lifespan function to create the client once at startup, store the settings and service on &lt;code&gt;app.state&lt;/code&gt;, and close the client on shutdown. From there, route handlers can focus on request and response behavior instead of lifecycle management.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/resources/{resource}&lt;/code&gt; route accepts a set of query parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;league_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;season_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;team_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;player_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;match_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status_type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;limit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offset&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lang&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sort&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are assembled into a &lt;code&gt;SportmicroQuery&lt;/code&gt;, then handed to &lt;code&gt;SportmicroService.list_resource()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From there, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;check whether the requested resource is supported,&lt;/li&gt;
&lt;li&gt;resolve the matching upstream endpoint,&lt;/li&gt;
&lt;li&gt;translate the query into upstream parameters,&lt;/li&gt;
&lt;li&gt;fetch the list from Sportmicro,&lt;/li&gt;
&lt;li&gt;build a response with source, endpoint, count, preview items, filters, and an optional note.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The route also handles errors at the edge. If the client raises &lt;code&gt;SportmicroClientError&lt;/code&gt;, the app returns a 502-style JSON response with a &lt;code&gt;detail&lt;/code&gt; message and &lt;code&gt;retry_after_seconds: 60&lt;/code&gt;. That keeps the gateway honest: the failure is upstream, not local.&lt;/p&gt;

&lt;p&gt;One detail I appreciated is the empty-state handling. If Sportmicro returns no items, the gateway does not treat that as an error. Instead, it adds a note suggesting the user widen the search or remove optional filters. For sports data, that is a sensible default because a narrow query is often valid even when it produces nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data modeling and response shaping
&lt;/h2&gt;

&lt;p&gt;The repository uses dataclasses instead of a heavier schema stack. That keeps the code easy to follow while still providing structure.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SportmicroQuery&lt;/code&gt; is the input shape. It includes defaults for pagination and language, and it has a &lt;code&gt;validate()&lt;/code&gt; classmethod that rejects unexpected fields. That method is a small guardrail against accidental input drift.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ResourcePreview&lt;/code&gt; is the output shape for the preview list. It intentionally selects a limited set of commonly useful fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status_type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start_time&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;league_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;season_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;team_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;player_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;match_id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The service converts raw items into preview objects and then back to dictionaries. That means the API response stays concise even if the upstream payload includes more detail.&lt;/p&gt;

&lt;p&gt;I think this is a useful pattern for sports data APIs in general. A gateway does not have to expose every field from the provider. It can present the smallest stable subset that helps clients search, inspect, and decide what to fetch next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and trade-offs
&lt;/h2&gt;

&lt;p&gt;The main trade-off here was intentional scope, not a dramatic implementation problem.&lt;/p&gt;

&lt;p&gt;The code supports a small, explicit set of resources instead of trying to mirror all Sportmicro endpoints. That choice keeps the gateway accurate and easy to reason about, but it also means the project is not a universal proxy. In practice, that is a feature: the app only exposes the football, basketball, and tennis resources already represented in the repository.&lt;/p&gt;

&lt;p&gt;Another constraint is the backend-only design. There is no browser UI, so the repository leans on FastAPI’s OpenAPI docs and JSON responses rather than a frontend experience. That keeps the example focused on integration architecture rather than presentation.&lt;/p&gt;

&lt;p&gt;A final design consideration is transport handling. The client expects an HTTP client to be available in the runtime environment, and the repository’s shape suggests the app is built around a controlled async backend context rather than a fully embedded HTTP stack. That keeps the example clean, but it also means the transport boundary matters when you extend or test the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and testing
&lt;/h2&gt;

&lt;p&gt;The repository includes enough evidence to describe local setup safely.&lt;/p&gt;

&lt;p&gt;The README shows the expected workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
uvicorn app.main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also documents the required &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; environment variable and optional base URLs for football, basketball, tennis, plus a timeout setting. Once the server is running, the API root, &lt;code&gt;/health&lt;/code&gt;, and &lt;code&gt;/docs&lt;/code&gt; are available locally.&lt;/p&gt;

&lt;p&gt;For validation, the repository also documents two useful checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; compileall &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lines up with the presence of a test suite under &lt;code&gt;tests/&lt;/code&gt;, which is a good sign that the project is meant to be exercised rather than just read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project shape at a glance
&lt;/h2&gt;

&lt;p&gt;Here is the compact structure I kept in mind while building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  config.py
  main.py
  models.py
  services.py
  sportmicro_client.py
tests/
  test_app.py
  test_client.py
  test_main.py
  test_models.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tree is small enough to understand quickly, but it still reflects a real backend boundary: configuration, transport, service logic, and API surface are all separated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;A few future improvements stand out if you wanted to evolve this gateway further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add more documented Sportmicro resources to the explicit map,&lt;/li&gt;
&lt;li&gt;expand response modeling for endpoints that have stable, known shapes,&lt;/li&gt;
&lt;li&gt;add caching where upstream update frequency makes sense,&lt;/li&gt;
&lt;li&gt;broaden tests around error paths and empty-state responses,&lt;/li&gt;
&lt;li&gt;consider a richer response contract for consumers that need deeper data than the current preview list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would keep those as deliberate extensions, not assumptions. The current repository does one thing clearly: it demonstrates how to wrap Sportmicro data in a clean FastAPI gateway without overcomplicating the integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The main lesson from this build is that a useful sports data gateway does not have to be large to be valuable. If the configuration is centralized, the client is narrow, the resource mapping is explicit, and the response contract is stable, consumers get something they can actually build against.&lt;/p&gt;

&lt;p&gt;That is the part I would reuse in another integration: keep the provider-specific details at the edge, expose only the resources you truly support, and make empty results and upstream failures behave predictably. For sports APIs, that discipline is often more useful than broad surface area.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Build a NCAA Football Schedule Tracker with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 20 Sep 2026 12:04:48 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-ncaa-football-schedule-tracker-with-sportmicro-19ga</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-ncaa-football-schedule-tracker-with-sportmicro-19ga</guid>
      <description>&lt;p&gt;I built this project because sports API demos often stop at “it renders data” and skip the part that actually matters in a real app: what happens when the payload is partial, empty, or unavailable. For an NCAA football schedule tracker, that’s the interesting problem. A schedule browser only feels trustworthy if it can show a clean table, explain loading, and fail gracefully when the provider doesn’t return what you expected.&lt;/p&gt;

&lt;p&gt;The goal of this repository was to make a small, production-style React app that demonstrates how to use &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; as a real sports data provider without pretending the project is a full sports platform. If you want to inspect the code alongside this write-up, you can &lt;a href="https://github.com/mihailove123/ncaa-football-sportmicro-schedule-tracker" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I set out to prove
&lt;/h2&gt;

&lt;p&gt;The scope is intentionally narrow: browse NCAA football fixtures, filter them by date and keyword, and make the app honest about loading and failure states. That narrowness is the point. It forces the integration decisions to be visible instead of hiding them behind a large feature set.&lt;/p&gt;

&lt;p&gt;The app is built around a few principles I wanted to demonstrate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the API client lives separately from the UI&lt;/li&gt;
&lt;li&gt;provider responses are normalized before rendering&lt;/li&gt;
&lt;li&gt;environment-based configuration keeps secrets out of the repo&lt;/li&gt;
&lt;li&gt;the UI handles loading, empty, and error states explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination makes the project more useful than a toy fetch-and-print example. It’s a practical &lt;strong&gt;ncaa football schedule tracker API&lt;/strong&gt; reference for anyone building with an American football API or a broader sports data API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: keep the Sportmicro boundary explicit
&lt;/h2&gt;

&lt;p&gt;The code is split into three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;src/lib/sportmicroClient.ts&lt;/code&gt; handles API access&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/fixtures.ts&lt;/code&gt; reshapes raw data for display&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/ui/App.tsx&lt;/code&gt; owns page state and rendering&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That boundary is the main architectural decision in the repo. It keeps the Sportmicro-specific details out of the React tree, which matters because API integration tends to get messy as soon as you mix request logic, parsing, and UI state in one component.&lt;/p&gt;

&lt;p&gt;The client module centralizes authentication and fetch behavior. It uses the American football base URL, reads &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; from the environment, and throws a clear error if the key is missing or if Sportmicro responds with a non-2xx status.&lt;/p&gt;

&lt;p&gt;A compact excerpt shows the pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestJson&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getApiKey&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SportmicroError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Sportmicro request failed with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper does three useful things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it keeps the base URL in one place&lt;/li&gt;
&lt;li&gt;it injects the API key consistently&lt;/li&gt;
&lt;li&gt;it turns provider errors into a message the UI can surface directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the sort of boundary that pays off later if the provider changes shape. The React code does not need to know how request headers are built or how query params are encoded.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Sportmicro is integrated
&lt;/h2&gt;

&lt;p&gt;This app uses three Sportmicro endpoints in the current implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /leagues&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /teams-by-league&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /matches-by-date-league&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client exposes a function for each endpoint, which is a sensible fit for a small schedule browser. The UI loads the three data sets together when the selected date changes, then combines them into one view.&lt;/p&gt;

&lt;p&gt;A few implementation choices are worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getUpcomingMatches(date)&lt;/code&gt; sends &lt;code&gt;league_id=eq.ncaaf&lt;/code&gt; and optionally filters by date&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getTeamsByLeague(leagueId)&lt;/code&gt; requests team data for the chosen league&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getLeagues()&lt;/code&gt; fetches a short league list for the summary cards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;code&gt;App.tsx&lt;/code&gt;, those calls are coordinated with &lt;code&gt;Promise.all&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;leagueData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;teamData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matchData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;getLeagues&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;getTeamsByLeague&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_LEAGUE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;getUpcomingMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this because the screen stays coherent. Instead of rendering one section immediately and letting the rest trickle in, the app waits until the core data is ready. That makes the page feel more deliberate and easier to reason about.&lt;/p&gt;

&lt;p&gt;The authentication path is also intentionally simple: the repo includes &lt;code&gt;.env.example&lt;/code&gt;, and the key lives in &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;. That keeps secrets out of the repository and makes the setup flow obvious for anyone cloning the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning raw matches into UI-ready fixtures
&lt;/h2&gt;

&lt;p&gt;The second layer is &lt;code&gt;src/lib/fixtures.ts&lt;/code&gt;, which handles normalization and filtering. This file does the unglamorous work that makes the UI simple.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;normalizeTeams()&lt;/code&gt; turns the team list into a lookup map&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolveFixtures()&lt;/code&gt; attaches &lt;code&gt;homeTeam&lt;/code&gt; and &lt;code&gt;awayTeam&lt;/code&gt; objects to each match&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filterFixtures()&lt;/code&gt; narrows the list by date and search text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters because raw API data is rarely shaped for the exact screen you want. In this project, a match may arrive with missing team names or timestamps, so the UI needs sensible fallbacks. The normalized fixture model gives the component display-friendly values like &lt;code&gt;displayDate&lt;/code&gt;, &lt;code&gt;displayTime&lt;/code&gt;, and &lt;code&gt;statusLabel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The filter logic shows the kind of practical compromise I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;teamText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search feature only needs to support team names and fixture IDs, so the helper keeps that logic outside the component tree. That means the React layer stays focused on state and rendering, while the data layer handles the awkward parts of partial provider responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the UI does, and why it stays small
&lt;/h2&gt;

&lt;p&gt;The interface is deliberately modest. It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a date picker&lt;/li&gt;
&lt;li&gt;a search field&lt;/li&gt;
&lt;li&gt;a fixtures table&lt;/li&gt;
&lt;li&gt;three summary cards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s enough to demonstrate the Sportmicro integration without turning the project into a fake sports portal. The application state in &lt;code&gt;src/ui/App.tsx&lt;/code&gt; stays readable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;selectedDate&lt;/code&gt; defaults to today&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search&lt;/code&gt; filters by team name or fixture ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;isLoading&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; control the screen state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;leagues&lt;/code&gt;, &lt;code&gt;teams&lt;/code&gt;, and &lt;code&gt;fixtures&lt;/code&gt; store the fetched data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful part of the UI is how explicitly it handles the three main states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loading&lt;/strong&gt;: show a loading message while the requests are in flight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error&lt;/strong&gt;: surface the message from &lt;code&gt;SportmicroError&lt;/code&gt;, including the missing-key case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty&lt;/strong&gt;: explain that no fixtures may exist for the chosen date or filter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That empty state is important. Sports APIs often return valid but sparse data, and the app doesn’t invent records or pretend something is broken just because the date has no matches.&lt;/p&gt;

&lt;p&gt;The table itself stays minimal. Each row shows the matchup, date, time, and status. If team data is missing, the UI falls back to the team ID or an “unavailable” label instead of crashing or hiding the row. That’s exactly the kind of defensive rendering I want in a live API demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project tree and local setup
&lt;/h2&gt;

&lt;p&gt;The source layout is compact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  lib/
    fixtures.ts
    sportmicroClient.ts
  ui/
    App.tsx
  main.tsx
  styles.css
  types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the repository is standard Vite and TypeScript scaffolding, plus tests and build configuration.&lt;/p&gt;

&lt;p&gt;Local setup is supported by the repository, so these steps are safe to follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install dependencies with &lt;code&gt;npm install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add your Sportmicro API key as &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run the app with &lt;code&gt;npm run dev&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository also supports build and test commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm test&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because it shows the project is not just a live demo with a UI shell. The source is set up to compile, build, and test the integration layer in a repeatable way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and trade-offs
&lt;/h2&gt;

&lt;p&gt;I didn’t approach this as a “solve everything” sports app. The design constraints were more practical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the provider boundary explicit&lt;/li&gt;
&lt;li&gt;avoid hardcoding secrets&lt;/li&gt;
&lt;li&gt;handle partial or missing fields honestly&lt;/li&gt;
&lt;li&gt;stay focused on schedule browsing instead of live scoring, standings, or player analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those trade-offs show up in the code. The app uses a limited set of endpoints and a single league filter, and it prefers clear fallback text over inventing data. That keeps the implementation understandable and keeps the UI aligned with what the provider actually returns.&lt;/p&gt;

&lt;p&gt;Another trade-off is that the app uses the current data shape from Sportmicro directly, with just enough normalization to make the interface stable. That’s a good fit for a small reference project, even if a larger production app would likely add deeper validation and caching around the API boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d improve next
&lt;/h2&gt;

&lt;p&gt;A few future improvements stand out naturally from the current codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stronger runtime validation of Sportmicro responses&lt;/li&gt;
&lt;li&gt;more detailed fixture status presentation&lt;/li&gt;
&lt;li&gt;additional NCAA-focused filters&lt;/li&gt;
&lt;li&gt;pagination if the selected date returns a larger fixture list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are all reasonable next steps, but I’d keep the same architecture if I added them. The client should stay the only place that knows how to talk to Sportmicro, while the UI should remain focused on state and rendering.&lt;/p&gt;

&lt;p&gt;If I extended the project further, I’d also consider making the filter experience a little richer without expanding the scope too far. The current version proves the data flow; the next step would be refining how a developer inspects and navigates that data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The main lesson from this build is simple: a good sports API integration is less about fancy UI and more about treating the provider boundary seriously. In this project, the API client is isolated, the fetched data is normalized, and the UI is honest about loading and empty states.&lt;/p&gt;

&lt;p&gt;That combination makes the app useful as both a schedule tracker and a reference implementation. If you’re building against &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt;, the pattern here should translate well: keep the request layer small, normalize the response once, and let the UI render only the data it can trust.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>opensource</category>
      <category>football</category>
    </item>
    <item>
      <title>Building a Football Fixture MCP Server with Sportmicro API</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sat, 19 Sep 2026 19:52:59 +0000</pubDate>
      <link>https://dev.to/mihailove123/building-a-football-fixture-mcp-server-with-sportmicro-api-3bbm</link>
      <guid>https://dev.to/mihailove123/building-a-football-fixture-mcp-server-with-sportmicro-api-3bbm</guid>
      <description>&lt;p&gt;I built this project around a simple problem: football fixture data is easy to query in isolation, but hard to expose cleanly to AI clients without turning the integration into a mess of loosely validated endpoints. My goal with this repository was to create a read-only Model Context Protocol server that lets an MCP client ask grounded questions about football fixtures, match details, incidents, and lineups through the Sportmicro API, while keeping the surface area intentionally small.&lt;/p&gt;

&lt;p&gt;That shape matters. Instead of trying to mirror the whole API, I focused on a narrow research layer: enough tools to answer fixture questions reliably, but not so many that the server becomes difficult to reason about or trust. The result is a TypeScript MCP server built with &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;, &lt;code&gt;zod&lt;/code&gt;, and a typed Sportmicro client.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mihailove123/football-fixture-research-sportmicro-mcp" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture I used
&lt;/h2&gt;

&lt;p&gt;The implementation is split into three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server.ts&lt;/code&gt; defines the MCP tools and validates tool input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sportmicro-client.ts&lt;/code&gt; handles HTTP requests to Sportmicro&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test/server.test.ts&lt;/code&gt; checks validation and error handling without needing a live API call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation keeps the MCP layer thin. The server is responsible for turning tool calls into structured responses, but it does not know how to build URLs or authenticate requests. The client owns those HTTP details. That line between “protocol” and “provider integration” is what keeps the code maintainable.&lt;/p&gt;

&lt;p&gt;The data flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An MCP client calls one of the registered tools.&lt;/li&gt;
&lt;li&gt;The server validates the input with &lt;code&gt;zod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Sportmicro client sends the request with the API key as a Bearer token.&lt;/li&gt;
&lt;li&gt;The server returns structured text so the caller can distinguish success from failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A small detail I appreciate here is that the server is intentionally read-only. It does not mutate any Sportmicro data, and it only exposes the documented football endpoints needed for fixture research. That keeps the contract predictable for downstream clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a narrow MCP surface works here
&lt;/h2&gt;

&lt;p&gt;I’ve found that MCP servers can grow too quickly if you expose every upstream endpoint just because it exists. This project takes the opposite approach: expose only the tools that match the actual research workflow.&lt;/p&gt;

&lt;p&gt;In this repository, that means five tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;search_matches_by_date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search_matches_by_date_and_league&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_match_by_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_match_incidents&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_match_lineups&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That small tool set is enough to answer common football research questions without encouraging speculative behavior. It also makes the server easier to validate. For example, &lt;code&gt;search_matches_by_date&lt;/code&gt; requires a date in &lt;code&gt;YYYY-MM-DD&lt;/code&gt; format, while &lt;code&gt;search_matches_by_date_and_league&lt;/code&gt; adds a league ID and optional pagination fields. Inputs are checked before any upstream request is made, which is exactly what I want in a server that sits between an AI client and a third-party API.&lt;/p&gt;

&lt;p&gt;The Sportmicro integration itself is equally focused. The client points at the football API and maps each operation to a documented endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/matches-by-date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/matches-by-date-league&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/matches&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/matches-incidents&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/matches-lineups&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That decision matters because it avoids inventing fields or behavior that aren’t present in the provider documentation. When you’re building an AI-facing integration, being precise is more valuable than being broad.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the implementation flows
&lt;/h2&gt;

&lt;p&gt;The core implementation lives in &lt;code&gt;src/server.ts&lt;/code&gt;. I like how compact it is, because the code reveals the actual contract without a lot of framework noise.&lt;/p&gt;

&lt;p&gt;A useful pattern here is the &lt;code&gt;structuredPayload&lt;/code&gt; and &lt;code&gt;errorPayload&lt;/code&gt; pair. Both return JSON text wrapped in MCP content blocks, which keeps the output machine-readable while still being easy for a client to inspect. The callback wrapper validates input, runs the handler, and converts exceptions into tool-level errors.&lt;/p&gt;

&lt;p&gt;Here’s the part that captures that flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildToolCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;structuredPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I like about this approach is that it keeps all the tool-specific plumbing in one place. The actual tool registrations become declarative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define an input schema&lt;/li&gt;
&lt;li&gt;connect it to a client method&lt;/li&gt;
&lt;li&gt;register the tool with the MCP server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pattern is repeated for each tool, so the file stays readable even as the server supports multiple research operations.&lt;/p&gt;

&lt;p&gt;The client in &lt;code&gt;src/sportmicro-client.ts&lt;/code&gt; follows the same principle. It builds a base URL, attaches the API key as &lt;code&gt;Authorization: Bearer ...&lt;/code&gt;, and converts non-2xx responses into a custom &lt;code&gt;SportmicroClientError&lt;/code&gt;. That gives the server a clean way to distinguish API failures from validation problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project structure at a glance
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally small, which helps a lot when you’re working on a protocol adapter rather than a full product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  index.ts               Entry point export
  server.ts              MCP server and tool registration
  sportmicro-client.ts   Typed Sportmicro HTTP client
  test/                  Node test files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the repository supports that core:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;README.md&lt;/code&gt; documents setup and usage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt; shows the required &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;package.json&lt;/code&gt; defines the TypeScript build and Node test flow&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/devto.md&lt;/code&gt; contains an article draft for the project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entry point is also minimal. &lt;code&gt;src/index.ts&lt;/code&gt; just re-exports &lt;code&gt;runServer&lt;/code&gt;, and the server starts with stdio transport when executed directly. That is exactly what I want for MCP: a small executable surface, not a lot of bootstrapping code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and local setup
&lt;/h2&gt;

&lt;p&gt;The repository supports a simple local workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; in the environment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node dist/index.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those commands are backed by the project files, so this isn’t speculative setup. The tests use Node’s built-in test runner and are wired to compile before execution through &lt;code&gt;pretest&lt;/code&gt;, which means the compiled output is what gets tested.&lt;/p&gt;

&lt;p&gt;The tests are also practical. They don’t try to hit the live Sportmicro API. Instead, they verify two things the repository can reliably check on its own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;malformed tool input is rejected before any client call&lt;/li&gt;
&lt;li&gt;upstream Sportmicro errors are turned into actionable tool responses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives confidence in the contract without depending on network availability or external fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and trade-offs
&lt;/h2&gt;

&lt;p&gt;I’d frame this project more as a set of design constraints than a story of runtime surprises. The code clearly reflects a few trade-offs I had to make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only by design:&lt;/strong&gt; useful for research, but it means the server is intentionally limited in scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small tool surface:&lt;/strong&gt; easier to trust and test, but it does not attempt to cover the full Sportmicro API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured JSON text responses:&lt;/strong&gt; simple for MCP clients to parse, though not as rich as a custom resource model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment-based API key handling:&lt;/strong&gt; secure and portable, but it requires setup before the server can run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s also a subtle constraint in the client implementation: it only sends query parameters when values are present. That keeps requests tidy, but it also means the server’s schemas need to stay aligned with what the API expects. In a narrow integration like this, that’s a good trade-off because it forces clarity instead of hidden behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d improve next
&lt;/h2&gt;

&lt;p&gt;A few next steps stand out as sensible future improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add richer match-detail tools where the Sportmicro documentation supports them&lt;/li&gt;
&lt;li&gt;include more explicit freshness metadata in tool responses&lt;/li&gt;
&lt;li&gt;add a small integration-test harness that mocks Sportmicro responses&lt;/li&gt;
&lt;li&gt;make the output shape even more descriptive for clients that want to inspect match data programmatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’d keep those improvements aligned with the same principle as the current implementation: only add what can be documented, validated, and supported cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;What I like most about this project is that it treats an MCP server as an integration boundary, not a feature dump. The server is small on purpose, the validation is explicit, and the Sportmicro client owns the API details cleanly. That makes the code easier to trust, easier to test, and easier to extend without breaking the contract.&lt;/p&gt;

&lt;p&gt;If you’re building a TypeScript MCP server for an external API, this is the pattern I’d recommend: keep the tool surface narrow, validate early, return structured errors, and let the provider client handle the HTTP mechanics. For a read-only football fixture research server, that combination is enough to stay focused while still being useful.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>mcp</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Build a NBA Game Odds Monitor with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Fri, 18 Sep 2026 20:12:35 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-nba-game-odds-monitor-with-sportmicro-1f0</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-nba-game-odds-monitor-with-sportmicro-1f0</guid>
      <description>&lt;p&gt;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 &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; into a React + TypeScript app without hiding the messy parts of real API usage.&lt;/p&gt;

&lt;p&gt;The repository is a small but practical example of a &lt;strong&gt;nba game odds monitor API&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mihailove123/nba-game-odds-monitor-sportmicro-api" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I set out to build
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/odds/bookmakers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/odds/moneyline&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I also wanted the structure to feel production-style even though the app is small. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the UI does not build API URLs directly&lt;/li&gt;
&lt;li&gt;the API key is read from environment variables&lt;/li&gt;
&lt;li&gt;the app handles loading, empty, and error states explicitly&lt;/li&gt;
&lt;li&gt;the response is normalized into a local UI-friendly shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those choices keep the demo honest. They also make it easier to extend later without rewriting the whole app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: keep the Sportmicro boundary isolated
&lt;/h2&gt;

&lt;p&gt;The cleanest part of the codebase is the separation between the UI and the provider-specific client. The data flow is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI -&amp;gt; app state -&amp;gt; Sportmicro client -&amp;gt; network request -&amp;gt; Sportmicro API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That boundary is implemented in &lt;code&gt;src/sportmicroClient.ts&lt;/code&gt;, while &lt;code&gt;src/App.tsx&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The client module does three important things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It chooses a base URL, defaulting to &lt;code&gt;https://basketball.sportmicro.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It reads &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; from the environment.&lt;/li&gt;
&lt;li&gt;It wraps fetch calls into typed helpers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the core pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://basketball.sportmicro.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBaseUrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_SPORTMICRO_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestJson&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SPORTMICRO_API_KEY&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SPORTMICRO_API_KEY is not set. Add it to your environment before refreshing odds.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Sportmicro request failed with HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;fetchBookmakers()&lt;/code&gt; and &lt;code&gt;fetchOddsForMatch()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation flow in the UI
&lt;/h2&gt;

&lt;p&gt;The main application logic lives in &lt;code&gt;src/App.tsx&lt;/code&gt;, and the flow is deliberately simple.&lt;/p&gt;

&lt;p&gt;First, the app loads bookmaker metadata on mount:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useEffect&lt;/code&gt; calls &lt;code&gt;fetchBookmakers()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the result populates a local &lt;code&gt;bookmakers&lt;/code&gt; state array&lt;/li&gt;
&lt;li&gt;if that request fails, the app quietly leaves the filter list empty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;After the fetch succeeds, the app normalizes each odds row into a presentation-friendly shape. The normalization happens in a local helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeMarket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OddsMarket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;MarketRow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmaker_id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmaker_name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market_name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;moneyline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;bookmaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmaker_name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmaker_id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown bookmaker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market_name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Moneyline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown selection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;odd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;odd&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;odd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;odd&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;live&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_live&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Live&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pre-match&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this pattern because it does two jobs at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it shields the UI from missing API fields&lt;/li&gt;
&lt;li&gt;it creates a consistent display model for the table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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: &lt;code&gt;Unknown&lt;/code&gt;, &lt;code&gt;—&lt;/code&gt;, or a fallback label.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project structure at a glance
&lt;/h2&gt;

&lt;p&gt;The tree is compact enough to understand quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The TypeScript models are intentionally small too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ApiError&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OddsMarket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bookmaker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MarketRow&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and how I verified the project shape
&lt;/h2&gt;

&lt;p&gt;The repository supports local development with standard Node tooling. The README documents the expected flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;set &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;npm run dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;optionally run &lt;code&gt;npm run build&lt;/code&gt; and &lt;code&gt;npm run test&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matches the scripts in &lt;code&gt;package.json&lt;/code&gt;, which include &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;preview&lt;/code&gt;, and &lt;code&gt;test&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;I also like that the test coverage is modest and honest. The existing test in &lt;code&gt;src/app.test.tsx&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and trade-offs
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;Those are all sensible decisions for an educational repository. They reduce complexity without pretending the problem is simpler than it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

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

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>opensource</category>
      <category>basketball</category>
    </item>
    <item>
      <title>How to Build a Fantasy Football App Starter with Next.js</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:11:07 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-fantasy-football-app-starter-with-nextjs-1nij</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-fantasy-football-app-starter-with-nextjs-1nij</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a Fantasy Football App Starter with Next.js
&lt;/h1&gt;

&lt;p&gt;Fantasy football is one of those ideas that sounds simple until you actually start wiring it to real sports data.&lt;/p&gt;

&lt;p&gt;You need player lists, teams, fixtures, statistics, and a UI that can survive missing fields or temporarily unavailable endpoints. That is exactly why I like starter kits: they reduce the amount of architectural decision-making you need to do before you can ship something useful.&lt;/p&gt;

&lt;p&gt;In this walkthrough, I built a &lt;strong&gt;Fantasy Football Starter Kit&lt;/strong&gt; with &lt;strong&gt;Next.js&lt;/strong&gt;, &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and &lt;strong&gt;Tailwind CSS&lt;/strong&gt;, powered by the &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; football API. The goal is not to create a complete commercial fantasy platform. Instead, the project is meant to be a practical foundation for developers who want to explore a &lt;strong&gt;Fantasy Football API&lt;/strong&gt; integration without starting from zero.&lt;/p&gt;

&lt;p&gt;The source code is available here: &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;https://github.com/&lt;/a&gt;/sportmicro-fantasy-football-starter-kit&lt;/p&gt;

&lt;p&gt;For API reference, see the Sportmicro documentation: &lt;a href="https://docs.sportmicro.com/" rel="noopener noreferrer"&gt;https://docs.sportmicro.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build a starter kit instead of a full platform?
&lt;/h2&gt;

&lt;p&gt;A full fantasy product usually needs authentication, leagues, user management, drafts, transfers, scoring rules, live updates, and a lot of business logic. That is a big project.&lt;/p&gt;

&lt;p&gt;A starter kit is more valuable when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validate an idea quickly&lt;/li&gt;
&lt;li&gt;learn how a football data API behaves&lt;/li&gt;
&lt;li&gt;prototype a UI around player statistics&lt;/li&gt;
&lt;li&gt;experiment with fantasy points calculations&lt;/li&gt;
&lt;li&gt;create a foundation you can grow later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This repository keeps the scope deliberately small so the architecture stays understandable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the project includes
&lt;/h2&gt;

&lt;p&gt;The app shows a few core fantasy-football building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;football player listing&lt;/li&gt;
&lt;li&gt;player cards&lt;/li&gt;
&lt;li&gt;team information&lt;/li&gt;
&lt;li&gt;upcoming fixtures&lt;/li&gt;
&lt;li&gt;player statistics&lt;/li&gt;
&lt;li&gt;fantasy squad builder&lt;/li&gt;
&lt;li&gt;selected team view&lt;/li&gt;
&lt;li&gt;an example fantasy points calculation&lt;/li&gt;
&lt;li&gt;responsive layout patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is designed to feel like a real developer starting point, not a polished consumer product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sportmicro integration
&lt;/h2&gt;

&lt;p&gt;Sportmicro provides the football data. In this starter kit, the API client is intentionally compact and only uses documented endpoints from the supplied reference.&lt;/p&gt;

&lt;p&gt;The current client fetches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/players&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/teams&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/matches-by-date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/players-statistics&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means the code demonstrates a realistic integration pattern without inventing endpoints or pretending to support features that are not in the docs.&lt;/p&gt;

&lt;p&gt;A nice detail from a reliability standpoint is the fallback pattern. If a request fails, the app can still render using local sample data. That is useful during development and also helps communicate to users that the app is a starter kit, not a locked-down production system.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple API client pattern
&lt;/h2&gt;

&lt;p&gt;I like to keep the API client in one place so the rest of the app can stay focused on UI composition.&lt;/p&gt;

&lt;p&gt;The core idea is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;build a URL from the Sportmicro base URL&lt;/li&gt;
&lt;li&gt;add query parameters only when needed&lt;/li&gt;
&lt;li&gt;send the request with &lt;code&gt;fetch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;return JSON on success&lt;/li&gt;
&lt;li&gt;throw on non-OK responses&lt;/li&gt;
&lt;li&gt;fall back to local data in the page layer when needed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That pattern makes it easy to add more documented endpoints later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example request helper
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// append params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Sportmicro request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact production code in the repository also wires the &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; environment variable through request headers so credentials never need to be hardcoded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment configuration
&lt;/h2&gt;

&lt;p&gt;The repo includes an &lt;code&gt;.env.example&lt;/code&gt; file so developers know what to configure locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SPORTMICRO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;SPORTMICRO_API_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://football.sportmicro.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a small but important habit: starter kits should make setup obvious before they make anything flashy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fantasy points calculation
&lt;/h2&gt;

&lt;p&gt;A fantasy app is not useful until it can translate football data into points. The repository includes a tiny scoring function that weights goals, assists, ratings, and cards.&lt;/p&gt;

&lt;p&gt;This is intentionally simple.&lt;/p&gt;

&lt;p&gt;Why? Because the point is to show where scoring logic lives, not to pretend there is one universal fantasy scoring system.&lt;/p&gt;

&lt;p&gt;If you want to extend the starter kit, you could replace the scoring function with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a league-specific rule set&lt;/li&gt;
&lt;li&gt;custom captain bonuses&lt;/li&gt;
&lt;li&gt;lineup constraints&lt;/li&gt;
&lt;li&gt;position-based scoring&lt;/li&gt;
&lt;li&gt;live match adjustments&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  UI structure
&lt;/h2&gt;

&lt;p&gt;The app uses reusable React components so you can grow the project without rewriting the page from scratch.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PlayerCard&lt;/code&gt; renders individual footballers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SquadBuilder&lt;/code&gt; renders the selected squad area&lt;/li&gt;
&lt;li&gt;the page composes the overall dashboard experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes it easier to add search, filters, sorting, or saved squads later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;The most obvious next steps are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real player search input&lt;/li&gt;
&lt;li&gt;position filtering controls&lt;/li&gt;
&lt;li&gt;selectable squad state&lt;/li&gt;
&lt;li&gt;match-centered projections using Sportmicro’s documented projection endpoints&lt;/li&gt;
&lt;li&gt;pagination and caching improvements&lt;/li&gt;
&lt;li&gt;stronger empty-state UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are working with live sports data, one thing worth paying attention to is consistency across paginated endpoints. Data can shift while a user browses, so client-side handling matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;A few practical lessons from building this starter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;small API clients are easier to trust&lt;/li&gt;
&lt;li&gt;fallback data keeps the app runnable&lt;/li&gt;
&lt;li&gt;reusable components make the UI easier to extend&lt;/li&gt;
&lt;li&gt;starter kits are more helpful when they show structure, not just screenshots&lt;/li&gt;
&lt;li&gt;it is better to build around documented endpoints only&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;If you want a &lt;strong&gt;football data API&lt;/strong&gt; example that is easy to read and extend, a starter kit is a great place to begin. Sportmicro fits that approach well because it provides a broad football data surface, and the repo can stay focused on integration patterns rather than trying to solve every fantasy feature at once.&lt;/p&gt;

&lt;p&gt;If you want to explore the project, start here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;https://github.com/&lt;/a&gt;/sportmicro-fantasy-football-starter-kit&lt;/li&gt;
&lt;li&gt;Sportmicro: &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;https://sportmicro.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.sportmicro.com/" rel="noopener noreferrer"&gt;https://docs.sportmicro.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you build on it, keep the scope honest. The best starter kits are the ones that help the next developer move quickly without hiding the hard parts.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>football</category>
      <category>api</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Building Real-Time Sports Apps with WebSockets with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Thu, 10 Sep 2026 00:51:57 +0000</pubDate>
      <link>https://dev.to/mihailove123/building-real-time-sports-apps-with-websockets-with-sportmicro-1bog</link>
      <guid>https://dev.to/mihailove123/building-real-time-sports-apps-with-websockets-with-sportmicro-1bog</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; fit naturally.&lt;/p&gt;

&lt;p&gt;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, &lt;a href="https://github.com/mihailove123/live-sports-websocket-demo" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I was solving
&lt;/h2&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; to fetch data from Sportmicro’s documented endpoints and then forwards the result back to the UI.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in plain English
&lt;/h2&gt;

&lt;p&gt;There are three moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;React client&lt;/strong&gt; — handles the UI, sport selection, and connection state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js WebSocket server&lt;/strong&gt; — accepts browser connections and relays data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sportmicro&lt;/strong&gt; — provides the sports data from documented REST endpoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client and server communicate with a tiny message schema. The browser sends a &lt;code&gt;subscribe&lt;/code&gt; message when a sport is selected. The server responds with &lt;code&gt;connection&lt;/code&gt;, &lt;code&gt;snapshot&lt;/code&gt;, or &lt;code&gt;error&lt;/code&gt; messages. That keeps the flow predictable and makes the UI logic straightforward.&lt;/p&gt;

&lt;p&gt;The supported sports in this demo are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;football&lt;/li&gt;
&lt;li&gt;basketball&lt;/li&gt;
&lt;li&gt;tennis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the relay pattern mattered
&lt;/h2&gt;

&lt;p&gt;This is one of those cases where the architecture is the feature.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;credentials stay server-side&lt;/li&gt;
&lt;li&gt;the upstream response can be normalized before the client sees it&lt;/li&gt;
&lt;li&gt;reconnect and error handling can live in one place&lt;/li&gt;
&lt;li&gt;the frontend stays focused on rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Sportmicro fits into the flow
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That snapshot contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the selected sport&lt;/li&gt;
&lt;li&gt;a fetch timestamp&lt;/li&gt;
&lt;li&gt;a list of matches&lt;/li&gt;
&lt;li&gt;a list of incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation flow
&lt;/h2&gt;

&lt;p&gt;I like to think of the implementation as four layers: types, transport, data shaping, and presentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Define the shared message contract
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;src/types.ts&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;That pays off immediately in a project like this. The frontend hook knows what a &lt;code&gt;snapshot&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build the WebSocket hook
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;useSportStream&lt;/code&gt; hook in &lt;code&gt;src/websocket/useSportStream.ts&lt;/code&gt; is the client’s communication layer.&lt;/p&gt;

&lt;p&gt;Its responsibilities are intentionally narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;connect to the WebSocket URL&lt;/li&gt;
&lt;li&gt;send a &lt;code&gt;subscribe&lt;/code&gt; message when the socket opens&lt;/li&gt;
&lt;li&gt;listen for &lt;code&gt;snapshot&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; messages&lt;/li&gt;
&lt;li&gt;handle reconnects after disconnects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;code&gt;reconnecting&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Shape the UI around live state
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;App.tsx&lt;/code&gt; component is where the project becomes visible.&lt;/p&gt;

&lt;p&gt;It keeps the selected sport in local state, passes that into &lt;code&gt;useSportStream&lt;/code&gt;, and then renders three main areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a hero section with the project framing&lt;/li&gt;
&lt;li&gt;a toolbar with sport selection and connection state&lt;/li&gt;
&lt;li&gt;a two-column content area with matches and events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The match rendering includes a few thoughtful details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a helper to build a human-readable title&lt;/li&gt;
&lt;li&gt;a helper to format score lines&lt;/li&gt;
&lt;li&gt;a fallback empty state when there are no live matches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep the server as the secure middle layer
&lt;/h3&gt;

&lt;p&gt;The server files are the part of the app I’d expect another engineer to extend first.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;, fetch data from documented endpoints only, and stream normalized snapshots back to the browser.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick look at the structure
&lt;/h2&gt;

&lt;p&gt;Here’s the shape of the project as it stands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tree tells the whole story: a small server, a focused React app, and shared types that keep the transport contract explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I liked about the implementation choices
&lt;/h2&gt;

&lt;p&gt;There are a few design decisions here that make the project feel well-scoped.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup, based on the repository docs
&lt;/h2&gt;

&lt;p&gt;The repository documents a straightforward local setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18+ is required&lt;/li&gt;
&lt;li&gt;an active Sportmicro API key is needed&lt;/li&gt;
&lt;li&gt;dependencies are installed with &lt;code&gt;npm install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt; is copied to &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the app is started with &lt;code&gt;npm run dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the client can be built with &lt;code&gt;npm run build:client&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the full production build uses &lt;code&gt;npm run build&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documented environment variables are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PORT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLIENT_ORIGIN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VITE_WS_URL&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sensible next improvements
&lt;/h2&gt;

&lt;p&gt;If I were extending this project, I’d keep the same architecture and improve it incrementally.&lt;/p&gt;

&lt;p&gt;A few natural next steps stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add clearer loading states for each sport transition&lt;/li&gt;
&lt;li&gt;surface richer metadata in the match cards if the upstream response supports it&lt;/li&gt;
&lt;li&gt;improve the event log grouping or ordering&lt;/li&gt;
&lt;li&gt;make the empty states more sport-aware&lt;/li&gt;
&lt;li&gt;add more tests around socket state changes and error rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this build is that real-time doesn’t have to mean complicated.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; powered sports dashboard: the browser subscribes, the server relays, and the UI updates as soon as new snapshots arrive.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>websocket</category>
      <category>node</category>
      <category>react</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Build a Football Odds Tracker with React</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 30 Aug 2026 11:29:16 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-football-odds-tracker-with-react-7f0</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-football-odds-tracker-with-react-7f0</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That is what led me to this project, a React + TypeScript app that uses &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mihailove123/football-odds-tracker" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I set out to solve
&lt;/h2&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So the project focuses on a few specific outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upcoming football matches&lt;/li&gt;
&lt;li&gt;league and competition details&lt;/li&gt;
&lt;li&gt;home and away teams&lt;/li&gt;
&lt;li&gt;kickoff times&lt;/li&gt;
&lt;li&gt;bookmaker odds for the full-time result market&lt;/li&gt;
&lt;li&gt;a simple league filter&lt;/li&gt;
&lt;li&gt;a manual refresh action&lt;/li&gt;
&lt;li&gt;loading and error states&lt;/li&gt;
&lt;li&gt;fallback sample data when live requests fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sportmicro fits the architecture
&lt;/h2&gt;

&lt;p&gt;The project uses the Sportmicro football API directly from the frontend, with a dedicated client in &lt;code&gt;src/apiClient.ts&lt;/code&gt;. The app only relies on two documented endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /matches&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /odds/full-time-results&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The API base URL used in the project is &lt;code&gt;https://football.sportmicro.com&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project structure I worked with
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally compact. The core files are easy to map mentally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  App.tsx
  apiClient.ts
  main.tsx
  mockData.ts
  styles.css
  types.ts
  test/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure is simple, but it covers the main layers of the app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apiClient.ts&lt;/code&gt; isolates Sportmicro requests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;types.ts&lt;/code&gt; defines the local data model&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mockData.ts&lt;/code&gt; provides sample matches, leagues, and odds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;App.tsx&lt;/code&gt; handles the user interface and data flow&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main.tsx&lt;/code&gt; mounts the React app&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test/&lt;/code&gt; contains tests for the client and shell rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation flow
&lt;/h2&gt;

&lt;p&gt;The app starts in &lt;code&gt;src/main.tsx&lt;/code&gt;, where React renders the root &lt;code&gt;App&lt;/code&gt; component and loads &lt;code&gt;styles.css&lt;/code&gt;. Nothing unusual there, which is a good sign. The important work happens inside &lt;code&gt;App.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) I defined the data model first
&lt;/h3&gt;

&lt;p&gt;The TypeScript interfaces in &lt;code&gt;src/types.ts&lt;/code&gt; keep the app grounded in the fields the UI actually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;League&lt;/code&gt; has an &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;, with a couple of optional metadata fields&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Match&lt;/code&gt; includes league ID, league name, team names, start time, status, and bookmaker count&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FullTimeResultOdds&lt;/code&gt; represents odds for a single match and bookmaker&lt;/li&gt;
&lt;li&gt;each outcome can carry a name and an optional price&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) I centralized requests in one helper
&lt;/h3&gt;

&lt;p&gt;The request helper in &lt;code&gt;src/apiClient.ts&lt;/code&gt; is small, but it does the important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it builds URLs against the Sportmicro football API base&lt;/li&gt;
&lt;li&gt;it appends query parameters in a reusable way&lt;/li&gt;
&lt;li&gt;it sends an &lt;code&gt;Accept: application/json&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;it adds a Bearer token when &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; is available&lt;/li&gt;
&lt;li&gt;it throws a typed &lt;code&gt;SportmicroApiError&lt;/code&gt; when the response is not OK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) I used local sample data as a safety net
&lt;/h3&gt;

&lt;p&gt;The mock data in &lt;code&gt;src/mockData.ts&lt;/code&gt; includes a few leagues, matches, and odds records. This is not a fake product layer; it is a fallback layer.&lt;/p&gt;

&lt;p&gt;That fallback is important for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it keeps the app useful when live API requests fail&lt;/li&gt;
&lt;li&gt;it makes the interface easier to understand before a real API key is configured&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) I built the refresh flow around live requests
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;refreshData&lt;/code&gt; function in &lt;code&gt;App.tsx&lt;/code&gt; is the main data-loading path. It sets a loading state, clears previous errors, and then requests both &lt;code&gt;/matches&lt;/code&gt; and &lt;code&gt;/odds/full-time-results&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the requests succeed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live matches replace the current match list&lt;/li&gt;
&lt;li&gt;live odds replace the current odds list&lt;/li&gt;
&lt;li&gt;empty responses fall back to sample data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the requests fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the app shows a clear message&lt;/li&gt;
&lt;li&gt;sample data is restored&lt;/li&gt;
&lt;li&gt;the interface stays usable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) I added filtering and visual separation
&lt;/h3&gt;

&lt;p&gt;The interface has a simple league filter powered by local state. It filters the displayed matches by &lt;code&gt;league_id&lt;/code&gt;, with an &lt;code&gt;all&lt;/code&gt; option for the full list. It is a small interaction, but it makes the screen more useful immediately.&lt;/p&gt;

&lt;p&gt;The odds are rendered in a separate section via a dedicated &lt;code&gt;OddsCard&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The render logic also deals with partial values carefully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Time unavailable&lt;/code&gt; when kickoff time is missing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Competition unavailable&lt;/code&gt; when the league name is absent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;N/A&lt;/code&gt; for missing odds prices&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;upcoming&lt;/code&gt; when a status is not provided&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That kind of defensive rendering is easy to overlook, but it is exactly what makes a tracker feel reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the UI is doing well
&lt;/h2&gt;

&lt;p&gt;The app is not trying to imitate a full sportsbook. Instead, it stays focused on a few useful views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a header that explains the purpose&lt;/li&gt;
&lt;li&gt;a manual refresh button&lt;/li&gt;
&lt;li&gt;a league filter sidebar&lt;/li&gt;
&lt;li&gt;match cards for upcoming fixtures&lt;/li&gt;
&lt;li&gt;odds cards for the full-time result market&lt;/li&gt;
&lt;li&gt;an API usage note at the bottom&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the core pieces
&lt;/h2&gt;

&lt;p&gt;The repository includes tests for the API client and for the app shell.&lt;/p&gt;

&lt;p&gt;The API client test checks two important behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it builds the correct request URL&lt;/li&gt;
&lt;li&gt;it throws a typed error when the response is not OK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives confidence that the Sportmicro integration layer behaves as expected.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup, based on the repository
&lt;/h2&gt;

&lt;p&gt;The repository includes a standard Vite setup and documents a straightforward local workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
npm run build
npm run &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also expects a local &lt;code&gt;.env&lt;/code&gt; file based on &lt;code&gt;.env.example&lt;/code&gt;, with &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; set for authenticated requests.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sensible next improvements
&lt;/h2&gt;

&lt;p&gt;Because the project is already small and focused, the next improvements should stay grounded in the same design.&lt;/p&gt;

&lt;p&gt;A few directions make sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add more documented Sportmicro views, such as leagues, standings, teams, or matches by date&lt;/li&gt;
&lt;li&gt;expand the odds display beyond the full-time result market if the documentation supports it&lt;/li&gt;
&lt;li&gt;make the filter controls richer without adding clutter&lt;/li&gt;
&lt;li&gt;improve empty-state messaging for fixtures and odds separately&lt;/li&gt;
&lt;li&gt;keep extending the test coverage around request handling and rendering paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>api</category>
      <category>football</category>
    </item>
    <item>
      <title>I Built a Football Livescore App That Still Works Without an API Key</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 14 Jun 2026 14:34:44 +0000</pubDate>
      <link>https://dev.to/mihailove123/i-built-a-football-livescore-app-that-still-works-without-an-api-key-19de</link>
      <guid>https://dev.to/mihailove123/i-built-a-football-livescore-app-that-still-works-without-an-api-key-19de</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F068g5rp0x80w5r19b9ie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F068g5rp0x80w5r19b9ie.png" alt="screenshot" width="799" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A football livescore app looks simple until you try to build one properly.&lt;/p&gt;

&lt;p&gt;At first, the idea was straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;show live matches&lt;/li&gt;
&lt;li&gt;let users browse fixtures by date&lt;/li&gt;
&lt;li&gt;organize games by league&lt;/li&gt;
&lt;li&gt;add a match details page&lt;/li&gt;
&lt;li&gt;display team and competition logos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But once I started building it, the real challenge was not rendering a score.&lt;/p&gt;

&lt;p&gt;The harder part was deciding how the data layer should behave when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the API key is missing&lt;/li&gt;
&lt;li&gt;the external API is unavailable&lt;/li&gt;
&lt;li&gt;the response shape changes&lt;/li&gt;
&lt;li&gt;a team logo does not exist&lt;/li&gt;
&lt;li&gt;the UI needs to work before the live integration is ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That led me to build &lt;strong&gt;PitchPulse Live&lt;/strong&gt;, a production-style football livescore application using Next.js, TypeScript, Tailwind CSS, and SportMicro football data.&lt;/p&gt;

&lt;p&gt;The part I like most is that the application does not become unusable when no API key is configured.&lt;/p&gt;

&lt;p&gt;It automatically switches to mock data.&lt;/p&gt;

&lt;p&gt;This means someone can clone the repository, run the app, explore the interface, and continue development without first setting up access to an external sports API.&lt;/p&gt;

&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mihailove123/pitchpulse-livescore-ap" rel="noopener noreferrer"&gt;github.com/mihailove123/pitchpulse-livescore-ap&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Wanted to Build
&lt;/h2&gt;

&lt;p&gt;I did not want the project to be only a page with several hardcoded match cards.&lt;/p&gt;

&lt;p&gt;The goal was to create a small but realistic football product with a structure that could later support more advanced features.&lt;/p&gt;

&lt;p&gt;The first version includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a live match scoreboard&lt;/li&gt;
&lt;li&gt;daily fixture browsing&lt;/li&gt;
&lt;li&gt;league filtering&lt;/li&gt;
&lt;li&gt;a league directory&lt;/li&gt;
&lt;li&gt;individual match pages&lt;/li&gt;
&lt;li&gt;team and league logos&lt;/li&gt;
&lt;li&gt;server-rendered data fetching&lt;/li&gt;
&lt;li&gt;automatic mock fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main routes are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/               Live matches and daily fixtures
/leagues        Football league directory
/matches/[id]   Match details
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app is built with the Next.js App Router.&lt;/p&gt;

&lt;p&gt;That allowed me to keep the initial data fetching on the server while still using interactive client components for date selection and league filtering.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;The project uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next.js 16
React 19
TypeScript
Tailwind CSS 4
SportMicro Football API
@sportmicro/endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  app/
    page.tsx
    leagues/
      page.tsx
    matches/
      [id]/
        page.tsx

  components/
    EntityLogo.tsx
    MatchCard.tsx
    MatchList.tsx
    LeagueFilter.tsx
    DateSelector.tsx

  lib/
    sportmicro.ts
    mock-data.ts
    utils.ts

  types/
    sportmicro.ts

public/
  screenshots/
    sc.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision was to keep the football data logic separate from the UI.&lt;/p&gt;

&lt;p&gt;The pages should not need to know how authentication headers are created, how query parameters are built, or how raw API responses are normalized.&lt;/p&gt;

&lt;p&gt;They should only receive app-friendly objects and render them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Problem: External Data Should Not Control the Entire App
&lt;/h2&gt;

&lt;p&gt;When building an API-powered interface, it is tempting to connect the UI directly to the raw response.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home_team&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;away&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;away_team&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works initially, but it creates a tight dependency between the interface and the external API.&lt;/p&gt;

&lt;p&gt;If the API changes a field name, every component using that field may need to change.&lt;/p&gt;

&lt;p&gt;Instead, PitchPulse normalizes the responses inside the data layer.&lt;/p&gt;

&lt;p&gt;The UI consumes internal types such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;homeScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;awayScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;league&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a useful boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External API response
        ↓
Normalization layer
        ↓
Application types
        ↓
React components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The components no longer care where the data came from.&lt;/p&gt;

&lt;p&gt;That becomes especially useful because the same components can render both live API data and local mock data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using &lt;code&gt;@sportmicro/endpoint&lt;/code&gt; as a Query Builder
&lt;/h2&gt;

&lt;p&gt;The project uses the &lt;code&gt;@sportmicro/endpoint&lt;/code&gt; package, but not as a full API client.&lt;/p&gt;

&lt;p&gt;I use it to construct PostgREST-style endpoint paths.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;matches&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;home_team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;away_team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;home_score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;away_score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;ascending&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package handles the query structure.&lt;/p&gt;

&lt;p&gt;The application still sends the actual HTTP request with &lt;code&gt;fetch&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FOOTBALL_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?lang=en`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SPORTMICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this approach because it keeps endpoint construction readable.&lt;/p&gt;

&lt;p&gt;Instead of manually building a long query string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/matches?status_type=eq.live&amp;amp;select=id,start_time,status_type...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the code describes the query step by step.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Wrapper for All Football Requests
&lt;/h2&gt;

&lt;p&gt;The SportMicro integration lives in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/lib/sportmicro.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;building endpoint paths&lt;/li&gt;
&lt;li&gt;attaching the API key&lt;/li&gt;
&lt;li&gt;sending requests&lt;/li&gt;
&lt;li&gt;handling errors&lt;/li&gt;
&lt;li&gt;switching to mock data&lt;/li&gt;
&lt;li&gt;normalizing responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the application calls functions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getLiveFootballMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;getFixturesByDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;getMatchDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;getLeagues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A page does not need to know which endpoint is being used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;liveMatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLiveFootballMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixtures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFixturesByDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchList&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;liveMatches&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchList&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fixtures&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the page focused on presentation.&lt;/p&gt;

&lt;p&gt;The transport logic remains in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Added Mock Mode
&lt;/h2&gt;

&lt;p&gt;The mock fallback became one of the most useful parts of the project.&lt;/p&gt;

&lt;p&gt;Normally, an API-powered repository creates friction for anyone trying to run it.&lt;/p&gt;

&lt;p&gt;They clone the project, install the dependencies, and immediately see an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Missing API key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a great first experience.&lt;/p&gt;

&lt;p&gt;In PitchPulse Live, the application checks whether the environment variable exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SPORTMICRO_API_KEY&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMockMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the key is missing, the data functions return local sample data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLiveFootballMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMockMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mockLiveMatches&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetchLiveMatchesFromApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the setup can be as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mihailove123/pitchpulse-livescore-ap.git

&lt;span class="nb"&gt;cd &lt;/span&gt;pitchpulse-livescore-ap

npm &lt;span class="nb"&gt;install

&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No external account is required to inspect the layout.&lt;/p&gt;

&lt;p&gt;No secret needs to be configured to work on the components.&lt;/p&gt;

&lt;p&gt;The live integration can be enabled later by creating &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPORTMICRO_API_KEY=your_sportmicro_api_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates two useful development modes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mock mode
  UI development
  component testing
  layout work
  demos
  local onboarding

Live mode
  real scores
  real fixtures
  real leagues
  real match details
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same UI works in both modes because both data sources are normalized into the same types.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making Missing Logos Look Intentional
&lt;/h2&gt;

&lt;p&gt;Football data is rarely perfectly complete.&lt;/p&gt;

&lt;p&gt;Some teams have logos.&lt;/p&gt;

&lt;p&gt;Some do not.&lt;/p&gt;

&lt;p&gt;Some image URLs fail.&lt;/p&gt;

&lt;p&gt;Some lower-level leagues may have incomplete branding.&lt;/p&gt;

&lt;p&gt;Rendering a broken image icon makes the whole application look unfinished.&lt;/p&gt;

&lt;p&gt;That is why I added a reusable &lt;code&gt;EntityLogo&lt;/code&gt; component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EntityLogoProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EntityLogo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;EntityLogoProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; logo`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-10 w-10 object-contain"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex h-10 w-10 items-center justify-center rounded-full bg-neutral-800 text-sm font-semibold"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initials&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remote images are loaded from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://images.sportmicro.com/{hash}.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no valid image is available, the component displays initials instead.&lt;/p&gt;

&lt;p&gt;This is a small feature, but it prevents incomplete data from damaging the interface.&lt;/p&gt;

&lt;p&gt;The fallback looks intentional rather than accidental.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Reusable Match Cards
&lt;/h2&gt;

&lt;p&gt;The main reusable UI unit is &lt;code&gt;MatchCard.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A match card needs to support several states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scheduled
Live
Finished
Postponed
Cancelled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scheduled match should emphasize kickoff time.&lt;/p&gt;

&lt;p&gt;A live match should emphasize the score and current status.&lt;/p&gt;

&lt;p&gt;A completed match should make it clear that the score is final.&lt;/p&gt;

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MatchCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isScheduled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded-xl border p-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center justify-between"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Team&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isScheduled&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt; &lt;span class="na"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;formatMatchTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeScore&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayScore&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"block text-xs"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              LIVE
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Team&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the card consumes normalized match data, it can be reused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the homepage&lt;/li&gt;
&lt;li&gt;fixture results&lt;/li&gt;
&lt;li&gt;league pages&lt;/li&gt;
&lt;li&gt;related-match sections&lt;/li&gt;
&lt;li&gt;search results&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Date and League Filtering
&lt;/h2&gt;

&lt;p&gt;The homepage includes controls for browsing fixtures.&lt;/p&gt;

&lt;p&gt;The user can select a date and narrow the list by league.&lt;/p&gt;

&lt;p&gt;The date selector updates the requested day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DateSelector&lt;/span&gt; &lt;span class="na"&gt;selectedDate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The league filter receives the competitions found in the returned fixtures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LeagueFilter&lt;/span&gt;
  &lt;span class="na"&gt;leagues&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;availableLeagues&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;selectedLeague&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;leagueId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filtering logic stays separate from the match card.&lt;/p&gt;

&lt;p&gt;That separation matters because the match component should not decide which matches are visible.&lt;/p&gt;

&lt;p&gt;Its job is only to render one match correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Match Details as a Dynamic Route
&lt;/h2&gt;

&lt;p&gt;Each match links to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/matches/[id]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page reads the dynamic route parameter and loads the details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchPageProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MatchPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MatchPageProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMatchDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchHeader&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchSummary&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current page displays a score snapshot and match summary.&lt;/p&gt;

&lt;p&gt;The route is intentionally designed so it can later support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;incidents&lt;/li&gt;
&lt;li&gt;goals&lt;/li&gt;
&lt;li&gt;cards&lt;/li&gt;
&lt;li&gt;substitutions&lt;/li&gt;
&lt;li&gt;lineups&lt;/li&gt;
&lt;li&gt;statistics&lt;/li&gt;
&lt;li&gt;head-to-head results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is one of the advantages of starting with a clear route and data layer rather than placing everything on one page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Server Rendering Keeps the Pages Simple
&lt;/h2&gt;

&lt;p&gt;The application uses server-rendered data fetching.&lt;/p&gt;

&lt;p&gt;This means pages can request their data before rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LeaguesPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leagues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLeagues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Football Leagues&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LeagueGrid&lt;/span&gt; &lt;span class="na"&gt;leagues&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;leagues&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no initial client-side loading effect.&lt;/p&gt;

&lt;p&gt;There is no empty page waiting for JavaScript to fetch the first result.&lt;/p&gt;

&lt;p&gt;Interactive pieces such as the date selector can still be client components, but the initial football data is rendered on the server.&lt;/p&gt;

&lt;p&gt;This gives the project a clean split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server Components
  data fetching
  page composition
  initial rendering

Client Components
  filters
  date controls
  interactive state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Complete Data Flow
&lt;/h2&gt;

&lt;p&gt;A typical request moves through the project like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User opens the homepage
        ↓
Next.js page calls getLiveFootballMatches()
        ↓
The helper checks for SPORTMICRO_API_KEY
        ↓
No key?
  Return normalized mock data
        ↓
Key exists?
  Build the API path with @sportmicro/endpoint
        ↓
Send the authenticated request with fetch
        ↓
Normalize the response
        ↓
Render MatchList
        ↓
MatchList renders MatchCard components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI never needs separate code for live and mock mode.&lt;/p&gt;

&lt;p&gt;That is the main benefit of normalizing the data before it reaches the components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Project
&lt;/h2&gt;

&lt;p&gt;Clone the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mihailove123/pitchpulse-livescore-ap.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;pitchpulse-livescore-ap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application will automatically run with sample data.&lt;/p&gt;

&lt;p&gt;To enable live football data, create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPORTMICRO_API_KEY=your_sportmicro_api_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the development server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
npm run build
npm run start
npm run lint
npm run typecheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before deploying, I recommend running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run lint
npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches type errors and production build issues before they reach the hosting platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;The repository can be deployed to Vercel or another Next.js-compatible platform.&lt;/p&gt;

&lt;p&gt;A basic deployment flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push the repository to GitHub
        ↓
Import the repository into Vercel
        ↓
Add SPORTMICRO_API_KEY
        ↓
Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mock mode can also be useful for preview deployments where live credentials should not be available.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Build Next
&lt;/h2&gt;

&lt;p&gt;The current repository provides a foundation rather than every possible football feature.&lt;/p&gt;

&lt;p&gt;The next additions could include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;league standings&lt;/li&gt;
&lt;li&gt;match incidents&lt;/li&gt;
&lt;li&gt;team lineups&lt;/li&gt;
&lt;li&gt;player statistics&lt;/li&gt;
&lt;li&gt;team pages&lt;/li&gt;
&lt;li&gt;league-specific fixture pages&lt;/li&gt;
&lt;li&gt;favorite teams&lt;/li&gt;
&lt;li&gt;goal notifications&lt;/li&gt;
&lt;li&gt;live polling&lt;/li&gt;
&lt;li&gt;match search&lt;/li&gt;
&lt;li&gt;user time zones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existing structure makes these additions easier because the transport and UI layers are already separated.&lt;/p&gt;

&lt;p&gt;New API methods can be added to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/lib/sportmicro.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New normalized types can be added to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/types/sportmicro.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The components can continue receiving predictable application objects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The most important lesson from this project was not about football data.&lt;/p&gt;

&lt;p&gt;It was about fallback design.&lt;/p&gt;

&lt;p&gt;An API integration should not make the entire application impossible to run without the API.&lt;/p&gt;

&lt;p&gt;By keeping mock and live data behind the same functions, the project becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;easier to clone&lt;/li&gt;
&lt;li&gt;easier to test&lt;/li&gt;
&lt;li&gt;easier to demonstrate&lt;/li&gt;
&lt;li&gt;easier to contribute to&lt;/li&gt;
&lt;li&gt;easier to develop offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second lesson was to normalize external data early.&lt;/p&gt;

&lt;p&gt;Raw API responses are useful at the network boundary.&lt;/p&gt;

&lt;p&gt;They should not become the language spoken by the entire application.&lt;/p&gt;

&lt;p&gt;The final architecture is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query builder
      ↓
Authenticated request
      ↓
Normalized application data
      ↓
Reusable football components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure is what turns a football API demo into an application that can continue growing.&lt;/p&gt;

&lt;p&gt;The source code is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mihailove123/pitchpulse-livescore-ap" rel="noopener noreferrer"&gt;PitchPulse Live on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would be interested to hear how other developers handle mock mode in API-powered projects. Do you keep static fixtures, use a local mock server, or make the real integration optional?&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>ai</category>
      <category>football</category>
    </item>
    <item>
      <title>My Football App Worked Perfectly Until Matchday Started</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 14 Jun 2026 14:17:09 +0000</pubDate>
      <link>https://dev.to/mihailove123/my-football-app-worked-perfectly-until-matchday-started-3i59</link>
      <guid>https://dev.to/mihailove123/my-football-app-worked-perfectly-until-matchday-started-3i59</guid>
      <description>&lt;p&gt;Building a football scores app looked easy when I first planned it.&lt;/p&gt;

&lt;p&gt;Fetch today's matches.&lt;/p&gt;

&lt;p&gt;Display the teams.&lt;/p&gt;

&lt;p&gt;Show the score.&lt;/p&gt;

&lt;p&gt;Refresh the page every few seconds.&lt;/p&gt;

&lt;p&gt;That was the entire architecture.&lt;/p&gt;

&lt;p&gt;During development, it worked perfectly. I tested it with a few sample matches, opened the page in two browser tabs, and watched the scores update.&lt;/p&gt;

&lt;p&gt;Then the first busy matchday arrived.&lt;/p&gt;

&lt;p&gt;Several games started at the same time. Hundreds of users opened the app. Requests began overlapping. Some scores appeared to move backward. Finished matches continued refreshing, and the same football data was fetched separately for every visitor.&lt;/p&gt;

&lt;p&gt;The API was working correctly.&lt;/p&gt;

&lt;p&gt;The frontend was rendering what it received.&lt;/p&gt;

&lt;p&gt;The problem was everything between them.&lt;/p&gt;

&lt;p&gt;That day taught me that a football livescore app is not simply a website connected to an API.&lt;/p&gt;

&lt;p&gt;It is a live data synchronization system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Version
&lt;/h2&gt;

&lt;p&gt;My first implementation was a client-side component with a polling interval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;homeScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="na"&gt;awayScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LiveMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/matches/live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="nf"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;fetchMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;fetchMatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;5000&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeScore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; - &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayScore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looked reasonable.&lt;/p&gt;

&lt;p&gt;Every five seconds, the app requested the newest scores and replaced the current state.&lt;/p&gt;

&lt;p&gt;The problem became obvious when the number of users increased.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 user      = 12 requests per minute
100 users   = 1,200 requests per minute
1,000 users = 12,000 requests per minute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of those requests were asking for exactly the same data.&lt;/p&gt;

&lt;p&gt;Every browser was independently fetching the same matches.&lt;/p&gt;

&lt;p&gt;The app had no shared cache, no request coordination, and no understanding of whether the matches could still change.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Live Score Does Not Need to Be Fetched Separately for Every User
&lt;/h2&gt;

&lt;p&gt;When two users open the same match page, they usually need the same score.&lt;/p&gt;

&lt;p&gt;The score is not personalized.&lt;/p&gt;

&lt;p&gt;That means the server should be able to reuse a recently fetched result instead of requesting the football provider again for every visitor.&lt;/p&gt;

&lt;p&gt;A basic server-side request function might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/football-api.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FOOTBALL_API_URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FOOTBALL_API_KEY&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FOOTBALL_API_URL is missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FOOTBALL_API_KEY is missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;footballRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Football API request failed with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the exact URL or authentication format.&lt;/p&gt;

&lt;p&gt;The important part is that the external API request happens on the server.&lt;/p&gt;

&lt;p&gt;The browser communicates with your application.&lt;/p&gt;

&lt;p&gt;Your application communicates with the football data provider.&lt;/p&gt;

&lt;p&gt;This gives you control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API credentials&lt;/li&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Response formatting&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Never Expose the Football API Key in the Browser
&lt;/h2&gt;

&lt;p&gt;This is unsafe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://example.com/live?key=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_FOOTBALL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variables prefixed with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; can be included in client-side JavaScript.&lt;/p&gt;

&lt;p&gt;Anyone using the application may be able to inspect the request and copy the key.&lt;/p&gt;

&lt;p&gt;Instead, keep the key in a private environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FOOTBALL_API_URL=https://api.example.com
FOOTBALL_API_KEY=your_private_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import the API client only from server-side files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A football app can generate a large number of requests during popular matches. An exposed key can quickly become expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do Not Use the Raw API Response Everywhere
&lt;/h2&gt;

&lt;p&gt;My next mistake was passing the provider response directly into React components.&lt;/p&gt;

&lt;p&gt;The component expected fields such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home_team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match_status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That created a strong dependency between the UI and one specific response format.&lt;/p&gt;

&lt;p&gt;When one endpoint returned &lt;code&gt;homeTeam&lt;/code&gt; instead of &lt;code&gt;home_team&lt;/code&gt;, I had to add special cases inside the component.&lt;/p&gt;

&lt;p&gt;A better approach is to define an internal match type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// types/match.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halftime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postponed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;logoUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;competition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt;
  &lt;span class="na"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt;
  &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="na"&gt;away&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MatchStatus&lt;/span&gt;
  &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;startsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then map the provider response into the application's format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/map-match.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;MatchStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ApiMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;league&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;home&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;away&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="nx"&gt;away&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mapMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApiMatch&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;competition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;league&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;league&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown competition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;league&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown-home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;logoUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;away&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown-away&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;away&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Away team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;logoUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;away&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;away&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;away&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;MatchStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first_half&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second_half&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in_progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;half_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halftime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halftime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postponed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postponed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the components depend on a stable application model.&lt;/p&gt;

&lt;p&gt;If the external API changes, only the mapping layer needs to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Render the First Match List on the Server
&lt;/h2&gt;

&lt;p&gt;My first app displayed an empty loading screen until the browser completed its first request.&lt;/p&gt;

&lt;p&gt;That was unnecessary.&lt;/p&gt;

&lt;p&gt;With Next.js Server Components, the initial matches can be loaded before the page reaches the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/page.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MatchCenter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/match-center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getMatchesByDate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/get-matches&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMatchesByDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Football scores&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Today's Matches&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchCenter&lt;/span&gt;
        &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;initialMatches&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server-side data function can apply a short cache duration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/get-matches.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;footballRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/football-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mapMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/map-match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchesResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMatchesByDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FootballMatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;footballRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MatchesResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/matches?date=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;mapMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visitor sees content immediately.&lt;/p&gt;

&lt;p&gt;The client-side code only needs to keep that content updated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use Your Own Route Handler for Updates
&lt;/h2&gt;

&lt;p&gt;The browser should not need to know which provider powers the football data.&lt;/p&gt;

&lt;p&gt;Create an internal endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/matches/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getMatchesByDate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/get-matches&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isValidDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A valid date in YYYY-MM-DD format is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMatchesByDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public, s-maxage=10, stale-while-revalidate=20&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load football matches&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Football data is temporarily unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint becomes the boundary between the browser and the football provider.&lt;/p&gt;

&lt;p&gt;Later, you can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Provider fallback&lt;/li&gt;
&lt;li&gt;Region-specific caching&lt;/li&gt;
&lt;li&gt;Subscription checks&lt;/li&gt;
&lt;li&gt;Request tracing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Stop Polling When Nothing Is Live
&lt;/h2&gt;

&lt;p&gt;This was the easiest improvement and one of the most valuable.&lt;/p&gt;

&lt;p&gt;My original app refreshed the match list every five seconds, even when every match was finished.&lt;/p&gt;

&lt;p&gt;A finished score is unlikely to change.&lt;/p&gt;

&lt;p&gt;A match scheduled for tomorrow does not need to be requested every few seconds.&lt;/p&gt;

&lt;p&gt;The polling logic should depend on the match state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/match-center.tsx&lt;/span&gt;

&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MatchList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/match-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchCenterProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;initialMatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MatchCenter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;initialMatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MatchCenterProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialMatches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastUpdated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbortController&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasLiveMatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halftime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshMatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;currentRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nx"&gt;currentRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;

      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`/api/matches?date=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`Request failed with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nf"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setLastUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DOMException&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Live updates are temporarily delayed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasLiveMatches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;refreshMatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;currentRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hasLiveMatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshMatches&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;small&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Updated&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;lastUpdated&lt;/span&gt;
          &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;small&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchList&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important check is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasLiveMatches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When no match is live, the refresh interval is not created.&lt;/p&gt;




&lt;h2&gt;
  
  
  Old Requests Can Overwrite New Scores
&lt;/h2&gt;

&lt;p&gt;Polling introduces a race condition that is easy to miss.&lt;/p&gt;

&lt;p&gt;Imagine these two requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A starts at 14:30:00
Request B starts at 14:30:15

Request B returns first with a score of 2-1
Request A returns later with a score of 1-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both responses update the same state, the app shows the older score after the newer one.&lt;/p&gt;

&lt;p&gt;From the user's perspective, the score moves backward.&lt;/p&gt;

&lt;p&gt;This does not necessarily mean the football API returned incorrect data.&lt;/p&gt;

&lt;p&gt;It may mean the responses arrived in the wrong order.&lt;/p&gt;

&lt;p&gt;That is why the component aborts the previous request before starting a new one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;currentRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;currentRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The older request is no longer allowed to overwrite the newest result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do Not Delete Good Data Because One Refresh Failed
&lt;/h2&gt;

&lt;p&gt;My first error handler replaced the match list with an empty array.&lt;/p&gt;

&lt;p&gt;That was a bad decision.&lt;/p&gt;

&lt;p&gt;A score that is fifteen seconds old is usually more useful than no score at all.&lt;/p&gt;

&lt;p&gt;When a background refresh fails, keep the last successful data visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latestMatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nf"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latestMatches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Updates are delayed. Showing the latest available scores.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI should communicate that the data may be stale, but it should not destroy useful information.&lt;/p&gt;

&lt;p&gt;A live football app has more states than just loading, success, and error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial loading
Fresh data
Refreshing
Temporarily stale data
Partial data
Provider unavailable
Recovered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Designing for these states makes the app feel much more stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Match Status Should Control Behavior
&lt;/h2&gt;

&lt;p&gt;At first, I treated match status as a label.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But status is not only presentation data.&lt;/p&gt;

&lt;p&gt;It should control the application.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/match-status.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halftime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isFinished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canStillChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halftime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These helpers can determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether polling should continue&lt;/li&gt;
&lt;li&gt;Whether the minute should be shown&lt;/li&gt;
&lt;li&gt;Whether kickoff time should be displayed&lt;/li&gt;
&lt;li&gt;Whether the score is final&lt;/li&gt;
&lt;li&gt;Whether notifications can still be triggered&lt;/li&gt;
&lt;li&gt;Whether standings may need updating&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the app grows, you may need additional states such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extra time
Penalties
Interrupted
Abandoned
Delayed
Awarded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalizing those states early prevents status checks from spreading throughout the codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Group Matches by Competition
&lt;/h2&gt;

&lt;p&gt;A flat list works when there are five matches.&lt;/p&gt;

&lt;p&gt;It becomes difficult to read when there are fifty.&lt;/p&gt;

&lt;p&gt;Group the matches by league or competition before rendering them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/group-matches.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CompetitionGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;groupMatchesByCompetition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CompetitionGroup&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;CompetitionGroup&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;competitionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;competitionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;competitionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;competitionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then render each competition separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/match-list.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;groupMatchesByCompetition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/group-matches&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MatchRow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/match-row&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchListProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MatchList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MatchListProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;groupMatchesByCompetition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;No matches are available.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MatchRow&lt;/span&gt;
                &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data structure now supports the interface instead of forcing every component to repeat the same transformation.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Simple Match Row
&lt;/h2&gt;

&lt;p&gt;Once the data is normalized, the UI becomes much easier to write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/match-row.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isLive&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/match-status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchRowProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FootballMatch&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MatchRow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MatchRowProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logoUrl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
            &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logoUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
            &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt; &lt;span class="na"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startsAt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startsAt&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;away&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt;
              &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'`&lt;/span&gt;
              &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LIVE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logoUrl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
            &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logoUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
            &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awayTeam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component does not know anything about the external provider.&lt;/p&gt;

&lt;p&gt;It only understands the application's &lt;code&gt;FootballMatch&lt;/code&gt; type.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching Does Not Mean Showing Old Scores for Minutes
&lt;/h2&gt;

&lt;p&gt;Developers sometimes avoid caching live data because they assume a cache will make the app feel outdated.&lt;/p&gt;

&lt;p&gt;That depends on the cache duration.&lt;/p&gt;

&lt;p&gt;A short shared cache can reduce duplicate requests without creating a noticeable delay.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser refresh interval:       15 seconds
Server cache duration:          10 seconds
Stale-while-revalidate window:  20 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users still receive frequent updates.&lt;/p&gt;

&lt;p&gt;But if hundreds of users request the same match data within a short period, the server can reuse the result.&lt;/p&gt;

&lt;p&gt;Without shared caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 users
500 external API requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a short server cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 users
A much smaller number of external API requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live does not always mean uncached.&lt;/p&gt;

&lt;p&gt;It often means briefly cached and frequently refreshed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Request Flow
&lt;/h2&gt;

&lt;p&gt;After restructuring the application, one visit follows this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User opens the football app

Server Component
  Fetches today's matches
  Uses a short shared cache
  Maps the provider response
  Renders the initial page

Browser
  Displays matches immediately
  Checks whether any match is live

If a match is live
  Starts a refresh interval
  Requests updates from the internal API route

Route Handler
  Validates the date
  Reuses recent shared data when possible
  Calls the football provider when needed
  Returns normalized match objects

Client Component
  Cancels the previous request
  Applies the newest result
  Keeps old data if refreshing fails
  Stops polling when all matches finish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every layer now has a clear responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model That Helped Me
&lt;/h2&gt;

&lt;p&gt;The football provider supplies the raw data.&lt;/p&gt;

&lt;p&gt;The server protects, caches, validates, and normalizes it.&lt;/p&gt;

&lt;p&gt;The browser renders the data and requests updates only while they are useful.&lt;/p&gt;

&lt;p&gt;That is the architecture I wish I had used from the beginning.&lt;/p&gt;

&lt;p&gt;My first version was not wrong because it used polling.&lt;/p&gt;

&lt;p&gt;It was wrong because polling was the entire architecture.&lt;/p&gt;

&lt;p&gt;There was no protection against duplicate requests, stale responses, race conditions, provider changes, or finished matches that could no longer change.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Add Next
&lt;/h2&gt;

&lt;p&gt;Once the live match flow is stable, the same foundation can support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Match detail pages&lt;/li&gt;
&lt;li&gt;Goals, cards, and substitutions&lt;/li&gt;
&lt;li&gt;Starting lineups&lt;/li&gt;
&lt;li&gt;League standings&lt;/li&gt;
&lt;li&gt;Team pages&lt;/li&gt;
&lt;li&gt;Player statistics&lt;/li&gt;
&lt;li&gt;Favorite teams&lt;/li&gt;
&lt;li&gt;Goal notifications&lt;/li&gt;
&lt;li&gt;Match search&lt;/li&gt;
&lt;li&gt;User time zones&lt;/li&gt;
&lt;li&gt;WebSocket updates&lt;/li&gt;
&lt;li&gt;Server-Sent Events&lt;/li&gt;
&lt;li&gt;Multiple sports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is keeping the same data flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External provider
  ↓
Server-side client
  ↓
Mapping layer
  ↓
Application data functions
  ↓
Route Handler or Server Component
  ↓
User interface
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When these boundaries remain clear, adding new football features becomes much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Lesson
&lt;/h2&gt;

&lt;p&gt;The visible part of a football app is simple.&lt;/p&gt;

&lt;p&gt;Two teams.&lt;/p&gt;

&lt;p&gt;One score.&lt;/p&gt;

&lt;p&gt;One match clock.&lt;/p&gt;

&lt;p&gt;The difficult part is making sure every user sees the newest correct version without generating unnecessary requests or exposing private credentials.&lt;/p&gt;

&lt;p&gt;A demo only needs to display football data.&lt;/p&gt;

&lt;p&gt;A real football product needs to manage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live updates&lt;/li&gt;
&lt;li&gt;Shared caching&lt;/li&gt;
&lt;li&gt;Request ordering&lt;/li&gt;
&lt;li&gt;Match states&lt;/li&gt;
&lt;li&gt;Provider failures&lt;/li&gt;
&lt;li&gt;Stale data&lt;/li&gt;
&lt;li&gt;API security&lt;/li&gt;
&lt;li&gt;Response normalization&lt;/li&gt;
&lt;li&gt;Rendering performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My app worked perfectly before matchday because it had never experienced matchday conditions.&lt;/p&gt;

&lt;p&gt;The busy afternoon did not break the application.&lt;/p&gt;

&lt;p&gt;It revealed the application I had actually built.&lt;/p&gt;

&lt;p&gt;Have you ever built a live sports app? What was the first problem that only appeared after real users arrived?&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>football</category>
    </item>
  </channel>
</rss>
