Skip to main content

Building a Music Discovery App with MusicAPI and React

Published on March 26, 2026

Building a Music Discovery App with MusicAPI and React

Music discovery apps let users search for songs, explore artists, and preview tracks before committing to a full listen. Building one from scratch teaches you how to work with real-world music data at scale.

In this tutorial, you will build a music discovery web app using React and MusicAPI. By the end, you will have a working application that searches across multiple streaming platforms, displays artist profiles, and plays 30-second track previews.

What You Will Build

The finished app includes three core features:

  1. Search - A search bar that queries MusicAPI for tracks, artists, and albums across platforms like Spotify, Apple Music, and Tidal.
  2. Artist Profiles - Detailed artist pages showing bios, top tracks, and discography data pulled from multiple sources.
  3. Track Previews - Inline audio players that stream 30-second previews directly in the browser.

Prerequisites

Before you start, make sure you have:

  • Node.js 18+ installed
  • A MusicAPI account with an active API key (sign up at musicapi.com)
  • Basic familiarity with React hooks and functional components

Project Setup

Scaffold a new React project with Vite and install the dependencies you need:

npm create vite@latest music-discovery -- --template react
cd music-discovery
npm install axios
npm install -D tailwindcss @tailwindcss/vite

Configure Tailwind in your vite.config.js:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

Add your MusicAPI key to a .env file in the project root:

VITE_MUSICAPI_KEY=your_api_key_here

Setting Up the API Client

Create a dedicated API module to keep your MusicAPI calls organized. This module handles authentication and provides typed helper functions for common queries.

Create src/api/musicapi.js:

import axios from 'axios'

const client = axios.create({
  baseURL: 'https://api.musicapi.com/v2',
  headers: {
    Authorization: `Bearer ${import.meta.env.VITE_MUSICAPI_KEY}`,
    'Content-Type': 'application/json',
  },
})

export async function searchTracks(query, limit = 20) {
  const response = await client.get('/search', {
    params: { q: query, type: 'track', limit },
  })
  return response.data.results
}

export async function getArtist(artistId, platform = 'spotify') {
  const response = await client.get(`/artists/${artistId}`, {
    params: { platform },
  })
  return response.data
}

export async function getArtistTopTracks(artistId, platform = 'spotify') {
  const response = await client.get(`/artists/${artistId}/top-tracks`, {
    params: { platform, limit: 10 },
  })
  return response.data.tracks
}

export async function getTrackPreviews(trackId) {
  const response = await client.get(`/tracks/${trackId}/previews`)
  return response.data
}

MusicAPI normalizes data from Spotify, Apple Music, Tidal, and other platforms into a single unified schema. This means you write one set of API calls and get results from every connected platform without handling each provider's unique response format.

Building the Search Component

The search component is the entry point for your app. It sends user queries to MusicAPI and renders results as a scrollable list of track cards.

Create src/components/SearchBar.jsx:

import { useState } from 'react'
import { searchTracks } from '../api/musicapi'

export default function SearchBar({ onResults, onLoading }) {
  const [query, setQuery] = useState('')

  async function handleSearch(e) {
    e.preventDefault()
    if (!query.trim()) return

    onLoading(true)
    try {
      const results = await searchTracks(query)
      onResults(results)
    } catch (error) {
      console.error('Search failed:', error.message)
      onResults([])
    } finally {
      onLoading(false)
    }
  }

  return (
    <form onSubmit={handleSearch} className="flex gap-2 w-full max-w-2xl mx-auto">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search for songs, artists, or albums..."
        className="flex-1 px-4 py-3 rounded-lg border border-gray-300
                   focus:outline-none focus:ring-2 focus:ring-indigo-500"
      />
      <button
        type="submit"
        className="px-6 py-3 bg-indigo-600 text-white rounded-lg
                   hover:bg-indigo-700 transition-colors"
      >
        Search
      </button>
    </form>
  )
}

Now create src/components/TrackList.jsx to display the search results:

import TrackCard from './TrackCard'

export default function TrackList({ tracks, onSelectArtist }) {
  if (tracks.length === 0) {
    return (
      <p className="text-center text-gray-500 mt-8">
        No tracks found. Try a different search term.
      </p>
    )
  }

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-8">
      {tracks.map((track) => (
        <TrackCard
          key={track.id}
          track={track}
          onSelectArtist={onSelectArtist}
        />
      ))}
    </div>
  )
}

Creating the Track Card with Audio Preview

Each track card displays the song title, artist name, album art, and a built-in audio player for the 30-second preview. MusicAPI returns preview URLs directly in the search response, so you do not need a separate API call.

Create src/components/TrackCard.jsx:

import { useRef, useState } from 'react'

