<?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: Nidhal Dalhoumi</title>
    <description>The latest articles on DEV Community by Nidhal Dalhoumi (@nidhaldal).</description>
    <link>https://dev.to/nidhaldal</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%2F4119049%2F2c42cd12-2e15-480a-a57b-9d8c6d3510ff.jpg</url>
      <title>DEV Community: Nidhal Dalhoumi</title>
      <link>https://dev.to/nidhaldal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nidhaldal"/>
    <language>en</language>
    <item>
      <title>Building a Fast Multi-Source Music Discovery Pipeline with FastAPI</title>
      <dc:creator>Nidhal Dalhoumi</dc:creator>
      <pubDate>Thu, 10 Sep 2026 10:31:46 +0000</pubDate>
      <link>https://dev.to/nidhaldal/building-a-fast-multi-source-music-discovery-pipeline-with-fastapi-3ai5</link>
      <guid>https://dev.to/nidhaldal/building-a-fast-multi-source-music-discovery-pipeline-with-fastapi-3ai5</guid>
      <description>&lt;p&gt;When building TheExperience, I wanted the application to do more than search for albums.&lt;/p&gt;

&lt;p&gt;The idea was to take an album search and turn it into a complete interactive experience starting from finding the album, retrieving reliable metadata, resolving its artwork, extracting its visual identity and finally using the information to drive the frontend.&lt;/p&gt;

&lt;p&gt;The challenge was that no single API provides everything I needed.&lt;/p&gt;

&lt;p&gt;The backend integrates &lt;strong&gt;Last.fm&lt;/strong&gt;, &lt;strong&gt;MusicBrainz&lt;/strong&gt;, and the &lt;strong&gt;Cover Art Archive&lt;/strong&gt;, each with different APIs, identifiers, response times and failure modes.&lt;/p&gt;

&lt;p&gt;The interesting engineering problem therefore became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do you combine several external APIs without making every search slow, wasteful, or fragile?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The solution was to separate discovery from enrichment, reuse identifiers whenever possible, execute independent requests concurrently, and treat external services as unreliable dependencies rather than extensions of the application itself.&lt;/p&gt;

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

&lt;p&gt;The backend is built with FastAPI and uses asynchronous HTTP clients to communicate with the external services.&lt;/p&gt;

&lt;p&gt;The overall flow 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;User searches for an album
        ↓
GET /albums/search
        ↓
Last.fm search
        ↓
Local ranking + result limiting
        ↓
Lightweight album results
        ↓
User selects an album
        ↓
GET /albums/details
        ↓
 ┌───────────────────────┐
 │                       │
Last.fm              MusicBrainz
popularity            enrichment
 │                       │
 └───────────┬───────────┘
             ↓
        Normalized Album
             ↓
        Cover Art Archive
             ↓
       Complete album data
             ↓
       React visual layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of the first decisions I made was to &lt;strong&gt;avoid doing all of this work during search&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Search is not enrichment
&lt;/h2&gt;

&lt;p&gt;Album autocomplete needs to feel lightweight.&lt;/p&gt;

&lt;p&gt;If every keystroke triggered Last.fm, MusicBrainz, and Cover Art requests, the application would perform unnecessary work before the user had even selected an album.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;/albums/search&lt;/code&gt; only performs the lightweight Last.fm search.&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;lastfm_candidates&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;search_albums_lastfm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;albums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Album&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;artist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;listeners&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;listeners&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;playcount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;playcount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;album&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lastfm_candidates&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;album&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend receives a larger set of candidates from Last.fm, ranks them locally according to title and artist relevance, and returns a limited result set.&lt;/p&gt;

&lt;p&gt;The deeper metadata pipeline only starts when the user selects an album.&lt;/p&gt;

&lt;p&gt;This creates two distinct stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discovery
---------
Last.fm search
      ↓
Local ranking
      ↓
Limited Album[]


Enrichment
----------
Last.fm popularity ─────┐
                        ├──→ Normalized Album
MusicBrainz ────────────┘
                        ↓
                    Cover Art
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation reduced unnecessary external requests and made the search endpoint much simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reusing identifiers instead of searching again
&lt;/h2&gt;

&lt;p&gt;One of the more interesting problems was matching data between Last.fm and MusicBrainz.&lt;/p&gt;

&lt;p&gt;Last.fm can sometimes provide a &lt;strong&gt;MusicBrainz ID (MBID)&lt;/strong&gt; for an album.&lt;/p&gt;

