<?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 Basketball Player Insights MCP Server with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:31:49 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-basketball-player-insights-mcp-server-with-sportmicro-4lja</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-basketball-player-insights-mcp-server-with-sportmicro-4lja</guid>
      <description>&lt;p&gt;I wanted a small, focused way for AI clients to ask basketball-player questions without pretending that every stat is always available. The project goal was straightforward: build an MCP server that exposes documented player and player-statistics data from &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt;, while keeping the response surface honest about what is known and what is not.&lt;/p&gt;

&lt;p&gt;That constraint shaped the whole implementation. Instead of trying to act like a full basketball data warehouse, the server is designed as a typed bridge between MCP clients and a basketball stats API. The result is a practical “research tool” for AI agents: query the facts that exist, avoid fabricating the rest, and keep the integration narrow enough to stay maintainable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vodazapienje2/basketball-player-insights-mcp-sportmicro" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built and why it stays narrow
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally small, and that is part of the design. From the package metadata alone, you can see the project is a TypeScript module with a single production entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.d.ts"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells me this is meant to compile down to a distributable MCP server rather than run as a framework-heavy application. The dependencies reinforce that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; for the MCP layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zod&lt;/code&gt; for schema validation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;typescript&lt;/code&gt; and &lt;code&gt;@types/node&lt;/code&gt; for the build toolchain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination points to a server that receives structured requests, validates them, and returns typed data in a way that AI clients can consume reliably. I like this shape for a player-insights tool because sports data often tempts you to overreach. Keeping the server centered on documented player data makes it easier to separate “available facts” from “missing stats” at the boundary.&lt;/p&gt;

&lt;p&gt;The project description is also specific: it is a “focused MCP server for querying documented basketball player and player-statistics data from Sportmicro.” That focus matters. It suggests that the integration is not trying to be a general basketball API wrapper; it is trying to be a purpose-built layer for MCP clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: MCP at the center, Sportmicro as the data source
&lt;/h2&gt;

&lt;p&gt;At a high level, the architecture is simple enough to be robust:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An MCP client asks for player-related information.&lt;/li&gt;
&lt;li&gt;The server validates the request shape.&lt;/li&gt;
&lt;li&gt;The server queries Sportmicro for documented basketball data.&lt;/li&gt;
&lt;li&gt;The server returns structured results, keeping unavailable values out of the response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That separation is the most important part of the design. MCP is the interface contract, while Sportmicro is the source of truth for the underlying basketball data. Because the project goal explicitly calls out “separating returned facts from unavailable statistics,” the implementation has to respect two different responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP responsibility:&lt;/strong&gt; expose tools and responses in a format that AI clients can call safely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data responsibility:&lt;/strong&gt; only return the player facts and statistics actually documented by the upstream source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Zod alongside the MCP SDK is a sensible fit here. In a server like this, schema validation is not just about catching malformed input; it also helps encode the shape of acceptable basketball queries and the shape of responses that downstream clients can reason about. For an AI-facing tool, that is a practical safeguard. It helps keep the server honest, especially when the domain itself can be ambiguous or incomplete.&lt;/p&gt;

&lt;p&gt;I also think the “focused” nature of the repository matters architecturally. Since the project is not trying to bundle a UI, analytics pipeline, or database layer, the code can stay centered on the MCP contract and the Sportmicro API integration. That keeps the surface area smaller for debugging, testing, and future extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation flow: from TypeScript build to MCP runtime
&lt;/h2&gt;

&lt;p&gt;The repository evidence shows a clean build-and-run path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; compiles TypeScript with &lt;code&gt;tsc -p tsconfig.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm start&lt;/code&gt; runs &lt;code&gt;node dist/index.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; executes Node’s test runner against compiled tests in &lt;code&gt;dist/test/**/*.test.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That tells us a few useful things about how the project is structured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source is TypeScript-first.&lt;/li&gt;
&lt;li&gt;Runtime code is emitted into &lt;code&gt;dist/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tests are compiled before being executed.&lt;/li&gt;
&lt;li&gt;The project is meant to be run as a Node module rather than through a custom launcher.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That workflow is a good fit for an MCP server. Once the TypeScript compiles cleanly, the runtime can be started directly from &lt;code&gt;dist/index.js&lt;/code&gt;, which keeps the deployed entry point obvious. For an AI integration, that simplicity helps because there are fewer moving parts between the schema definitions, the MCP server wiring, and the actual request handling.&lt;/p&gt;

&lt;p&gt;The validation layer also likely plays an important role in the request flow. Even without the source files beyond &lt;code&gt;package.json&lt;/code&gt;, the dependency choice implies the flow follows a pattern like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define the request schema with Zod,&lt;/li&gt;
&lt;li&gt;register MCP tools using the SDK,&lt;/li&gt;
&lt;li&gt;validate incoming arguments,&lt;/li&gt;
&lt;li&gt;fetch the requested basketball data from Sportmicro,&lt;/li&gt;
&lt;li&gt;return the response in the MCP format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the kind of implementation flow I prefer for research-oriented integrations. It avoids “smart guessing” in the server itself and leaves the system behavior predictable.&lt;/p&gt;

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

&lt;p&gt;The supplied repository context is minimal, so the tree is intentionally compact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;basketball-player-insights-mcp-sportmicro/
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because only &lt;code&gt;package.json&lt;/code&gt; was available in the supplied evidence, I am not going to pretend there are extra files, scripts, or directories that were not shown. What &lt;em&gt;is&lt;/em&gt; clear from the package manifest is enough to understand the core shape of the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dist/index.js&lt;/code&gt; is the runtime entry point.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dist/index.d.ts&lt;/code&gt; is the published type surface.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsc&lt;/code&gt; is used for building.&lt;/li&gt;
&lt;li&gt;Node’s test runner is used for verification.&lt;/li&gt;
&lt;li&gt;The server depends on MCP and Zod.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, that means the real structure of the project is likely centered around a TypeScript source tree that compiles into &lt;code&gt;dist/&lt;/code&gt;, but I can only describe what is evidenced. For a build story, that restraint is useful: the package file already reveals the operational contract of the app without inventing a file layout.&lt;/p&gt;

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

&lt;p&gt;The repository materials support a few design constraints worth calling out.&lt;/p&gt;

&lt;p&gt;First, the project goal itself creates a trade-off: if the server is supposed to surface documented basketball facts while avoiding unavailable statistics, then the implementation needs to be conservative about response contents. That makes the tool more trustworthy, but it also means the server must resist the temptation to fill gaps with inference. For an MCP server used by AI clients, that is a feature, not a limitation.&lt;/p&gt;

&lt;p&gt;Second, the choice to depend on Zod suggests a deliberate investment in schema discipline. That adds some upfront structure, but it pays off when the server is expected to mediate between unpredictable client prompts and a typed upstream API. In other words, the project favors correctness and clarity over loose convenience.&lt;/p&gt;

&lt;p&gt;Third, the build setup is intentionally plain: TypeScript compile, Node runtime, Node tests. That keeps the toolchain easy to reason about, but it also means the project is optimized for a straightforward server lifecycle rather than a larger application ecosystem. Again, that seems aligned with the stated goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup: what the repository evidence supports
&lt;/h2&gt;

&lt;p&gt;The repository metadata supports a simple local workflow, but only at the level that the package scripts reveal.&lt;/p&gt;

&lt;p&gt;A developer can reasonably infer the following commands exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; to compile the TypeScript sources&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm start&lt;/code&gt; to run the compiled server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; to execute compiled tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not adding extra setup steps, environment variables, or transport configuration because none were evidenced in the supplied repository context. If you are looking at the project locally, the package manifest is the authoritative place to start, and it makes the build/run/test loop very clear.&lt;/p&gt;

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

&lt;p&gt;These are future improvements, not current features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expand the tool surface carefully.&lt;/strong&gt; If the current server only exposes a narrow set of player lookup and statistic queries, additional tools should follow the same “documented facts only” rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add more explicit validation errors.&lt;/strong&gt; Since Zod is already present, it would be natural to make request failures even more descriptive for client authors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the Sportmicro query boundaries.&lt;/strong&gt; A short explanation of what data is in scope versus out of scope would help AI developers use the server more effectively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add more compiled test coverage around edge cases.&lt;/strong&gt; For an MCP integration, tests that verify schema handling and response shape are especially valuable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish a concise usage example.&lt;/strong&gt; A minimal example of an MCP client calling the server would lower friction without expanding the implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those ideas require changing the project’s core philosophy. They just make the same focused server easier to adopt and safer to extend.&lt;/p&gt;

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

&lt;p&gt;The main lesson from this build is that a useful basketball player MCP server does not need to be broad to be valuable. By centering the implementation on TypeScript, MCP, Zod, and a documented source like Sportmicro, the project creates a clean boundary between client requests and verified sports data.&lt;/p&gt;

&lt;p&gt;That boundary is what makes the server interesting: it gives AI clients a practical way to ask for basketball facts without encouraging unsupported answers. For this kind of tool, restraint is an engineering choice, not a missing feature.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>mcp</category>
      <category>api</category>
      <category>basketball</category>
    </item>
    <item>
      <title>How to Build a WNBA Live Score Tracker with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:57:50 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-wnba-live-score-tracker-with-sportmicro-2ln6</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-wnba-live-score-tracker-with-sportmicro-2ln6</guid>
      <description>&lt;p&gt;I wanted a small but realistic way to explore WNBA data without turning the app into a toy demo. The goal of this project was to build a clean, production-style live score tracker that shows how a developer can use &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; as a basketball data provider in a real Next.js app.&lt;/p&gt;

&lt;p&gt;What made this interesting to me was not just rendering scores. Sports data is messy in practice: live matches can be empty, standings can be partial, and upstream responses can change shape over time. So I treated the app as a boundary around provider data rather than as a generic dashboard. That mindset shaped the whole implementation.&lt;/p&gt;

&lt;p&gt;If you want to skim the source while reading, &lt;a href="https://github.com/ganibanani/sportmicro-wnba-live-score-tracker" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built and why I kept it narrow
&lt;/h2&gt;

