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.
The repository is an open-source example of that shape, powered by Sportmicro. 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, View the repository.
Why a gateway instead of a direct proxy
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:
- credentials appear in too many places,
- response shapes leak into every layer,
- error handling becomes inconsistent.
A gateway solves that by putting the upstream integration behind one service boundary.
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.
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.
The architecture I used
The repository is intentionally small, but the separation of responsibilities is clear.
-
app/config.pyreads settings from the environment. -
app/sportmicro_client.pyhandles outbound HTTP calls to Sportmicro. -
app/services.pymaps supported resources to known Sportmicro endpoints. -
app/main.pydefines the FastAPI app and route behavior. -
app/models.pydefines the query and response structures.
That split is the core of the implementation.
Configuration stays at the edge
Settings reads SPORTMICRO_API_KEY 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 get_settings() is cached with lru_cache(maxsize=1). That means configuration is loaded once and reused.
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.
The client does one job
The SportmicroClient 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 SportmicroClientError when something goes wrong.
A small excerpt shows the shape:
headers = {"Authorization": f"Bearer {self._api_key}"}
response = await self._client.get(url, headers=headers)
response.raise_for_status()
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.
The service layer owns resource mapping
The service layer is where the gateway becomes opinionated. RESOURCE_MAP explicitly lists supported resource keys such as football-teams, football-players, football-matches, basketball-matches, and tennis-rankings. Each key maps to a SportmicroEndpoint with a source, base URL, and path.
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.
The service also translates the query object into upstream parameters. It uses order 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.
How the request flow works
The main request path is straightforward, and that is part of the point.
The app uses a FastAPI lifespan function to create the client once at startup, store the settings and service on app.state, and close the client on shutdown. From there, route handlers can focus on request and response behavior instead of lifecycle management.
The /resources/{resource} route accepts a set of query parameters:
league_idseason_idteam_idplayer_idmatch_idnamedatestatus_typelimitoffsetlangsort
These are assembled into a SportmicroQuery, then handed to SportmicroService.list_resource().
From there, the flow is:
- check whether the requested resource is supported,
- resolve the matching upstream endpoint,
- translate the query into upstream parameters,
- fetch the list from Sportmicro,
- build a response with source, endpoint, count, preview items, filters, and an optional note.
The route also handles errors at the edge. If the client raises SportmicroClientError, the app returns a 502-style JSON response with a detail message and retry_after_seconds: 60. That keeps the gateway honest: the failure is upstream, not local.
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.
Data modeling and response shaping
The repository uses dataclasses instead of a heavier schema stack. That keeps the code easy to follow while still providing structure.
SportmicroQuery is the input shape. It includes defaults for pagination and language, and it has a validate() classmethod that rejects unexpected fields. That method is a small guardrail against accidental input drift.
ResourcePreview is the output shape for the preview list. It intentionally selects a limited set of commonly useful fields:
idnamedescriptionstatus_typestart_timeleague_idseason_idteam_idplayer_idmatch_id
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.
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.
Challenges and trade-offs
The main trade-off here was intentional scope, not a dramatic implementation problem.
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.
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.
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.
Local setup and testing
The repository includes enough evidence to describe local setup safely.
The README shows the expected workflow:
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reload
It also documents the required SPORTMICRO_API_KEY environment variable and optional base URLs for football, basketball, tennis, plus a timeout setting. Once the server is running, the API root, /health, and /docs are available locally.
For validation, the repository also documents two useful checks:
python3 -m compileall -q .
python3 -m unittest discover -s tests
That lines up with the presence of a test suite under tests/, which is a good sign that the project is meant to be exercised rather than just read.
Project shape at a glance
Here is the compact structure I kept in mind while building:
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
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.
What I would improve next
A few future improvements stand out if you wanted to evolve this gateway further:
- add more documented Sportmicro resources to the explicit map,
- expand response modeling for endpoints that have stable, known shapes,
- add caching where upstream update frequency makes sense,
- broaden tests around error paths and empty-state responses,
- consider a richer response contract for consumers that need deeper data than the current preview list.
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.
Takeaway
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.
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.
Top comments (0)