&lt;p&gt;When that identifier exists, there is no reason to search MusicBrainz again using the album title and artist.&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;musicbrainz_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;find_album_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;musicbrainz_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;artist&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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;find_album&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;artist&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;Instead, I can resolve the album directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Last.fm result
      │
      ├── MusicBrainz ID exists
      │        ↓
      │   Direct release lookup
      │        ↓
      │   Release + release-group
      │
      └── No MBID
               ↓
        Release-group search
               ↓
          Match candidate
               ↓
          Release lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The direct path requires one MusicBrainz request.&lt;/p&gt;

&lt;p&gt;The fallback path may require two: one release-group search followed by a release lookup.&lt;/p&gt;

&lt;p&gt;This is a small optimization, but it illustrates an important principle when working with external APIs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If an upstream service has already given you a stable identifier, reuse it instead of performing another search.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When an MBID is available, the lookup can be constructed directly:&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MUSICBRAINZ_RELEASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;musicbrainz_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MusicBrainz response is then transformed into the application's &lt;code&gt;Album&lt;/code&gt; model.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Concurrency — but only where dependencies allow it
&lt;/h2&gt;

&lt;p&gt;Once an album has been selected, some pieces of information can be retrieved independently.&lt;/p&gt;

&lt;p&gt;Last.fm popularity and MusicBrainz metadata do not depend on each other, so running them sequentially would unnecessarily increase latency.&lt;/p&gt;

&lt;p&gt;FastAPI's asynchronous model makes it possible to execute these operations concurrently with &lt;code&gt;asyncio.gather()&lt;/code&gt;.&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;popularity_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;get_album_popularity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;artist&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="n"&gt;musicbrainz_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;find_album_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;musicbrainz_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;artist&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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;find_album&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;popularity_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_album&lt;/span&gt;&lt;span class="p"&gt;,&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="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;popularity_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_task&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 Cover Art request is different.&lt;/p&gt;

&lt;p&gt;The Cover Art Archive lookup needs a specific MusicBrainz &lt;strong&gt;release ID&lt;/strong&gt;, so it cannot start until MusicBrainz resolution has produced that identifier.&lt;/p&gt;

&lt;p&gt;The pipeline is therefore not simply "make everything concurrent."&lt;/p&gt;

&lt;p&gt;It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Last.fm popularity ──────┐
                         ├── concurrent
MusicBrainz resolution ──┘
                         ↓
                    release_id
                         ↓
                     Cover Art
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cover_url&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;get_cover_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;musicbrainz_album&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release_id&lt;/span&gt;
    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;musicbrainz_album&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction matters because asynchronous code is not automatically faster. The goal is to identify which operations are independent and parallelize only those.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. External APIs need their own failure architecture
&lt;/h2&gt;

&lt;p&gt;External services can fail for reasons that have nothing to do with the application.&lt;/p&gt;

&lt;p&gt;A timeout, temporary service outage, invalid response, or network error should not necessarily turn the entire album experience into a server error.&lt;/p&gt;

&lt;p&gt;For MusicBrainz, I added a small asynchronous request layer responsible for rate limiting, timeouts, retry handling, and converting recoverable failures into empty results.&lt;/p&gt;

&lt;p&gt;MusicBrainz requests are coordinated with a module-level asynchronous lock and a minimum one-second interval between scheduled requests within the process.&lt;/p&gt;

&lt;p&gt;The service also retries a &lt;code&gt;503 Service Unavailable&lt;/code&gt; response once after a two-second delay.&lt;/p&gt;

&lt;p&gt;Other failures, such as timeouts or request errors, are converted into empty results so the application can continue where possible.&lt;/p&gt;

&lt;p&gt;This is deliberately provider-specific rather than pretending that every API behaves the same way.&lt;/p&gt;

&lt;p&gt;For example, Last.fm operations have different failure semantics, while Cover Art is treated as non-critical because missing artwork should not prevent an album's metadata from being displayed.&lt;/p&gt;

&lt;p&gt;That leads to a useful design principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The importance of an external dependency should determine how its failure is handled.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Album metadata is important.&lt;/p&gt;

&lt;p&gt;Album artwork is useful, but optional.&lt;/p&gt;

&lt;p&gt;Those two failures should not have identical consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Normalizing data from different systems
&lt;/h2&gt;

&lt;p&gt;Another challenge was that the three services don't share the same data model.&lt;/p&gt;

&lt;p&gt;Last.fm provides search and popularity information.&lt;/p&gt;