&lt;p&gt;This project is intentionally focused on three Sportmicro endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/matches-live&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/standings&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/player-projections&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three calls give the app enough variety to cover the main patterns you run into with sports APIs: current game status, a league table snapshot, and forecast-style data. That mix was enough to demonstrate the integration without hiding it behind a lot of routing, state management, or unrelated UI.&lt;/p&gt;

&lt;p&gt;The app is built with Next.js, React, TypeScript, and Tailwind CSS. In the repository, the page itself is very small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/page.tsx&lt;/code&gt; renders a single &lt;code&gt;LiveScoreTracker&lt;/code&gt; component.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/layout.tsx&lt;/code&gt; provides metadata and the root document shell.&lt;/li&gt;
&lt;li&gt;Most of the behavior lives in &lt;code&gt;components/live-score-tracker.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;API access and data shaping live under &lt;code&gt;lib/sportmicro/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That structure is useful because it keeps the UI thin. The page doesn’t know how to authenticate, what endpoint paths exist, or how to normalize provider payloads. It only knows how to display the models it receives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: one boundary for Sportmicro, one boundary for the UI
&lt;/h2&gt;

&lt;p&gt;The core architectural choice in this repo is the separation between transport, mapping, and rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Transport lives in a dedicated client
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/sportmicro/client.ts&lt;/code&gt; contains a &lt;code&gt;SportmicroClient&lt;/code&gt; that does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;reads &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; from the environment&lt;/li&gt;
&lt;li&gt;sends it as a bearer token&lt;/li&gt;
&lt;li&gt;throws a structured error when a request fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That means the rest of the app never touches raw authentication logic. The client also uses &lt;code&gt;cache: 'no-store'&lt;/code&gt;, which fits the live-data use case because the app is meant to request fresh snapshots rather than serve cached responses.&lt;/p&gt;

&lt;p&gt;The client exposes a small error shape too:&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;class&lt;/span&gt; &lt;span class="nc"&gt;SportmicroApiError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;details&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="c1"&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 matters because the UI can treat API problems as real application states, not generic failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Endpoint wrappers stay tiny
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/sportmicro/index.ts&lt;/code&gt; wraps the actual endpoint paths in descriptive functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetchLiveMatches()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchWNBAStandings()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchPlayerProjections()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a good pattern because the rest of the app imports intent, not URLs. For example, the standings call hardcodes &lt;code&gt;type: 'total'&lt;/code&gt;, while the other optional filters are left open for future extension. That tells you where the app is opinionated and where it is intentionally flexible.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Mappers normalize provider data before rendering
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/sportmicro/mappers.ts&lt;/code&gt; converts raw Sportmicro objects into UI-ready shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;toLiveScoreCard&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toStandingRow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toProjectionRow&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the strongest decisions in the project. The UI doesn’t render provider objects directly. Instead, it uses compact structures like &lt;code&gt;LiveScoreCard&lt;/code&gt;, &lt;code&gt;StandingsRow&lt;/code&gt;, and &lt;code&gt;ProjectionRow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That helps in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the component tree stays readable&lt;/li&gt;
&lt;li&gt;the app can display fallback values like &lt;code&gt;—&lt;/code&gt; when a field is missing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I like that the mapper handles both names and IDs for teams and players. For example, if a human-readable name is missing, the app can still show the underlying identifier instead of failing silently. That’s the kind of small resilience you want in a sports data UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the implementation flows from fetch to screen
&lt;/h2&gt;

&lt;p&gt;The app flow is simple, but each step is deliberate.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;components/live-score-tracker.tsx&lt;/code&gt; is the main screen. It does a concurrent fetch with &lt;code&gt;Promise.all&lt;/code&gt;, then maps the results into presentation-friendly arrays:&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;matchesData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;standingsData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectionData&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;fetchLiveMatches&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchWNBAStandings&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchPlayerProjections&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 chose to keep these requests together because the page is designed as a snapshot view. It doesn’t need each section to load independently. The result is easier to read and reason about: either the dashboard data arrives together, or the UI shows an error state.&lt;/p&gt;

&lt;p&gt;The component then renders three areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live games&lt;/li&gt;
&lt;li&gt;standings&lt;/li&gt;
&lt;li&gt;player projections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each section is wrapped in a shared &lt;code&gt;SectionCard&lt;/code&gt; component, which keeps the visual treatment consistent. That matters less for aesthetics than for maintainability. When a dashboard has multiple sections, consistency reduces the amount of repeated markup and makes future changes easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the UI handles real-world data states
&lt;/h2&gt;

&lt;p&gt;A sports app is only believable if it handles the awkward states honestly.&lt;/p&gt;

&lt;p&gt;This project does that in three ways:&lt;/p&gt;

&lt;h3&gt;
  
  
  Error state
&lt;/h3&gt;

&lt;p&gt;If the Sportmicro request fails, the component catches the exception and shows &lt;code&gt;ErrorBanner&lt;/code&gt;. The banner is explicit about the source of the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Unable to load Sportmicro data”&lt;/li&gt;
&lt;li&gt;followed by the error message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a better experience than a blank screen because it tells the user the issue is upstream, not just missing UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Empty state
&lt;/h3&gt;

&lt;p&gt;Each section has its own empty-state treatment. If there are no live games, no standings rows, or no projections, the app shows a local explanatory card. This is important because “no data” is not the same as “broken.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback values
&lt;/h3&gt;

&lt;p&gt;The mapper layer converts nullish fields into &lt;code&gt;—&lt;/code&gt;. That keeps the display stable even when some parts of a record are missing. In practice, that’s one of the easiest ways to make an API-driven UI feel trustworthy.&lt;/p&gt;

&lt;p&gt;One subtle but valuable detail: the live score cards show both score and event phase. That makes the snapshot useful even when a game is not actively changing. You get status context, not just a number.&lt;/p&gt;

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

&lt;p&gt;Here’s the compact shape of the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  layout.tsx
  page.tsx
  globals.css
components/
  live-score-tracker.tsx
  error-banner.tsx
  section-card.tsx
lib/
  sportmicro/
    client.ts
    index.ts
    mappers.ts
    types.ts
tests/
  client.test.ts
  mappers.test.ts
  page-content.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I find this tree useful because it reflects the boundaries cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/&lt;/code&gt; is for composition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;components/&lt;/code&gt; is for UI building blocks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/sportmicro/&lt;/code&gt; is the provider integration layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tests/&lt;/code&gt; is where the behavior gets locked down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes the project easy to extend without creating a tangle of cross-dependencies.&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 solve every sports-data problem. The trade-offs were mostly design constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app uses server-fetched snapshots rather than a persistent realtime stream.&lt;/li&gt;
&lt;li&gt;The UI is scoped to a few Sportmicro endpoints instead of claiming broad WNBA coverage.&lt;/li&gt;
&lt;li&gt;Response types are modeled in TypeScript, but the repository does not add runtime schema validation.&lt;/li&gt;
&lt;li&gt;Query filters exist in the client wrapper for some endpoints, but the current screen keeps the experience simple and focused.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those constraints are sensible for an educational integration example. They keep the code honest about what it does today, while leaving room for growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and what the repo supports
&lt;/h2&gt;

&lt;p&gt;The repository does support local setup, and the README makes the expected workflow clear.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or newer&lt;/li&gt;
&lt;li&gt;a Sportmicro API key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setup flow is straightforward:&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;create a local &lt;code&gt;.env.local&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;set &lt;code&gt;SPORTMICRO_API_KEY=your_key_here&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;npm run dev&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project also includes scripts for:&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;li&gt;&lt;code&gt;npm run typecheck&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I appreciated that there is a &lt;code&gt;.env.example&lt;/code&gt; file in the repo. That’s a small but practical signal that the app expects environment-based configuration and wants to make setup repeatable.&lt;/p&gt;

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

&lt;p&gt;If I were extending this further, I’d focus on a few natural follow-ups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add query controls
&lt;/h3&gt;

&lt;p&gt;The client already supports optional parameters like &lt;code&gt;leagueId&lt;/code&gt;, &lt;code&gt;seasonId&lt;/code&gt;, and &lt;code&gt;matchId&lt;/code&gt;. A next step would be to expose those in the UI so users can narrow the data source instead of always rendering a broad snapshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add runtime validation
&lt;/h3&gt;

&lt;p&gt;The types are helpful, but TypeScript alone doesn’t validate live API responses. A runtime schema layer would make the integration more defensive when provider data changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add live refresh behavior
&lt;/h3&gt;

&lt;p&gt;Right now, the app renders a fresh server snapshot. Polling or another refresh mechanism would make it more suitable for active game monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expand the endpoint coverage
&lt;/h3&gt;

&lt;p&gt;The current scope is intentionally compact. More Sportmicro basketball endpoints could deepen the app without changing its architecture.&lt;/p&gt;

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

&lt;p&gt;The main lesson from this build is that a useful sports app is really a good integration boundary.&lt;/p&gt;

&lt;p&gt;By keeping Sportmicro access inside a small client, normalizing data before it reaches React, and treating empty/error states as first-class UI conditions, the project stays understandable even though it is driven by external live data.&lt;/p&gt;

&lt;p&gt;That pattern is reusable beyond WNBA scores. If you’re building with a &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;sports data API&lt;/a&gt;, a clean boundary like this will save you from coupling the UI too tightly to provider-specific payloads.&lt;/p&gt;

&lt;p&gt;The repo is small on purpose, but that’s what makes it instructive: it shows the shape of a real integration without pretending the hard parts don’t exist.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>opensource</category>
      <category>basketball</category>
    </item>
    <item>
      <title>Building a Fantasy Football Data MCP Server with Sportmicro API</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sun, 27 Sep 2026 13:28:37 +0000</pubDate>
      <link>https://dev.to/mihailove123/building-a-fantasy-football-data-mcp-server-with-sportmicro-api-185m</link>
      <guid>https://dev.to/mihailove123/building-a-fantasy-football-data-mcp-server-with-sportmicro-api-185m</guid>
      <description>&lt;p&gt;When I started this project, I wanted a better answer to a familiar problem: how do you give an AI client grounded football data without letting it drift into invented stats, vague abstractions, or feature creep?&lt;/p&gt;