export default function TrackCard({ track, onSelectArtist }) {
  const audioRef = useRef(null)
  const [isPlaying, setIsPlaying] = useState(false)

  function togglePlay() {
    if (!track.previewUrl) return

    if (isPlaying) {
      audioRef.current.pause()
    } else {
      audioRef.current.play()
    }
    setIsPlaying(!isPlaying)
  }

  return (
    <div className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow">
      <div className="relative">
        <img
          src={track.albumArt || '/placeholder-album.png'}
          alt={`${track.title} album art`}
          className="w-full h-48 object-cover"
        />
        {track.previewUrl && (
          <button
            onClick={togglePlay}
            className="absolute bottom-3 right-3 w-12 h-12 bg-indigo-600
                       rounded-full flex items-center justify-center text-white
                       hover:bg-indigo-700 transition-colors shadow-lg"
            aria-label={isPlaying ? 'Pause preview' : 'Play preview'}
          >
            {isPlaying ? '⏸' : '▶'}
          </button>
        )}
      </div>

      <div className="p-4">
        <h3 className="font-semibold text-lg truncate">{track.title}</h3>
        <button
          onClick={() => onSelectArtist(track.artistId)}
          className="text-indigo-600 hover:underline text-sm"
        >
          {track.artistName}
        </button>
        <p className="text-gray-500 text-sm mt-1">{track.albumName}</p>
        <div className="flex items-center gap-2 mt-2">
          {track.platforms?.map((platform) => (
            <span
              key={platform}
              className="text-xs bg-gray-100 px-2 py-1 rounded-full text-gray-600"
            >
              {platform}
            </span>
          ))}
        </div>
      </div>

      {track.previewUrl && (
        <audio
          ref={audioRef}
          src={track.previewUrl}
          onEnded={() => setIsPlaying(false)}
        />
      )}
    </div>
  )
}

Notice the platforms array in the track data. MusicAPI tells you which streaming services carry each track, so your app can show users exactly where they can listen to the full song.

Building the Artist Profile View

When a user clicks an artist name, the app fetches detailed artist information and their top tracks. MusicAPI aggregates data from all connected platforms, giving you richer artist profiles than any single source provides.

Create src/components/ArtistProfile.jsx:

import { useState, useEffect } from 'react'
import { getArtist, getArtistTopTracks } from '../api/musicapi'

export default function ArtistProfile({ artistId, onBack }) {
  const [artist, setArtist] = useState(null)
  const [topTracks, setTopTracks] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    async function fetchArtistData() {
      setLoading(true)
      try {
        const [artistData, tracks] = await Promise.all([
          getArtist(artistId),
          getArtistTopTracks(artistId),
        ])
        setArtist(artistData)
        setTopTracks(tracks)
      } catch (error) {
        console.error('Failed to load artist:', error.message)
      } finally {
        setLoading(false)
      }
    }

    fetchArtistData()
  }, [artistId])

  if (loading) {
    return <div className="text-center py-12">Loading artist info...</div>
  }

  if (!artist) {
    return <div className="text-center py-12">Artist not found.</div>
  }

  return (
    <div className="max-w-4xl mx-auto">
      <button
        onClick={onBack}
        className="text-indigo-600 hover:underline mb-6 inline-block"
      >
        ← Back to search
      </button>

      <div className="flex flex-col md:flex-row gap-8 items-start">
        <img
          src={artist.imageUrl}
          alt={artist.name}
          className="w-64 h-64 rounded-2xl object-cover shadow-lg"
        />
        <div>
          <h2 className="text-3xl font-bold">{artist.name}</h2>
          <p className="text-gray-600 mt-2">
            {artist.genres?.join(', ')}
          </p>
          <div className="flex gap-4 mt-4 text-sm text-gray-500">
            <span>{artist.followers?.toLocaleString()} followers</span>
            <span>{artist.monthlyListeners?.toLocaleString()} monthly listeners</span>
          </div>
          {artist.bio && (
            <p className="mt-4 text-gray-700 leading-relaxed">
              {artist.bio}
            </p>
          )}
        </div>
      </div>

      <h3 className="text-xl font-semibold mt-10 mb-4">Top Tracks</h3>
      <div className="space-y-3">
        {topTracks.map((track, index) => (
          <div
            key={track.id}
            className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50"
          >
            <span className="text-gray-400 w-6 text-right">{index + 1}</span>
            <img
              src={track.albumArt}
              alt={track.title}
              className="w-12 h-12 rounded"
            />
            <div className="flex-1">
              <p className="font-medium">{track.title}</p>
              <p className="text-sm text-gray-500">{track.albumName}</p>
            </div>
            <span className="text-sm text-gray-400">
              {Math.floor(track.durationMs / 60000)}:
              {String(Math.floor((track.durationMs % 60000) / 1000)).padStart(2, '0')}
            </span>
          </div>
        ))}
      </div>
    </div>
  )
}