&lt;p&gt;MusicBrainz provides structured music metadata and identifiers.&lt;/p&gt;

&lt;p&gt;The Cover Art Archive provides artwork associated with MusicBrainz releases.&lt;/p&gt;

&lt;p&gt;Rather than passing provider-specific response objects throughout the application, the backend converts the results into a common &lt;code&gt;Album&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;MusicBrainz also distinguishes between a &lt;strong&gt;release group&lt;/strong&gt; and an individual &lt;strong&gt;release&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The application keeps both concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Album
├── id          → release-group identity
├── release_id  → specific release
├── title
├── artist
└── year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release-group represents the album-level identity, while the specific release ID is useful when retrieving artwork.&lt;/p&gt;

&lt;p&gt;This normalization keeps the frontend independent from the details of each external API.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Measuring instead of guessing
&lt;/h2&gt;

&lt;p&gt;Another useful addition during development was instrumentation.&lt;/p&gt;

&lt;p&gt;I used Python's &lt;code&gt;time.perf_counter()&lt;/code&gt; around provider requests and larger sections of the enrichment pipeline.&lt;/p&gt;

&lt;p&gt;This made it possible to see where time was actually being spent instead of treating the entire backend request as one opaque operation.&lt;/p&gt;

&lt;p&gt;For example, during local testing, cached Last.fm searches were effectively instantaneous, while uncached searches were typically below roughly 1.5 seconds in my development environment.&lt;/p&gt;

&lt;p&gt;Album enrichment was more variable because it depends heavily on external provider latency, particularly artwork retrieval.&lt;/p&gt;

&lt;p&gt;These numbers are &lt;strong&gt;development observations, not production benchmarks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The more important result was having visibility into individual stages of the pipeline.&lt;/p&gt;

&lt;p&gt;Once external calls are measured independently, optimization becomes much more concrete.&lt;/p&gt;

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

&lt;p&gt;The current implementation works well for the project's scale, but there are several areas I would improve before treating the architecture as production-grade.&lt;/p&gt;

&lt;p&gt;First, the rate limiter is process-local. A distributed deployment would need a shared mechanism if multiple backend instances were making MusicBrainz requests.&lt;/p&gt;

&lt;p&gt;Second, retry handling could be expanded to account for rate-limit responses such as &lt;code&gt;429&lt;/code&gt;, with more sophisticated backoff behavior.&lt;/p&gt;

&lt;p&gt;Third, the in-memory caches could eventually be replaced or supplemented with a shared cache if the application needed to run across multiple instances.&lt;/p&gt;

&lt;p&gt;Finally, album matching could become more sophisticated for ambiguous metadata instead of relying on normalized exact title and artist matching in the MusicBrainz fallback path.&lt;/p&gt;

&lt;p&gt;These are not problems that need to be solved simply because they exist. They are the next engineering trade-offs that would become relevant as scale and reliability requirements increase.&lt;/p&gt;

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

&lt;p&gt;The hardest part of integrating several APIs was not learning how to send HTTP requests.&lt;/p&gt;

&lt;p&gt;It was deciding &lt;strong&gt;when&lt;/strong&gt; to make those requests, &lt;strong&gt;which identifiers&lt;/strong&gt; to reuse, &lt;strong&gt;which operations&lt;/strong&gt; could run concurrently, and &lt;strong&gt;what should happen when a dependency fails&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The resulting architecture is relatively simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep search lightweight.&lt;/li&gt;
&lt;li&gt;Enrich only after the user selects an album.&lt;/li&gt;
&lt;li&gt;Reuse MusicBrainz IDs whenever available.&lt;/li&gt;
&lt;li&gt;Run independent I/O concurrently.&lt;/li&gt;
&lt;li&gt;Respect provider-specific constraints.&lt;/li&gt;
&lt;li&gt;Normalize external data into an application-owned model.&lt;/li&gt;
&lt;li&gt;Treat optional dependencies as optional.&lt;/li&gt;
&lt;li&gt;Measure external latency instead of guessing where the bottleneck is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For me, this project became a useful exercise in moving from simply integrating APIs to thinking about the behavior of a system around those APIs.&lt;/p&gt;

&lt;p&gt;And that is ultimately what I found most interesting about building TheExperience: the visual experience is what you see, but the engineering challenge underneath it is making several independent systems behave like one coherent application.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; TheExperience — an interactive music discovery and visual album experience built with React, Angular, TypeScript, FastAPI, and external music metadata services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status:&lt;/strong&gt; Active development.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