&lt;p&gt;The goal of this repository is deliberately narrow. It is a read-only MCP server for fantasy-football development questions, backed by &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt;, and it only exposes documented football data. That means the server is meant to help with research, prototyping, and tool integration—not to pretend it can generate fantasy scoring logic or broader league workflows that are not actually supported.&lt;/p&gt;

&lt;p&gt;That constraint shaped almost every design choice in the codebase.&lt;/p&gt;

&lt;p&gt;I’m linking the code once here so you can inspect the implementation while reading: &lt;a href="https://github.com/bryan3eng/sportmicro-fantasy-football-data-mcp" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to solve
&lt;/h2&gt;

&lt;p&gt;A lot of sports-data demos are easy to build because they quietly assume the data is static and complete. Real football data is messier than that. Even when you only care about documented resources, you still need to handle empty responses, partial responses, request validation, and a clean boundary between upstream provider data and the tool output your client sees.&lt;/p&gt;

&lt;p&gt;This project was built around one practical idea: if an MCP client asks for football data, it should get actual Sportmicro data in a predictable shape, not an ad hoc response layered with assumptions.&lt;/p&gt;

&lt;p&gt;So the server focuses on a small set of read-only resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;players&lt;/li&gt;
&lt;li&gt;teams&lt;/li&gt;
&lt;li&gt;matches&lt;/li&gt;
&lt;li&gt;players-statistics&lt;/li&gt;
&lt;li&gt;player-projections&lt;/li&gt;
&lt;li&gt;player-projections-week&lt;/li&gt;
&lt;li&gt;player-projections-season&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That scope is important because it keeps the integration honest. If the provider documents a football endpoint, the server can expose it. If not, it stays out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: a thin MCP layer over a typed Sportmicro client
&lt;/h2&gt;

&lt;p&gt;The architecture is intentionally simple and easy to reason about.&lt;/p&gt;

&lt;p&gt;At the entry point, &lt;code&gt;src/index.ts&lt;/code&gt; creates an MCP server with the official &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;, uses stdio transport, and wires the tool handlers together. The server is named &lt;code&gt;sportmicro-fantasy-football-data-mcp&lt;/code&gt;, and it only advertises tool capability.&lt;/p&gt;

&lt;p&gt;The data path 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;MCP client
  -&amp;gt; tool handler
  -&amp;gt; Sportmicro client
  -&amp;gt; documented Sportmicro endpoint
  -&amp;gt; normalized tool result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is the main design choice I wanted to preserve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/config.ts&lt;/code&gt; owns required environment access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/sportmicro/client.ts&lt;/code&gt; owns HTTP transport and authentication.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/sportmicro/schemas.ts&lt;/code&gt; owns runtime input validation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/tools/registry.ts&lt;/code&gt; maps MCP tool names to handlers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/tools/resource-format.ts&lt;/code&gt; normalizes results into a consistent shape.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This structure matters because the MCP layer stays small. It doesn’t need to know how Sportmicro builds URLs or how request parameters are validated. It just routes a request to the right handler and returns JSON text.&lt;/p&gt;

&lt;p&gt;One small but meaningful detail is that the client is typed and injectable. The &lt;code&gt;SportmicroClient&lt;/code&gt; accepts an API key, an optional base URL, and an optional &lt;code&gt;fetchImpl&lt;/code&gt;. That makes the client easier to test and keeps the production path separate from test setup.&lt;/p&gt;

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

&lt;p&gt;The integration is built around a dedicated &lt;code&gt;SportmicroClient&lt;/code&gt;. It defaults to &lt;code&gt;https://football.sportmicro.com&lt;/code&gt;, accepts a bearer token, and attaches &lt;code&gt;Accept: application/json&lt;/code&gt; on every request.&lt;/p&gt;

&lt;p&gt;Here’s the heart of it:&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchImpl&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="k"&gt;this&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;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&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="k"&gt;this&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s a good example of the server’s overall posture. The integration is narrow, explicit, and read-only.&lt;/p&gt;

&lt;p&gt;A few implementation choices stand out:&lt;/p&gt;

&lt;h3&gt;
  
  
  1) URLs are built from a base path and query object
&lt;/h3&gt;

&lt;p&gt;The client takes a path plus a query object, then appends any defined query fields to the URL. That keeps the transport layer generic enough for multiple documented Sportmicro football resources without hardcoding one-off request logic per endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Non-OK responses become errors early
&lt;/h3&gt;

&lt;p&gt;If the response is not successful, the client throws immediately with the status code and path. The tests in &lt;code&gt;src/test/sportmicro-client.test.ts&lt;/code&gt; show that behavior directly. That is a good boundary because it prevents invalid upstream responses from leaking into tool formatting code.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) The client returns an item envelope instead of raw JSON
&lt;/h3&gt;

&lt;p&gt;The client normalizes the response into &lt;code&gt;{ items, status }&lt;/code&gt;. If the upstream body is an array, it becomes &lt;code&gt;items&lt;/code&gt;; otherwise the client falls back to an empty array. That choice keeps the tool layer focused on application behavior instead of response-shape repair.&lt;/p&gt;

&lt;p&gt;That said, I’d treat this as a constrained normalization strategy, not a universal claim about every Sportmicro response. The repository is careful to support the documented football endpoints it exposes, not every possible provider format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool design: strict inputs, consistent outputs
&lt;/h2&gt;

&lt;p&gt;The tool layer is where the MCP surface becomes concrete.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/tools/registry.ts&lt;/code&gt; defines seven tools and maps each one to a Sportmicro endpoint. Each tool validates input using Zod schemas from &lt;code&gt;src/sportmicro/schemas.ts&lt;/code&gt;, calls the client, and then wraps the data with &lt;code&gt;makeToolResult()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That result helper is small but useful:&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;function&lt;/span&gt; &lt;span class="nf"&gt;makeToolResult&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;resource&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;query&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="nx"&gt;unknown&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;items&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this pattern because every tool response carries the same metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the resource name&lt;/li&gt;
&lt;li&gt;the original query&lt;/li&gt;
&lt;li&gt;the item count&lt;/li&gt;
&lt;li&gt;whether the result was empty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives MCP consumers a predictable envelope while still preserving the source data itself.&lt;/p&gt;

&lt;p&gt;The schemas are also doing real work here. The repository uses Zod to constrain fields like &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;offset&lt;/code&gt;, and &lt;code&gt;player_id&lt;/code&gt;, and it keeps schemas strict so unknown input keys don’t slip through unnoticed. This is especially helpful in a tool server, where a permissive surface can quickly turn into unclear behavior.&lt;/p&gt;

&lt;p&gt;The tests reflect that design. One test checks that a valid player query is passed through correctly and wrapped with metadata. Another confirms that invalid tool input is rejected. That’s exactly the sort of contract I want a server like this to enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project tree, local setup, and what the repo supports
&lt;/h2&gt;

&lt;p&gt;The project is compact enough to understand at a glance:&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                MCP server entrypoint
  config.ts               Environment validation
  sportmicro/
    client.ts             Typed Sportmicro football client
    schemas.ts            Runtime validation and request schemas
    types.ts              Shared TypeScript types
  tools/
    registry.ts           MCP tool registration and handlers
    resource-format.ts    Normalized response helpers
  test/
    sportmicro-client.test.ts
    tool-contracts.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository also supports a straightforward local setup. Based on the README and package scripts, the supported workflow is:&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;Set &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build with &lt;code&gt;npm run build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start the MCP server with &lt;code&gt;node dist/index.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project expects Node.js 18 or newer and uses stdio transport, so it is meant to be launched by an MCP client rather than served as a traditional web API.&lt;/p&gt;

&lt;p&gt;Tests are run with &lt;code&gt;npm test&lt;/code&gt;, and the repository indicates they use mocked Sportmicro responses rather than live API calls. That’s a good fit for a contract-focused tool server.&lt;/p&gt;

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

&lt;p&gt;I don’t see evidence of a dramatic incident or failure in the repository, so I’d frame the main considerations as design constraints rather than setbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Narrow scope versus broad convenience
&lt;/h3&gt;

&lt;p&gt;The biggest trade-off is obvious: supporting only documented read-only football endpoints keeps the server honest, but it also means the MCP surface is intentionally smaller than what a fantasy app builder might eventually want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strict validation versus loose flexibility
&lt;/h3&gt;

&lt;p&gt;The schemas and tool handlers favor explicit input rules over permissive parsing. That can feel strict, but it reduces ambiguity and makes failures easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Normalized wrappers versus raw pass-through
&lt;/h3&gt;

&lt;p&gt;Wrapping results in a consistent &lt;code&gt;meta&lt;/code&gt; block improves tool usability, but it also means the server adds a small abstraction layer instead of exposing raw provider responses unchanged. In this project, that trade-off makes sense because the server is meant to be a dependable research interface, not a generic API proxy.&lt;/p&gt;

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

&lt;p&gt;If I were continuing the project, the next steps would be practical rather than flashy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expand documentation around tool examples for each supported endpoint.&lt;/li&gt;
&lt;li&gt;Add more contract tests for edge cases around empty results and optional filters.&lt;/li&gt;
&lt;li&gt;Consider whether the tool result format should preserve more upstream status detail where it is useful to clients.&lt;/li&gt;
&lt;li&gt;Add clearer usage guidance for MCP clients that connect over stdio.&lt;/li&gt;
&lt;li&gt;Keep watching the boundary between documented Sportmicro fields and any future convenience mapping so the server doesn’t overpromise semantics it cannot prove.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are future improvements, not missing features. The current repo already does the main job: it exposes a small, typed, read-only football data surface with strong validation.&lt;/p&gt;

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

&lt;p&gt;The most useful thing I learned from building this was that a good MCP data server is less about being clever and more about being disciplined.&lt;/p&gt;

&lt;p&gt;If the goal is to help fantasy-football builders prototype against real football data, the safest path is to keep the surface small, validate inputs strictly, preserve the provider boundary, and make empty or partial responses visible instead of guessing.&lt;/p&gt;