Wiring It All Together

Now connect everything in your App.jsx. The app manages three states: search results, the currently selected artist, and a loading indicator.

Update src/App.jsx:

import { useState } from 'react'
import SearchBar from './components/SearchBar'
import TrackList from './components/TrackList'
import ArtistProfile from './components/ArtistProfile'

export default function App() {
  const [tracks, setTracks] = useState([])
  const [selectedArtist, setSelectedArtist] = useState(null)
  const [loading, setLoading] = useState(false)

  if (selectedArtist) {
    return (
      <div className="min-h-screen bg-gray-50 p-8">
        <ArtistProfile
          artistId={selectedArtist}
          onBack={() => setSelectedArtist(null)}
        />
      </div>
    )
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-white shadow-sm">
        <div className="max-w-6xl mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold text-center mb-6">
            Music Discovery
          </h1>
          <SearchBar onResults={setTracks} onLoading={setLoading} />
        </div>
      </header>

      <main className="max-w-6xl mx-auto px-4 py-8">
        {loading ? (
          <p className="text-center text-gray-500">Searching...</p>
        ) : (
          <TrackList
            tracks={tracks}
            onSelectArtist={setSelectedArtist}
          />
        )}
      </main>
    </div>
  )
}

Run the dev server to see it in action:

npm run dev

Open http://localhost:5173 in your browser, type a search query, and you should see track cards populate with album art, artist names, and playable previews.

Cross-Platform Data: Why MusicAPI Matters

A common challenge when building music apps is handling data from multiple streaming platforms. Spotify, Apple Music, and Tidal each use their own artist IDs, track formats, and metadata schemas. Without a normalization layer, you end up writing and maintaining separate integrations for each provider.

MusicAPI solves this by providing a single API that returns normalized data across all major platforms. When you call /search, the results include matches from every connected service, each mapped to a consistent schema. One artistId works across platforms. One trackId resolves to the same song regardless of source.

This matters for music discovery apps because:

  • Broader catalog coverage - No single platform has every song. By searching across multiple services, your app surfaces tracks that users would miss on any single platform.
  • Accurate metadata - MusicAPI cross-references metadata from multiple sources, reducing errors in artist names, album titles, and genre classifications.
  • Platform-agnostic links - Your app can show users where a track is available and deep-link to their preferred streaming service.

Adding Error Handling and Loading States

Production apps need robust error handling. Here is a custom hook that wraps MusicAPI calls with consistent loading and error state management:

Create src/hooks/useMusicApi.js:

import { useState, useCallback } from 'react'

export default function useMusicApi(apiFunction) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  const execute = useCallback(
    async (...args) => {
      setLoading(true)
      setError(null)
      try {
        const result = await apiFunction(...args)
        setData(result)
        return result
      } catch (err) {
        const message =
          err.response?.status === 429
            ? 'Rate limit reached. Wait a moment and try again.'
            : err.response?.data?.message || 'Something went wrong.'
        setError(message)
        return null
      } finally {
        setLoading(false)
      }
    },
    [apiFunction]
  )

  return { data, loading, error, execute }
}

MusicAPI uses standard HTTP status codes. A 429 means you hit the rate limit (configurable in your MusicAPI dashboard). A 401 means your API key is invalid or expired. Handle these explicitly so your users see helpful messages instead of generic errors.

Next Steps

You now have a working music discovery app that searches across platforms, shows artist profiles, and plays track previews. Here are some ways to extend it:

  • Add album browsing - Use the /albums/{id}/tracks endpoint to show full album track listings.
  • Implement playlist creation - MusicAPI supports creating and managing playlists across connected platforms through the /playlists endpoints.
  • Add music recommendations - The /recommendations endpoint returns personalized track suggestions based on seed artists or tracks.
  • Server-side rendering - Move API calls to a Next.js backend to keep your API key out of client-side code. In a production deployment, you should never expose your MusicAPI key in the browser.
  • Caching - MusicAPI responses include standard cache headers. Add a caching layer (React Query or SWR) to reduce redundant API calls and speed up repeat searches.

Source Code

The complete source code for this tutorial is available on GitHub. Clone the repo, add your MusicAPI key, and start experimenting:

git clone https://github.com/musicapi/music-discovery-react-tutorial
cd music-discovery-react-tutorial
npm install
npm run dev

For the full MusicAPI documentation, including authentication guides, rate limit details, and endpoint references, visit musicapi.com/docs.