Building a Smart Music Search with iTunes API, Next.js, and a Flask Proxy
When building MelodyOne, a full-stack, glassmorphic music streaming web application, one of the biggest challenges wasn't styling or responsive design—it was building a fast, reliable, and decoupled audio search experien
When building MelodyOne, a full-stack, glassmorphic music streaming web application, one of the biggest challenges wasn't styling or responsive design—it was building a fast, reliable, and decoupled audio search experience.
In this article, I'll walk you through how I architected a smart music discovery feature using the iTunes Search API, wrapped in a rate-limited Python Flask proxy, and seamlessly consumed by a Next.js 16 App Router frontend with global player state.
1. The Problem
Connecting a modern frontend directly to public media APIs (like iTunes, Spotify, or YouTube) from the browser leads straight to three major architectural roadblocks:
- CORS (Cross-Origin Resource Sharing) Hell: Direct browser-to-API calls frequently trigger CORS errors or get blocked by ad blockers and strict tracking prevention.
- Rate Limiting & IP Bans: If 500 active users search simultaneously from the browser, client-side IP abuse or throttling can break the user experience with zero graceful fallbacks.
- Payload Bloat & Low-Res Artwork: Raw search responses return excessive metadata and tiny 100x100 thumbnails that look pixelated on modern Retina screens.
The Solution? A decoupled proxy architecture.
The frontend talks solely to our own lightweight Python Flask microservice, which handles upstream caching, data normalization (e.g., automatically bumping thumbnails from 100x100 to crisp 600x600 HD), rate limiting, and error handling.
2. The Architecture at a Glance
[ User in Browser ]
│
├── Types query ("Starboy") + Hits Enter
▼
[ Next.js 16 (App Router on Vercel) ]
│
├── GET /api/search?song=Starboy (CORS-safe)
▼
[ Flask Microservice (Render) + Flask-Limiter ]
│
├── Fetches from iTunes API (10s Timeout)
├── Normalizes Artwork (100x100 ➔ 600x600)
├── Extracts Direct Preview Stream (.m4a)
▼
[ Global Player Context (HTML5 Audio) ] ──▶ Seamless Auto-Play!
3. Code Snippets: How It Works
Snippet 1: The Resilient Flask Search Proxy (backend/run.py)
Here is our production Flask endpoint. It enforces an IP-based rate limit of 30 requests per minute, queries the iTunes Search API with a 10-second timeout, and returns only the clean fields our UI needs:
import requests
from flask import Flask, request, jsonify
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})
limiter = Limiter(get_remote_address, app=app, default_limits=["200 per day", "50 per hour"])
@app.route('/api/search', methods=['GET'])
@limiter.limit("30 per minute")
def search_song():
query = request.args.get('song')
if not query:
return jsonify({"error": "Search query is required"}), 400
try:
url = f"https://itunes.apple.com/search?term={requests.utils.quote(query)}&limit=1&entity=song"
response = requests.get(url, timeout=10)
data = response.json()
if data.get('resultCount', 0) == 0:
return jsonify({"error": "Track not found"}), 404
track = data['results'][0]
# Normalize metadata & transform thumbnail to HD 600x600
result = {
"videoId": str(track['trackId']),
"title": track['trackName'],
"artist": track['artistName'],
"thumbnail": track.get('artworkUrl100', '').replace('100x100', '600x600'),
"stream_url": track.get('previewUrl', '')
}
return jsonify(result), 200
except requests.exceptions.Timeout:
return jsonify({"error": "Search service timed out. Please try again."}), 504
except Exception as e:
return jsonify({"error": "Backend failed to fetch audio stream"}), 500
Snippet 2: Next.js Frontend Search Handler (frontend/src/pages/Home.tsx)
On the client side, the search bar triggers cleanly when the user hits Enter, with instant loading states and clear error feedback:
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null);
const [searchedSong, setSearchedSong] = useState<Song | null>(null);
const { playSong } = usePlayer();
const handleSearch = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && query.trim() !== '') {
setLoading(true);
setSearchError(null);
setSearchedSong(null);
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/search?song=${encodeURIComponent(query)}`
);
const data = await response.json();
if (data.error) {
setSearchError(data.error);
} else {
setSearchedSong(data);
}
} catch (err) {
setSearchError("Failed to connect to streaming backend. Please check your network.");
} finally {
setLoading(false);
}
}
};
Snippet 3: Seamless Audio Queue & Playback (PlayerContext.tsx)
Once a track is fetched, playing it is just a single call to our global PlayerContext. It manages HTML5 Audio, tracks the queue, and automatically prevents duplicate tracks:
const playSong = useCallback((songData: Song) => {
if (!audioRef.current) return;
setQueue((prevQueue) => {
const existingIndex = prevQueue.findIndex(s => s.stream_url === songData.stream_url);
if (existingIndex !== -1) {
setCurrentIndex(existingIndex);
return prevQueue;
}
const newQueue = [...prevQueue, songData];
setCurrentIndex(newQueue.length - 1);
return newQueue;
});
}, []);
4. The Challenges & How We Solved Them
1. The Blurry Album Art Dilemma
iTunes returns low-res 100x100 thumbnails by default (artworkUrl100). Instead of letting blurred artwork ruin our glassmorphic UI, our proxy does a string transformation:
thumbnail = track.get('artworkUrl100', '').replace('100x100', '600x600')
Apple's CDN dynamically delivers the high-resolution 600x600 artwork without any extra latency!
2. Preventing Render Free-Tier Cold Starts
Because the backend is hosted on Render's free tier, it sleeps after 15 minutes of inactivity. We set up an automated cron via cronjob.org that pings our /health endpoint every 10 minutes:
@app.route('/health', methods=['GET'])
def health():
return jsonify({"status": "ok", "service": "melodyone-backend"})
This keeps the Gunicorn worker pool warm 24/7 with zero cold starts for our users.
3. State Synchronization with Clerk Auth & MongoDB
When an authenticated user clicks the "Like" button (♥) on any search result, the track is synced instantly to MongoDB Atlas without interrupting active playback.
5. Key Takeaways
- Never call third-party media APIs directly from browser client components if you care about security, rate-limiting, and payload optimization.
- Micro-proxies keep frontends fast: A lightweight Flask or FastAPI proxy gives you full control over caching, headers, and data sanitation.
- Decoupled state wins: Keeping the search UI separate from the audio playback engine ensures users can browse, search, and navigate while music plays uninterrupted.
🔗 Links & Resources
- 🌐 Live Demo: MelodyOne on Vercel
- 💻 Source Code: GitHub Repository
- 👨💻 Developer Portfolio: Shubham Raj Portfolio
Found this helpful? Drop a reaction or let me know your thoughts in the comments below!
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.