&lt;p&gt;That approach makes the server easier to trust, easier to test, and easier to extend later without drifting away from the actual Sportmicro data it is supposed to represent.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>mcp</category>
      <category>api</category>
      <category>football</category>
    </item>
    <item>
      <title>Building Telentir Call Reliability Monitor with Telentir</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sat, 26 Sep 2026 17:32:55 +0000</pubDate>
      <link>https://dev.to/mihailove123/building-telentir-call-reliability-monitor-with-telentir-8do</link>
      <guid>https://dev.to/mihailove123/building-telentir-call-reliability-monitor-with-telentir-8do</guid>
      <description>&lt;p&gt;When I started this project, I wanted a small, honest way to answer a deceptively hard question: what actually happened to a Telentir call?&lt;/p&gt;

&lt;p&gt;Voice workflows do not behave like ordinary API requests. A call can complete cleanly, fail midway, get dropped, remain pending, or end in a state that is not useful for reporting. For operators, that means the difference between “works” and “reliable” depends on how carefully you treat state.&lt;/p&gt;

&lt;p&gt;This repository is my build around that problem. It keeps the integration boundary narrow, models call events first, and only then turns them into counts and metrics. If you want to inspect the code, &lt;a href="https://github.com/jocoomafija/telentir-call-reliability-monitor" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;. I also use &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; as the public developer surface reference in the broader workflow around Telentir integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal: normalize call events before measuring anything
&lt;/h2&gt;

&lt;p&gt;The main design decision in this project is to normalize first and summarize second.&lt;/p&gt;

&lt;p&gt;Instead of assuming that every Telentir event already maps cleanly to a dashboard metric, I defined a minimal event shape in TypeScript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;callId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;raw &lt;code&gt;state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;optional &lt;code&gt;outcome&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;optional &lt;code&gt;failureCategory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;retry and completion flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That choice keeps the model small enough to reason about, but still useful for reliability analysis. A call event can be represented without inventing undocumented provider behavior.&lt;/p&gt;

&lt;p&gt;From there, the project maps events into stable outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;successful&lt;/li&gt;
&lt;li&gt;failed&lt;/li&gt;
&lt;li&gt;dropped&lt;/li&gt;
&lt;li&gt;incomplete&lt;/li&gt;
&lt;li&gt;pending&lt;/li&gt;
&lt;li&gt;unavailable&lt;/li&gt;
&lt;li&gt;unknown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction matters because not every non-success is the same. A pending call should not be treated like a failed one, and an unavailable result is different from an incomplete call. The summary layer preserves those differences so the output can support monitoring, reporting, or follow-up logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the architecture is split
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally compact, and the structure reflects that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── src
│   ├── cli.ts
│   ├── index.ts
│   ├── telentir.ts
│   └── test
│       └── telentir.test.ts
├── .env.example
├── package.json
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the number of files, but the separation of responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/telentir.ts&lt;/code&gt; contains the reliability model and metrics helpers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/cli.ts&lt;/code&gt; runs a local demonstration with sanitized sample events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/index.ts&lt;/code&gt; re-exports the library surface&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/test/telentir.test.ts&lt;/code&gt; validates normalization and summary behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no server, worker, or background sync process in this repository. That is deliberate. The project is a reference implementation for reliability modeling, not a full production ingestion pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Telentir integration boundary
&lt;/h2&gt;

&lt;p&gt;I kept the Telentir side of the project deliberately conservative.&lt;/p&gt;

&lt;p&gt;The repository does not call undocumented endpoints or pretend to know more than the public surface exposes. That shows up in the implementation in two ways.&lt;/p&gt;

&lt;p&gt;First, the code focuses on normalized event inputs rather than live API orchestration. The event model is enough to express the kinds of states a voice workflow can produce, without binding the project to a specific undocumented response schema.&lt;/p&gt;

&lt;p&gt;Second, the examples stay local and fake. The CLI in &lt;code&gt;src/cli.ts&lt;/code&gt; uses sanitized demo events:&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;demoEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NormalizedCallEvent&lt;/span&gt;&lt;span class="p"&gt;[]&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="na"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;call_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;successful&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;callId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;call_2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&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&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;failureCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upstream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;retryEligible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outcome&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&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;callId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;call_3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dropped&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dropped&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;callId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;call_4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&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;That is a useful pattern if you are building around a voice AI platform like Telentir: keep the reliability layer small, testable, and honest about what it knows. The public docs and API pages remain the source of truth for anything live, while this project handles state modeling and summarization.&lt;/p&gt;

&lt;p&gt;In practice, that means the integration boundary is about observation, not automation. The project does not start calls, transfer calls, send messages, or mutate business systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation flow: from event to metric
&lt;/h2&gt;

&lt;p&gt;The code path is straightforward, which is one reason I like it as a reference.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Normalize a single event
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;normalizeCallState&lt;/code&gt; function decides how an event should be classified. The order of checks is important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unavailable wins first&lt;/li&gt;
&lt;li&gt;explicit &lt;code&gt;outcome&lt;/code&gt; comes next&lt;/li&gt;
&lt;li&gt;completed calls become successful&lt;/li&gt;
&lt;li&gt;dropped calls become dropped&lt;/li&gt;
&lt;li&gt;failure categories map to failed&lt;/li&gt;
&lt;li&gt;pending stays pending&lt;/li&gt;
&lt;li&gt;everything else falls back to unknown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That order preserves intent. For example, a completed call should not be downgraded just because a later field is missing. Likewise, unavailable should remain distinct from unknown, because those states imply different operational realities.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Aggregate a batch
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;summarizeCalls&lt;/code&gt; walks the events, counts each outcome, tracks retry-eligible events, and increments failure-category totals. It also computes a completion rate from resolved outcomes only.&lt;/p&gt;

&lt;p&gt;That last detail is worth calling out. The completion rate is calculated from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;successful&lt;/li&gt;
&lt;li&gt;failed&lt;/li&gt;
&lt;li&gt;dropped&lt;/li&gt;
&lt;li&gt;incomplete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; include pending, unavailable, or unknown. That is a sensible boundary because unresolved states should not distort a rate that is meant to describe completed call outcomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Export a text-based metric view
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;exportMetrics&lt;/code&gt; helper turns the summary into a simple text payload. The output is intentionally plain, which makes it easy to adapt for monitoring pipelines or to inspect locally during development.&lt;/p&gt;

&lt;p&gt;I like this choice because it keeps the repository flexible. The project does not force a dashboard format or a specific telemetry vendor. It just emits a predictable, script-friendly representation of the summary.&lt;/p&gt;

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

&lt;p&gt;There were a few design constraints to consider while keeping the project narrow.&lt;/p&gt;

&lt;p&gt;The first is that voice-call reliability is not a single state machine. A call can be “done” from one perspective and still be operationally incomplete from another. That is why the model preserves multiple outcome buckets instead of collapsing everything into success or failure.&lt;/p&gt;

&lt;p&gt;The second is that retry logic is not universally safe. The repository tracks &lt;code&gt;retryEligible&lt;/code&gt;, but it does not automate retries. That is a good trade-off for a monitoring layer, because automatic retry behavior depends on business rules that this project should not assume.&lt;/p&gt;

&lt;p&gt;The third is privacy. The repository avoids logging sensitive data by default and keeps the example events sanitized. That constraint reduces surprise and keeps the code usable as a reference for teams that need to be careful about call data handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tests tell me
&lt;/h2&gt;

&lt;p&gt;The test file is small, but it validates the key assumptions.&lt;/p&gt;

&lt;p&gt;There are tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unavailable state staying unavailable&lt;/li&gt;
&lt;li&gt;completed state mapping to successful&lt;/li&gt;
&lt;li&gt;summary counts across mixed events&lt;/li&gt;
&lt;li&gt;completion rate calculation&lt;/li&gt;
&lt;li&gt;retry-eligible counting&lt;/li&gt;
&lt;li&gt;failure-category counting&lt;/li&gt;
&lt;li&gt;metrics export format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly the level of coverage I would want for this kind of library. The tests protect the normalization rules, and the normalization rules are the heart of the project.&lt;/p&gt;

&lt;p&gt;The test suite also reflects the project’s philosophy: verify the classification logic, not some imagined external provider behavior. That keeps the repository stable even if the surrounding Telentir surface evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and running the project
&lt;/h2&gt;

&lt;p&gt;The repository includes the pieces needed for local development.&lt;/p&gt;

&lt;p&gt;From the supplied files, I can confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or newer is required&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm install&lt;/code&gt; is the install step&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt; exists for local environment setup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TELENTIR_API_KEY&lt;/code&gt; is documented as a server-side value to keep out of client code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package scripts show the intended workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; compiles TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; runs the Node test files from &lt;code&gt;dist/test/*.test.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run dev&lt;/code&gt; runs the CLI through &lt;code&gt;tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm start&lt;/code&gt; launches the built CLI from &lt;code&gt;dist/cli.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository does not show a live server setup, and it does not document any UI. So I would treat this as a local reference project rather than a deploy-ready app.&lt;/p&gt;

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

&lt;p&gt;If I were extending this project, I would keep the same conservative approach and add only the pieces that fit the current shape.&lt;/p&gt;

&lt;p&gt;A few future improvements make sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a real ingestion layer for documented Telentir data, while keeping credentials server-side&lt;/li&gt;
&lt;li&gt;expand the CLI so it can read event samples from a file instead of only using built-in demo data&lt;/li&gt;
&lt;li&gt;add more granular reporting around incomplete and unknown outcomes&lt;/li&gt;
&lt;li&gt;introduce a small adapter layer if the project needs to support multiple data sources later&lt;/li&gt;
&lt;li&gt;add a dashboard only if there is a real operational need for one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would also be careful not to turn the monitor into an automation engine. The value of this repository is that it helps you understand call reliability before you try to act on it.&lt;/p&gt;

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

&lt;p&gt;The main lesson for me was that reliability work gets better when the model stays explicit.&lt;/p&gt;

&lt;p&gt;For a voice AI system, it is not enough to know whether a call “worked.” You need to preserve the difference between successful, failed, dropped, incomplete, pending, unavailable, and unknown states, and you need to keep failure categories separate enough to be useful.&lt;/p&gt;

&lt;p&gt;This project does that with a small TypeScript core, a conservative Telentir boundary, and a simple metric export path. That combination makes it easy to inspect, easy to test, and easy to extend without assuming more than the public surface actually supports.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>api</category>
      <category>voice</category>
    </item>
    <item>
      <title>Building Telentir Human Handoff Demo with Telentir</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:10:34 +0000</pubDate>
      <link>https://dev.to/mihailove123/building-telentir-human-handoff-demo-with-telentir-2fb6</link>
      <guid>https://dev.to/mihailove123/building-telentir-human-handoff-demo-with-telentir-2fb6</guid>
      <description>&lt;p&gt;When I started this demo, I wanted a very specific outcome: model a safe escalation path from an AI voice agent to a human operator without pretending the provider gives me more than it actually does.&lt;/p&gt;

&lt;p&gt;That constraint shaped the whole project. In voice workflows, “handoff” sounds simple, but the real work is deciding when escalation is allowed, who should receive it, what context survives the transfer, and how to keep the result auditable. I built this repository as a small TypeScript example around those boundaries, using &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; as the integration reference point for the public developer surface, while keeping the local logic intentionally conservative.&lt;/p&gt;

&lt;p&gt;The repository is public here: &lt;a href="https://github.com/gobebarcelona/telentir-human-handoff-demo" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted the demo to prove
&lt;/h2&gt;

&lt;p&gt;The goal was not to build a full call center app. It was to show how I would structure a handoff boundary in a real codebase.&lt;/p&gt;

&lt;p&gt;That meant three things mattered more than fancy plumbing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the escalation decision should be explicit&lt;/li&gt;
&lt;li&gt;the human target selection should be separate from the decision itself&lt;/li&gt;
&lt;li&gt;the output should be easy to audit later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project keeps that boundary small on purpose. It does not invent Telentir endpoints, SDK methods, or hidden provider behavior. Instead, it demonstrates the application-side logic that would sit next to a real &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Telentir API&lt;/a&gt; integration. In other words, I treated the provider layer as something to wire in carefully, not something to assume away.&lt;/p&gt;

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

&lt;p&gt;The codebase is tiny, which is what makes it useful as a reference.&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
│   ├── telentir.ts
│   └── test
│       └── handoff.test.ts
├── .env.example
├── .gitignore
├── LICENSE
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/telentir.ts&lt;/code&gt; holds the handoff model and helper functions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/index.ts&lt;/code&gt; shows a runnable local example&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/test/handoff.test.ts&lt;/code&gt; checks the important rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters because it keeps the “business logic” of handoff visible. If I ever connect this to a live voice flow, I want the rules around consent, escalation, and target selection to stay easy to inspect without digging through transport code.&lt;/p&gt;

&lt;p&gt;The state model is also deliberate. The project represents many outcomes explicitly: &lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;empty&lt;/code&gt;, &lt;code&gt;invalid-input&lt;/code&gt;, &lt;code&gt;permission&lt;/code&gt;, &lt;code&gt;rate-limit&lt;/code&gt;, &lt;code&gt;telephony&lt;/code&gt;, &lt;code&gt;upstream&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;unavailable&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;unknown&lt;/code&gt;, and &lt;code&gt;successful&lt;/code&gt;. I like this approach because it avoids collapsing every problem into one vague error. For a human handoff system, the reason matters as much as the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the handoff logic is wired
&lt;/h2&gt;

&lt;p&gt;The core logic lives in &lt;code&gt;src/telentir.ts&lt;/code&gt;, and it is intentionally small enough to read in one sitting.&lt;/p&gt;

&lt;p&gt;A few examples show the shape of the system:&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;function&lt;/span&gt; &lt;span class="nf"&gt;requireExplicitUserAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirmed&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="nx"&gt;HandoffState&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;confirmed&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;'&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;User confirmed the handoff action.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permission&lt;/span&gt;&lt;span class="dl"&gt;'&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;Explicit user action is required before transferring.&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;This function is a good example of the project’s philosophy. Rather than assuming a handoff is allowed, it requires an explicit confirmation signal and turns that into a typed state. In a live application, that would sit before any transfer, outbound message, or business-system mutation.&lt;/p&gt;

&lt;p&gt;The same pattern shows up in escalation assessment:&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;function&lt;/span&gt; &lt;span class="nf"&gt;assessEscalation&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;EscalationRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HandoffState&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transcriptSummary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;empty&lt;/span&gt;&lt;span class="dl"&gt;'&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;No transcript summary is available for escalation.&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&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;The conversation timed out before a handoff decision could be made.&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;'&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;`Escalation evaluated for reason: &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;reason&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That function only models the cases that are evidenced in the repo. It checks for missing transcript context, handles a timeout condition, and otherwise returns a successful evaluation. I like that restraint. It keeps the demo honest about what it knows and avoids pretending there is a larger policy engine behind it.&lt;/p&gt;

&lt;p&gt;Target selection is separate again:&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;function&lt;/span&gt; &lt;span class="nf"&gt;selectHumanTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HumanTarget&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;HumanTarget&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;target&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;available&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="nx"&gt;available&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps “should we escalate?” apart from “who receives it?”. That separation is useful in real workflows because the escalation rule may change more often than the routing rule.&lt;/p&gt;

&lt;p&gt;Finally, the audit string gives the flow a traceable shape:&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;function&lt;/span&gt; &lt;span class="nf"&gt;formatTransferAudit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;handoffState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HandoffState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HumanTarget&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;correlationId&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;span class="kr"&gt;string&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;targetLabel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&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="s2"&gt;`audit:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;correlationId&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handoffState&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;targetLabel&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’m not claiming this is a full audit system. It’s a compact record format that proves the shape of the data I want to preserve: correlation ID, handoff outcome, and target label.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runnable example and test coverage
&lt;/h2&gt;

&lt;p&gt;The entry point in &lt;code&gt;src/index.ts&lt;/code&gt; wires the helpers together with a small local example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it defines two targets&lt;/li&gt;
&lt;li&gt;it evaluates an escalation request&lt;/li&gt;
&lt;li&gt;it checks explicit user confirmation&lt;/li&gt;
&lt;li&gt;it selects an available human target&lt;/li&gt;
&lt;li&gt;it prints a JSON payload with the outcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That file is useful because it shows the intended integration flow without needing a UI or external orchestration layer.&lt;/p&gt;

&lt;p&gt;The tests in &lt;code&gt;src/test/handoff.test.ts&lt;/code&gt; focus on the behaviors that matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit confirmation is required before transfer&lt;/li&gt;
&lt;li&gt;the first available human target is selected&lt;/li&gt;
&lt;li&gt;empty transcript summaries return &lt;code&gt;empty&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;timeout conditions return &lt;code&gt;timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the audit format includes the correlation ID, state, and target label&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test runner itself is also simple: &lt;code&gt;src/test/runner.ts&lt;/code&gt; just imports the handoff test module. That’s enough for the project’s current size, and it keeps the testing story easy to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and environment handling
&lt;/h2&gt;

&lt;p&gt;The repository supports local TypeScript development, and the scripts in &lt;code&gt;package.json&lt;/code&gt; make that clear:&lt;/p&gt;

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

&lt;p&gt;The project targets Node.js 18 or newer, compiles with TypeScript, and runs tests against the built output. There is also an &lt;code&gt;.env.example&lt;/code&gt; file, which signals that the intended integration key is &lt;code&gt;TELENTIR_API_KEY&lt;/code&gt;. The README and docs both keep that key server-side and out of source control.&lt;/p&gt;

&lt;p&gt;That setup tells me the project is meant to be safe by default: local first, secret-aware, and explicit about credentials. It does not present a browser UI or a deployed demo surface, so I wouldn’t infer more setup steps than the repository actually shows.&lt;/p&gt;

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

&lt;p&gt;In this case, the “challenge” was mainly a design constraint: keep the demo useful without inventing undocumented provider behavior.&lt;/p&gt;

&lt;p&gt;That led to a few trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the integration layer stays thin instead of trying to simulate everything&lt;/li&gt;
&lt;li&gt;the state model is explicit instead of compressed into generic failures&lt;/li&gt;
&lt;li&gt;the example is local and inspectable rather than pretending to be a finished product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think that’s the right shape for a handoff demo. Voice escalation is exactly the kind of workflow where overpromising hurts clarity. A narrow, honest boundary is more valuable than a broad one with imaginary features.&lt;/p&gt;

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

&lt;p&gt;A few next steps feel natural if this were growing into a real integration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expand the escalation policy model&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Right now, the demo handles a small set of reasons and outcomes. A future version could separate policy decisions from transport details even further.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add richer target selection rules&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The current implementation picks the first available human target. A more complete system could consider queues, skills, or service windows, as long as those rules remain visible in code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Introduce a more structured audit record&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The string-based audit output is intentionally simple. A future improvement would be a typed audit object with serialization at the edge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect to real Telentir documentation and public APIs&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The repository already positions itself around the public Telentir surface exposed by &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt;, but any live integration should stay aligned with the documented API surface and keep secrets server-side.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add more state-specific tests&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The current tests cover the core path, but more cases around &lt;code&gt;rate-limit&lt;/code&gt;, &lt;code&gt;telephony&lt;/code&gt;, &lt;code&gt;upstream&lt;/code&gt;, and &lt;code&gt;unknown&lt;/code&gt; would make the state model even more useful.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;What I like most about this project is that it treats human handoff as a boundary problem, not just a transfer problem.&lt;/p&gt;

&lt;p&gt;That distinction changes how you build it. Instead of asking, “How do I hand off the call?”, I found it more useful to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when is escalation allowed?&lt;/li&gt;
&lt;li&gt;who should receive it?&lt;/li&gt;
&lt;li&gt;what context should survive?&lt;/li&gt;
&lt;li&gt;what do I need to record so the decision is understandable later?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are the real foundation of a safe &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Telentir handoff&lt;/a&gt;. If you keep the boundary small, explicit, and testable, the rest of the integration becomes much easier to trust.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>api</category>
      <category>voice</category>
    </item>
    <item>
      <title>Building OptaReach AI Campaign Message Template Demo with the OptaReach API</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sat, 26 Sep 2026 14:53:23 +0000</pubDate>
      <link>https://dev.to/mihailove123/building-optareach-ai-campaign-message-template-demo-with-the-optareach-api-2ioi</link>
      <guid>https://dev.to/mihailove123/building-optareach-ai-campaign-message-template-demo-with-the-optareach-api-2ioi</guid>
      <description>&lt;p&gt;I built this demo because I wanted a small, trustworthy way to understand the OptaReach ai API boundary without guessing what happens behind the scenes. The goal was simple: fetch campaigns, choose one, send only documented prompt inputs, and inspect the generated message-template output in a way that stays transparent the whole time.&lt;/p&gt;

&lt;p&gt;That sounds straightforward, but integration demos often drift into “magic” very quickly. They hide the request payload, blur server and client responsibilities, or quietly assume that every workspace has the same shape of data. This project takes the opposite approach. It keeps the boundary explicit, uses server-side fetching, and treats error states as part of the design rather than an afterthought.&lt;/p&gt;

&lt;p&gt;The repository is public here: &lt;a href="https://github.com/masanjaka2/optareach-ai-message-template-generator" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The project is a small Next.js app that focuses on one OptaReach workflow: campaign message-template generation. The intent is not to build a full campaign manager. It is to show the path from campaign discovery to generated template output with as little indirection as possible.&lt;/p&gt;

&lt;p&gt;That workflow is visible in the page itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;load campaigns from OptaReach&lt;/li&gt;
&lt;li&gt;show a selection-oriented view&lt;/li&gt;
&lt;li&gt;prepare prompt inputs such as objective, tone, and context notes&lt;/li&gt;
&lt;li&gt;generate a template from the selected campaign&lt;/li&gt;
&lt;li&gt;render the returned output without hiding it behind extra app logic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I like this kind of demo because it teaches the integration shape directly. If you are evaluating the &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;OptaReach API&lt;/a&gt; for campaign workflows, you can inspect the code and quickly see what the app does and does not do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: a server-rendered page with a thin API client
&lt;/h2&gt;

&lt;p&gt;The structure is deliberately compact. There are only two real application layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/page.tsx&lt;/code&gt; handles the server-rendered UI and orchestrates data loading&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/optareach.ts&lt;/code&gt; wraps the API requests and error mapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app uses the Next.js App Router, which fits the project goal well because the initial campaign fetch happens server-side. That keeps credentials out of the browser and makes the first render reflect real API state instead of a client-side loading illusion.&lt;/p&gt;

&lt;p&gt;A concise view of the project tree 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;app/
  layout.tsx
  page.tsx
  globals.css
lib/
  optareach.ts
tests/
  optareach.test.ts
.env.example
README.md
package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client wrapper is intentionally narrow. In &lt;code&gt;lib/optareach.ts&lt;/code&gt;, the app only talks to two documented endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /api/v1/platforms/{platform}/campaigns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /api/v1/platforms/{platform}/campaigns/{campaignId}/message-template/generate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That implementation choice matters. The code does not invent a wider SDK abstraction or try to normalize the API into something it is not. It just wraps the documented calls and maps failures into readable error kinds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the OptaReach integration works
&lt;/h2&gt;

&lt;p&gt;The most useful part of this demo is that it keeps the request boundary visible. The API helper defines the request shape for message-template generation as three fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;objective&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tone&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contextNotes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That strictness is a feature, not a limitation. In &lt;code&gt;generateMessageTemplate&lt;/code&gt;, the request body is serialized exactly as provided, and the surrounding comment makes the boundary explicit:&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;// API boundary: only the documented request body fields are sent to OptaReach.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think that kind of comment is valuable in an integration demo because it tells future readers what is intentional. The project is not trying to infer undocumented payloads or sneak in extra defaults. It is showing the smallest valid integration surface.&lt;/p&gt;

&lt;p&gt;Another important choice is the error mapping. The helper converts HTTP status codes into a small set of categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;permission&lt;/li&gt;
&lt;li&gt;unavailable&lt;/li&gt;
&lt;li&gt;rate_limit&lt;/li&gt;
&lt;li&gt;upstream&lt;/li&gt;
&lt;li&gt;network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives the UI a vocabulary for meaningful feedback. A 422 validation problem is different from a 403 permission issue, and the app treats them that way instead of collapsing everything into “something went wrong.”&lt;/p&gt;

&lt;p&gt;The page component then uses those categories to render a readable state. That’s the kind of behavior I wanted to demonstrate with &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt;: not just that the request succeeds, but that the integration remains understandable when it doesn’t.&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;app/page.tsx&lt;/code&gt; file follows a simple flow that mirrors the project goal.&lt;/p&gt;

&lt;p&gt;First, it tries to load campaigns for a fixed platform value:&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;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multi-platform&lt;/span&gt;&lt;span class="dl"&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 fixed platform keeps the demo focused. It avoids adding a selector or routing complexity before the core workflow is proven.&lt;/p&gt;

&lt;p&gt;Next, it calls &lt;code&gt;fetchCampaigns(platform)&lt;/code&gt; and separates the happy path from the error path. If an error occurs, the page captures it and shows a readable status message. If no campaigns are returned, the UI shows an empty state instead of pretending the workspace is ready.&lt;/p&gt;

&lt;p&gt;Then, if a campaign is available, the page attempts a preview generation call using the first campaign’s ID and a small request object:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;objective: &lt;code&gt;Demo preview&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tone: &lt;code&gt;clear&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;context notes: a short note about the server-side boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That preview is wrapped in a try/catch block, and failures simply result in no preview being shown. That is a sensible choice for a demo because it keeps the page usable even when generated output is unavailable.&lt;/p&gt;

&lt;p&gt;The UI itself is intentionally modest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a campaign selector card&lt;/li&gt;
&lt;li&gt;a template generation form&lt;/li&gt;
&lt;li&gt;a generated output preview section&lt;/li&gt;
&lt;li&gt;an implementation notes section&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The form is presentational in this repository. The button is disabled, which makes the boundary clear: the app demonstrates the integration structure, but it does not pretend to be a fully wired production workflow in the local demo state.&lt;/p&gt;

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

&lt;p&gt;I did not treat this as a “solve everything” build. The main design constraints were clarity, trust, and API honesty.&lt;/p&gt;

&lt;p&gt;A few trade-offs show up clearly in the repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed platform value:&lt;/strong&gt; keeping &lt;code&gt;multi-platform&lt;/code&gt; hard-coded reduces complexity, but it also means the demo stays focused on one integration shape rather than becoming a general platform switcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side fetching:&lt;/strong&gt; this protects credentials and makes the data boundary clean, but it also means the app is designed around server-rendered behavior instead of a fully client-driven interaction model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal form state:&lt;/strong&gt; the UI shows prompt inputs, but the repository does not wire a complete interactive generation flow in the local demo state. That keeps the example small and explicit, though it leaves room for a richer connected experience later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those constraints are reasonable for a project that wants to be inspectable. I would rather have a demo that is limited but honest than a broad example that silently blurs documented API behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and what the repository supports
&lt;/h2&gt;

&lt;p&gt;The repository includes enough evidence to describe local setup confidently. The README and &lt;code&gt;package.json&lt;/code&gt; show a standard Next.js 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
cp&lt;/span&gt; .env.example .env.local
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production build is also supported:&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 build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The environment setup uses &lt;code&gt;OPTAREACH_API_KEY&lt;/code&gt; for authenticated server-side requests, and the repository makes it clear that the key is not printed or exposed to the browser.&lt;/p&gt;

&lt;p&gt;That is enough to run the app locally and inspect its empty-state behavior, campaign-loading path, and API-client structure. I would keep expectations modest here: the repo is a demo, not a fully scripted deployment guide, so the supported setup is intentionally lightweight.&lt;/p&gt;

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

&lt;p&gt;If I were extending this project, I would keep the same philosophy and add only changes that preserve the documented boundary.&lt;/p&gt;

&lt;p&gt;Some sensible next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wire the form to a real submission flow in the connected deployment&lt;/li&gt;
&lt;li&gt;persist the selected campaign instead of always using the first loaded campaign&lt;/li&gt;
&lt;li&gt;add a copy-to-clipboard action for generated template output&lt;/li&gt;
&lt;li&gt;render more of the returned message-template shape once the exact response structure is confirmed&lt;/li&gt;
&lt;li&gt;expand tests around the API client and the error mapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would also keep the documentation discipline intact. Any future enhancement should still be tied directly to the documented OptaReach endpoints rather than a guessed workflow.&lt;/p&gt;

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

&lt;p&gt;The biggest lesson from this build is that a good API demo should make the integration feel obvious. You should be able to tell where the request starts, what is sent, how errors are classified, and what the UI does when data is missing.&lt;/p&gt;

&lt;p&gt;This project does that well by staying small, server-side, and explicit. It does not try to impress with abstraction. Instead, it shows how to build a clean bridge between a Next.js app and the OptaReach campaign message-template generation flow, which is exactly what I wanted from the start.&lt;/p&gt;

</description>
      <category>api</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>sales</category>
    </item>
    <item>
      <title>How to Build a Snooker Tournament Dashboard with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sat, 26 Sep 2026 13:33:31 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-snooker-tournament-dashboard-with-sportmicro-128p</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-snooker-tournament-dashboard-with-sportmicro-128p</guid>
      <description>&lt;p&gt;I wanted a snooker dashboard that felt like a real application, not a throwaway API demo.&lt;/p&gt;

&lt;p&gt;The goal of this project was simple: build a production-style dashboard around the &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; sports data API, using Next.js, React, and TypeScript, while keeping the integration easy to inspect and extend. Instead of hardcoding data or hiding the provider behind a large abstraction, I treated the app as a practical example of how to work with a &lt;strong&gt;snooker tournament dashboard API&lt;/strong&gt; in a clean, server-side-friendly way.&lt;/p&gt;

&lt;p&gt;What made the project interesting to me was the shape of the data itself. Tournaments, seasons, teams, matches, and cup brackets are related, but they do not always arrive in perfect order or with complete fields. That pushed me to design the app around a few core concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep Sportmicro access in one place&lt;/li&gt;
&lt;li&gt;handle loading, empty, and error states explicitly&lt;/li&gt;
&lt;li&gt;preserve stable UI behavior even when data is incomplete&lt;/li&gt;
&lt;li&gt;make the dashboard readable enough that another developer could extend it without guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/monka7monka7/sportmicro-snooker-tournament-dashboard" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built and why I kept the scope narrow
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally focused on one workflow: explore snooker tournament data from Sportmicro and inspect the related records that matter to a developer.&lt;/p&gt;

&lt;p&gt;From the README and code, the dashboard currently works with these documented Sportmicro endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/tournaments&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/seasons-by-tournament&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/teams-by-tournament&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;/cup-bracket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That selection was enough to build a useful demo without pretending to be an exhaustive sports platform. I like this scope because it demonstrates a realistic integration pattern: fetch a list of tournaments, let the user narrow the list, then load detail data for the selected tournament.&lt;/p&gt;

&lt;p&gt;The app structure reflects that same constraint. It is not a multi-page product with dozens of views. It is a single dashboard page that prioritizes clarity over feature breadth.&lt;/p&gt;

&lt;p&gt;A concise project tree makes that easier to see:&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/
    error.tsx
    globals.css
    layout.tsx
    loading.tsx
    page.tsx
  lib/
    sportmicro.ts
    tournament-dashboard.ts
tests/
  tournament-dashboard.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That layout tells the story of the project pretty well: route-level UI in &lt;code&gt;src/app&lt;/code&gt;, API access and helpers in &lt;code&gt;src/lib&lt;/code&gt;, and tests that focus on the data logic rather than the whole app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: keep provider logic out of the UI
&lt;/h2&gt;

&lt;p&gt;The first design decision I made was to separate Sportmicro communication from the page component.&lt;/p&gt;

&lt;p&gt;The repository does that in &lt;code&gt;src/lib/sportmicro.ts&lt;/code&gt;, which owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Sportmicro base URL&lt;/li&gt;
&lt;li&gt;request construction&lt;/li&gt;
&lt;li&gt;API key handling through &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;response parsing&lt;/li&gt;
&lt;li&gt;basic error normalization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of boundary I reach for whenever a project depends on an external API. The UI should know what data it wants, not how each request is assembled.&lt;/p&gt;

&lt;p&gt;The client is small but practical. It exposes methods for the documented snooker endpoints and uses a helper to build query parameters only when values are present. That keeps URL construction consistent and avoids clutter in the component layer.&lt;/p&gt;

&lt;p&gt;A simplified view of that pattern is visible in the repository:&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;function&lt;/span&gt; &lt;span class="nf"&gt;createSportmicroClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&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;async&lt;/span&gt; &lt;span class="nf"&gt;getTournaments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;leagueId&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;classId&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;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="nf"&gt;createUrl&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tournaments&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;league_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildQueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;league_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;leagueId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;class_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildQueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;classId&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;requestJson&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Tournament&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&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="nf"&gt;getMatchesByTournament&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;tournamentId&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;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="nf"&gt;createUrl&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="na"&gt;tournament_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildQueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tournament_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tournamentId&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;requestJson&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&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;That pattern gives me two useful benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The page can request a tournament list or tournament details without knowing anything about URL formatting.&lt;/li&gt;
&lt;li&gt;Extending the project later means adding another client method rather than scattering fetch logic across the app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also appreciate that the project keeps the API key on the server side. The page creates the client from &lt;code&gt;process.env.SPORTMICRO_API_KEY&lt;/code&gt;, which matches the repository’s goal of avoiding exposed credentials and using the same configuration model locally and in deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the dashboard flows from search to detail view
&lt;/h2&gt;

&lt;p&gt;The main page in &lt;code&gt;src/app/page.tsx&lt;/code&gt; follows a straightforward flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create the Sportmicro client&lt;/li&gt;
&lt;li&gt;fetch tournaments&lt;/li&gt;
&lt;li&gt;filter them using the search query&lt;/li&gt;
&lt;li&gt;select one tournament&lt;/li&gt;
&lt;li&gt;fetch its related seasons, teams, matches, and cup bracket data&lt;/li&gt;
&lt;li&gt;render the dashboard with explicit state handling&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence is easy to read because the file keeps the orchestration in one place. The page component is server-rendered, and it only asks for the data it needs based on the current search params.&lt;/p&gt;

&lt;p&gt;The search experience is intentionally modest. The helper in &lt;code&gt;src/lib/tournament-dashboard.ts&lt;/code&gt; trims and lowercases the query, then checks a small set of fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tournament name&lt;/li&gt;
&lt;li&gt;tournament ID&lt;/li&gt;
&lt;li&gt;league ID&lt;/li&gt;
&lt;li&gt;class ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That choice felt right for this project because the goal was not to build a complex search system. It was to create a useful browsing experience with almost no extra machinery.&lt;/p&gt;

&lt;p&gt;The match list also has one implementation detail worth highlighting: deterministic sorting.&lt;/p&gt;

&lt;p&gt;Sports data can arrive with incomplete timestamps, and if a list order shifts from refresh to refresh, the UI starts to feel unstable. The repository avoids that by sorting in layers: known start time first, then status type, then match ID.&lt;/p&gt;

&lt;p&gt;Here is the relevant logic:&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;function&lt;/span&gt; &lt;span class="nf"&gt;sortMatches&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;Match&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;matches&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;aTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_time&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Date&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;NaN&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;bTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_time&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Date&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;NaN&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aTime&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="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bTime&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="nx"&gt;aTime&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;bTime&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;aTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;bTime&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aTime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bTime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&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;aStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status_type&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status_type&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;bStatus&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;aStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&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;a&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="nf"&gt;localeCompare&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;b&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="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 is a small function, but it solves a real UX issue: list order should stay predictable even when upstream data is imperfect.&lt;/p&gt;

&lt;p&gt;The page also treats loading, empty, and failure as first-class outcomes. If the tournament list cannot load, the UI shows a visible error. If the search returns nothing, it tells the user to adjust the query instead of presenting a blank state. And if a selected tournament has no related records, the summary panel explains that too. That feels appropriate for a dashboard built on live sports data.&lt;/p&gt;

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

&lt;p&gt;I did not document a dramatic failure during the build, so the more honest way to frame this section is as a set of constraints I had to design around.&lt;/p&gt;

&lt;p&gt;The biggest trade-off was scope. Sportmicro exposes a broader API, but this repository intentionally sticks to a narrow snooker workflow. That means the project is easier to understand, easier to test, and less likely to drift into vague “integration demo” territory.&lt;/p&gt;

&lt;p&gt;A few other design constraints shaped the implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server-side credentials only&lt;/strong&gt;: the app reads &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt; on the server, which keeps secrets out of the client bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No invented data&lt;/strong&gt;: the UI uses actual provider responses and shows empty/error states instead of fabricating records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No over-engineering&lt;/strong&gt;: the repo uses small modules and simple helpers instead of introducing a heavy state-management layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit boundaries&lt;/strong&gt;: Sportmicro request logic lives in one file, and UI-friendly filtering/sorting lives in another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those choices may sound conservative, but for a sports data dashboard they matter. When the provider response changes or a field is missing, a narrow architecture is easier to reason about than a clever one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and what the repository supports
&lt;/h2&gt;

&lt;p&gt;The repository does document how to run the project locally, so I can speak to that part confidently.&lt;/p&gt;

&lt;p&gt;The setup is straightforward:&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
cp&lt;/span&gt; .env.example .env.local
&lt;span class="nv"&gt;SPORTMICRO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_sportmicro_api_key_here
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README also shows &lt;code&gt;npm run build&lt;/code&gt; and &lt;code&gt;npm test&lt;/code&gt;, and the &lt;code&gt;package.json&lt;/code&gt; confirms the available scripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typecheck&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That lines up with the rest of the codebase: a Next.js app with TypeScript, Tailwind CSS, and a small Node test suite. There is no evidence of a more complicated local environment, so I would keep expectations aligned with that simple setup.&lt;/p&gt;

&lt;p&gt;If I were reusing this project as a starting point, I would appreciate that the local workflow is minimal. It is just enough to validate the integration without hiding behind a long bootstrap process.&lt;/p&gt;

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

&lt;p&gt;A few future improvements would make sense here, and they follow naturally from the current structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add more documented endpoints where useful&lt;/strong&gt;: the client is already set up for extension, so additional Sportmicro methods would fit cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add runtime validation near the API boundary&lt;/strong&gt;: the app currently works with typed shapes, but validation would make the edge between raw data and UI even safer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache repeated requests on the server&lt;/strong&gt;: tournament detail data is a good candidate if the same tournament is revisited often.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand the detail view&lt;/strong&gt;: the repository already fetches seasons, teams, matches, and cup bracket rows, so a richer presentation of those records would be a natural next step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broaden tests around the helpers and client boundary&lt;/strong&gt;: the sort and filter logic is small enough that coverage would stay readable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would treat those as evolutions, not prerequisites. The current implementation already does the important part: it demonstrates a real integration pattern without hiding the edges of the API.&lt;/p&gt;

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

&lt;p&gt;What I like most about this project is that it treats sports data as a systems problem, not just a UI problem.&lt;/p&gt;

&lt;p&gt;The useful lessons here are not specific to snooker. They are the same ones I would carry into any app that consumes live or semi-live provider data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep provider details isolated&lt;/li&gt;
&lt;li&gt;make empty and error states visible&lt;/li&gt;
&lt;li&gt;sort and filter with the data’s imperfections in mind&lt;/li&gt;
&lt;li&gt;keep secrets on the server&lt;/li&gt;
&lt;li&gt;build a narrow, understandable first version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That approach makes the dashboard feel durable, even if the API is imperfect or the data is incomplete. And for a developer exploring Sportmicro, that is the real value: a clean reference implementation that shows how to build around a live sports data API without turning the app into a pile of fetch calls.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>opensource</category>
      <category>snooker</category>
    </item>
    <item>
      <title>How to Build a Premier League Live Scores Dashboard with Sportmicro</title>
      <dc:creator>Miheve</dc:creator>
      <pubDate>Sat, 26 Sep 2026 10:45:29 +0000</pubDate>
      <link>https://dev.to/mihailove123/how-to-build-a-premier-league-live-scores-dashboard-with-sportmicro-923</link>
      <guid>https://dev.to/mihailove123/how-to-build-a-premier-league-live-scores-dashboard-with-sportmicro-923</guid>
      <description>&lt;p&gt;When I started this project, the problem I wanted to solve was narrow but realistic: build a Premier League live scores dashboard that behaves like a real product, not a toy demo. Sports data is messy in ways that generic API examples often ignore. Matches can be live, finished, postponed, or interrupted. Fields can be missing. A response can be empty and still be perfectly valid.&lt;/p&gt;

&lt;p&gt;So the goal of this build was not “show some football cards.” It was to create a clean, production-style dashboard that demonstrates how to use &lt;a href="https://sportmicro.com" rel="noopener noreferrer"&gt;Sportmicro&lt;/a&gt; in a practical Next.js application, with a data layer that stays reusable and a UI that makes state changes explicit.&lt;/p&gt;

&lt;p&gt;If you want to inspect the source, &lt;a href="https://github.com/monka7monka7/premier-league-live-scores-dashboard-sportmicro" rel="noopener noreferrer"&gt;View the repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the app
&lt;/h2&gt;

&lt;p&gt;I kept the architecture intentionally small. The repository is a Next.js app using React, TypeScript, and Tailwind CSS, but the important part is how the responsibilities are split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/lib/sportmicro.ts&lt;/code&gt; owns Sportmicro-specific data access&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/page.tsx&lt;/code&gt; focuses on rendering the dashboard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/layout.tsx&lt;/code&gt; provides the app metadata and document shell&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/page.test.ts&lt;/code&gt; and &lt;code&gt;src/lib/sportmicro.test.ts&lt;/code&gt; cover the score normalization behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters because sports API code tends to accumulate tiny decisions: how to authenticate, what to do when the API is missing, how to display nullable scores, and how to present status values. I wanted all of that to live in one place instead of leaking into the page component.&lt;/p&gt;

&lt;p&gt;The app currently centers on one documented live-data workflow: fetching live Premier League matches and rendering them as cards with status, teams, scores, and kickoff time. The page also includes clear handling for empty and error states, which is where these dashboards often become useful or frustrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sportmicro integration: keep the boundary small
&lt;/h2&gt;

&lt;p&gt;The integration is built around a small client module. That module uses the default base URL &lt;code&gt;https://football.sportmicro.com&lt;/code&gt;, reads &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;, and exposes a couple of fetch helpers. The live dashboard uses the documented &lt;code&gt;/matches-live&lt;/code&gt; endpoint, and the client also includes a &lt;code&gt;getMatchesByDateLeague&lt;/code&gt; helper for the &lt;code&gt;/matches-by-date-league&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;That client is doing more than just wrapping &lt;code&gt;fetch&lt;/code&gt;. It is the boundary between the UI and the API.&lt;/p&gt;

&lt;p&gt;A simplified 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;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="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;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="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="s1"&gt;SPORTMICRO_API_KEY is not configured.&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="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="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;10&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;`Sportmicro request failed with 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="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="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 practical value here is not the syntax. It is the discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the UI never needs to know how authentication works&lt;/li&gt;
&lt;li&gt;the UI never needs to know the base URL&lt;/li&gt;
&lt;li&gt;missing credentials fail clearly&lt;/li&gt;
&lt;li&gt;upstream failures surface as readable errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the dashboard easier to reason about and easier to extend later. If I add another Sportmicro endpoint, I can do it in the same client without changing the rest of the app’s structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation flow: from API response to dashboard state
&lt;/h2&gt;

&lt;p&gt;The app flow is straightforward, but each step exists for a reason.&lt;/p&gt;

&lt;p&gt;First, the page calls &lt;code&gt;getLiveMatches()&lt;/code&gt; inside the server component. The page starts with an empty array and a nullable error string, then swaps those values depending on the outcome of the request. That gives the UI three meaningful branches: error, empty, and data.&lt;/p&gt;

&lt;p&gt;Second, each match is rendered as a card with a status pill, team names, scores, and kickoff time. The page reads fields defensively with fallback text like &lt;code&gt;Home team&lt;/code&gt;, &lt;code&gt;Away team&lt;/code&gt;, and &lt;code&gt;unknown&lt;/code&gt;, which is important because live sports responses are rarely perfectly complete.&lt;/p&gt;

&lt;p&gt;Third, scores are normalized before rendering. The project keeps this deliberately boring: &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;, and empty string become &lt;code&gt;—&lt;/code&gt;, while real values are stringified.&lt;/p&gt;

&lt;p&gt;That logic is small enough to be tested directly, which is exactly what I wanted. The tests in the repository confirm two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing values become a placeholder&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; is preserved as a legitimate display value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second point is subtle but important. In sports interfaces, zero is data. It should never disappear just because it is falsy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard UI: simple, responsive, and honest
&lt;/h2&gt;

&lt;p&gt;The dashboard page uses Tailwind CSS for a responsive layout, but I was careful not to let styling distract from the data model. The UI has a headline, a “Live matches” section, and a grid of match cards. There is also a search input in the layout, though the current code shows it as interface scaffolding rather than a fully wired filter.&lt;/p&gt;

&lt;p&gt;The most useful part of the UI is not the cards themselves; it is how clearly the page communicates state.&lt;/p&gt;

&lt;p&gt;If Sportmicro returns nothing, the dashboard explains that no live matches were returned and that this can be expected when the Premier League is not active or when the configured account has no data available. If the request fails, the page shows an alert-like error message and points the user toward environment variables and API reachability.&lt;/p&gt;

&lt;p&gt;That distinction is what makes the app feel like a real integration. Empty is not error. Error is not empty. And a missing API key is neither of those; it is a configuration problem that should be obvious immediately.&lt;/p&gt;

&lt;p&gt;I also appreciated the way the app keeps the data visualization restrained. Each card shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a status pill&lt;/li&gt;
&lt;li&gt;team names&lt;/li&gt;
&lt;li&gt;home and away scores&lt;/li&gt;
&lt;li&gt;kickoff time&lt;/li&gt;
&lt;li&gt;a stable identifier when available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough to demonstrate a usable live scores interface without over-committing to a design that would be hard to evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup and environment configuration
&lt;/h2&gt;

&lt;p&gt;The repository supports a normal local setup, and the evidence in the project makes that part clear.&lt;/p&gt;

&lt;p&gt;The README documents 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;npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env.local
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app expects &lt;code&gt;SPORTMICRO_API_KEY&lt;/code&gt;, and &lt;code&gt;src/types/global.d.ts&lt;/code&gt; also defines &lt;code&gt;SPORTMICRO_BASE_URL&lt;/code&gt; as an optional environment variable. The client defaults to &lt;code&gt;https://football.sportmicro.com&lt;/code&gt;, so local setup stays simple unless you explicitly want to override the base URL.&lt;/p&gt;

&lt;p&gt;There are also scripts for build and test:&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;li&gt;&lt;code&gt;npm run typecheck&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I appreciate that the project keeps setup visible instead of hiding it behind helper scripts. For a developer learning an API integration, that is usually the right trade-off.&lt;/p&gt;

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

&lt;p&gt;Because this is a build-focused example, the important constraints were mostly design constraints rather than documented failure stories.&lt;/p&gt;

&lt;p&gt;The first trade-off was scope. The repository intentionally focuses on live matches rather than trying to model every possible Sportmicro football endpoint. That keeps the integration honest and easier to understand.&lt;/p&gt;

&lt;p&gt;The second trade-off is data handling. The code favors explicit fallbacks over assumptions, which means the UI stays predictable when the API returns missing names, nullable scores, or no matches at all. That is less flashy than complex client logic, but it is much safer for a sports dashboard.&lt;/p&gt;

&lt;p&gt;The third trade-off is test coverage. The tests in the repo focus on score normalization, which is a sensible place to start because formatting mistakes are easy to miss and easy to reuse elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project tree
&lt;/h2&gt;

&lt;p&gt;Here is the concise shape of the project:&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/
    globals.css
    layout.tsx
    page.tsx
    page.test.ts
  lib/
    sportmicro.ts
    sportmicro.test.ts
  types/
    global.d.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tree tells the story pretty well. The app is small, the client is isolated, and the tests sit close to the code they verify.&lt;/p&gt;

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

&lt;p&gt;There are a few natural next steps that fit the current structure without changing the project’s direction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Expand the dashboard with more documented Sportmicro endpoints, such as fixtures or standings.&lt;/li&gt;
&lt;li&gt;Add richer runtime validation for API responses so the app can distinguish more match field shapes safely.&lt;/li&gt;
&lt;li&gt;Wire the existing search input into an actual filter model.&lt;/li&gt;
&lt;li&gt;Broaden the tests beyond score normalization to cover page states and client behavior.&lt;/li&gt;
&lt;li&gt;Add a dedicated section for upcoming matches so the app covers more of the match lifecycle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would treat those as future improvements, not as missing pieces. The current project already demonstrates the core integration pattern: fetch live football data, normalize it carefully, and present it in a UI that makes the data state obvious.&lt;/p&gt;

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

&lt;p&gt;The main lesson from this build is that a useful sports dashboard is less about clever UI and more about clear boundaries. Keep the API client separate, treat empty data as a first-class state, normalize the awkward values early, and let the page stay focused on rendering.&lt;/p&gt;

&lt;p&gt;That approach made this Sportmicro example feel like a real application rather than a demo glued to an endpoint. If you are building your own football API project, start with one narrow workflow and make that workflow robust before you widen the scope.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>opensource</category>
      <category>football</category>
    </item>
    <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>
  </channel>
</rss>
