Integrating Music Streaming APIs in React Native and Flutter Apps
Adding music streaming features to a mobile app sounds straightforward until you start building. Each service has its own SDK, its own OAuth flow with mobile-specific redirect URI handling, and its own response format. Multiply that by the number of services your users expect, and you are looking at weeks of integration work per platform.
A unified music API reduces this to one REST integration that works identically in React Native and Flutter. This guide shows you how to set it up in both frameworks, with working code for auth, playlists, and favorites.
Why Mobile Music Integration Is Harder Than It Looks
Mobile music API integration requires handling OAuth redirects through in-app browsers or deep links, managing token storage securely on-device, and adapting to each streaming service's SDK quirks. A unified API approach eliminates per-service SDK dependencies and gives mobile developers a single REST interface that works across iOS and Android with the same codebase.
Platform-Specific SDKs vs. REST APIs: The Mobile Developer's Dilemma
Most streaming services offer native SDKs. Spotify has separate SDKs for iOS and Android. Apple Music has MusicKit for Swift. These SDKs provide deep platform integration (background playback, lock screen controls), but they come with tradeoffs:
| Factor | Native SDKs (per service) | Unified REST API |
|---|---|---|
| Setup time per service | 2-4 weeks | 1-2 days (total) |
| Services supported | 1 per SDK | 12+ with one integration |
| Cross-platform code sharing | Minimal (separate iOS/Android) | Full (same HTTP calls) |
| Maintenance burden | SDK updates per platform per service | One API version to track |
| Auth implementation | Custom per service | Single OAuth flow |
| Offline playback | Yes (SDK-managed) | No (streaming only) |
| React Native / Flutter support | Requires native modules | Works with standard HTTP |
If your app needs background playback and lock screen controls, you will eventually need native SDK integration. But for playlist management, search, favorites, library browsing, and user profile access, a REST API handles everything without native module complexity.
OAuth Flows on Mobile: Redirect URIs, Deep Links, and Token Storage
OAuth on mobile is different from web. Instead of HTTP redirects, you need:
- Custom URL schemes or universal links for redirect URIs (e.g.,
musicapp://auth/callback) - In-app browser (not WebView) for the consent screen, as most services block embedded WebViews
- Secure token storage using the platform keychain (iOS) or encrypted shared preferences (Android)
MusicAPI's auth flow handles the OAuth exchange server-side. Your mobile app only needs to:
- Open the auth URL in an in-app browser
- Catch the redirect back to your app
- Store the resulting session token
No per-service OAuth client registration. No token refresh logic in your app code.
Setting Up MusicAPI in a React Native Project
Setting up MusicAPI in React Native requires installing an HTTP client, configuring an in-app browser for OAuth, and storing auth tokens securely. The entire integration uses standard React Native libraries with no native module linking required beyond the auth browser.
Installing Dependencies and Configuring the API Client
You need three packages:
npm install axios react-native-inappbrowser-reborn react-native-keychain
Create a reusable API client:
// src/api/musicapi.ts
import axios from 'axios';
import * as Keychain from 'react-native-keychain';
const API_BASE = 'https://api.musicapi.com';
const client = axios.create({
baseURL: API_BASE,
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
// Attach user session token to every request
client.interceptors.request.use(async (config) => {
const credentials = await Keychain.getGenericPassword();
if (credentials) {
config.headers['X-User-Token'] = credentials.password;
}
return config;
});
export default client;
Handling OAuth Authentication with In-App Browsers
The auth flow uses MusicAPI's authentication initialization to get a URL, opens it in the system browser, and catches the callback:
// src/auth/connectService.ts
import InAppBrowser from 'react-native-inappbrowser-reborn';
import * as Keychain from 'react-native-keychain';
import client from '../api/musicapi';
export async function connectService(serviceName: string) {
// Step 1: Get the auth URL from MusicAPI
const { data } = await client.post('/auth/initialize', {
service: serviceName,
redirectUri: 'musicapp://auth/callback',
});
// Step 2: Open the OAuth consent screen
const result = await InAppBrowser.openAuth(
data.authUrl,
'musicapp://auth/callback',
{ showTitle: true, enableUrlBarHiding: true }
);
if (result.type === 'success') {
// Step 3: Exchange the callback URL for a session token
const callbackUrl = new URL(result.url);
const { data: session } = await client.post('/auth/callback', {
code: callbackUrl.searchParams.get('code'),
service: serviceName,
});
// Step 4: Store the session token securely
await Keychain.setGenericPassword('musicapi', session.token);
return session;
}
throw new Error('Authentication cancelled');
}
Code Example: Fetching a User's Playlists in React Native
Once authenticated, fetching playlists is a single API call:
// src/hooks/usePlaylists.ts
import { useState, useEffect } from 'react';
import client from '../api/musicapi';
interface Playlist {
id: string;
name: string;
trackCount: number;
service: string;
artwork_url: string;
}
export function usePlaylists(service: string) {
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPlaylists() {
try {
const { data } = await client.get(`/playlists`, {
params: { service, limit: 50 },
});
setPlaylists(data.items);
} catch (error) {
console.error('Failed to fetch playlists:', error);
} finally {
setLoading(false);
}
}
fetchPlaylists();
}, [service]);
return { playlists, loading };
}
The response format is identical whether the playlists come from Spotify, Apple Music, or any other supported service. Your component code never branches on service type.
Setting Up MusicAPI in a Flutter Project
Flutter integration follows the same pattern: an HTTP client, a browser-based OAuth flow, and secure token storage. Dart's strong typing makes the API response handling particularly clean.
Dart HTTP Client Setup and API Configuration
Add dependencies to pubspec.yaml:
dependencies:
http: ^1.2.0
url_launcher: ^6.2.0
flutter_secure_storage: ^9.0.0
uni_links: ^0.5.1
Create the API client:
// lib/services/musicapi_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class MusicApiClient {
static const _baseUrl = 'https://api.musicapi.com';
static const _apiKey = 'YOUR_API_KEY';
final _storage = const FlutterSecureStorage();
Future<Map<String, String>> _headers() async {
final token = await _storage.read(key: 'user_token');
return {
'Authorization': 'Bearer $_apiKey',
if (token != null) 'X-User-Token': token,
'Content-Type': 'application/json',
};
}
Future<Map<String, dynamic>> get(String path,
{Map<String, String>? params}) async {
final uri = Uri.parse('$_baseUrl$path')
.replace(queryParameters: params);
final response = await http.get(uri, headers: await _headers());
return jsonDecode(response.body);
}
Future<Map<String, dynamic>> post(String path,
Map<String, dynamic> body) async {
final response = await http.post(
Uri.parse('$_baseUrl$path'),
headers: await _headers(),
body: jsonEncode(body),
);
return jsonDecode(response.body);
}
}
Managing Auth Tokens with Secure Storage
The authentication callback flow in Flutter uses url_launcher for the browser and uni_links to catch the redirect:
// lib/services/auth_service.dart
import 'package:url_launcher/url_launcher.dart';
import 'package:uni_links/uni_links.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'musicapi_client.dart';
class AuthService {
final _client = MusicApiClient();
final _storage = const FlutterSecureStorage();
Future<void> connectService(String serviceName) async {
// Get the auth URL
final initResponse = await _client.post('/auth/initialize', {
'service': serviceName,
'redirectUri': 'musicapp://auth/callback',
});
// Open the consent screen
await launchUrl(
Uri.parse(initResponse['authUrl']),
mode: LaunchMode.externalApplication,
);
// Listen for the redirect
final link = await linkStream.firstWhere(
(link) => link?.startsWith('musicapp://auth/callback') ?? false,
);
// Exchange the code for a session
final uri = Uri.parse(link!);
final session = await _client.post('/auth/callback', {
'code': uri.queryParameters['code'],
'service': serviceName,
});
// Store the token
await _storage.write(key: 'user_token', value: session['token']);
}
}
Code Example: Displaying a User's Favorite Tracks in Flutter
Fetching favorite tracks works the same across all services:
// lib/widgets/favorites_list.dart
import 'package:flutter/material.dart';
import '../services/musicapi_client.dart';
class FavoritesList extends StatefulWidget {
final String service;
const FavoritesList({required this.service, super.key});
@override
State<FavoritesList> createState() => _FavoritesListState();
}
class _FavoritesListState extends State<FavoritesList> {
final _client = MusicApiClient();
List<Map<String, dynamic>> _tracks = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadFavorites();
}
Future<void> _loadFavorites() async {
final data = await _client.get(
'/favorites',
params: {'service': widget.service, 'limit': '50'},
);
setState(() {
_tracks = List<Map<String, dynamic>>.from(data['items']);
_loading = false;
});
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
return ListView.builder(
itemCount: _tracks.length,
itemBuilder: (context, index) {
final track = _tracks[index];
return ListTile(
leading: Image.network(track['artwork_url'], width: 48, height: 48),
title: Text(track['title']),
subtitle: Text(track['artist']),
trailing: Text(track['service']),
);
},
);
}
}
MusicAPI handles OAuth token refresh and service-specific request formatting behind the scenes. Your mobile code stays clean whether users connect Spotify, Apple Music, Tidal, or any of the other supported services.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.
Common Mobile Patterns: Offline Caching, Background Sync, and Pagination
Mobile apps need to handle spotty connectivity, large data sets, and battery-conscious background work. These patterns apply to both React Native and Flutter when working with the MusicAPI REST endpoints.
Caching API Responses for Offline Access
Cache playlist and library data locally so the app remains usable without a connection. In React Native, use AsyncStorage or MMKV. In Flutter, use shared_preferences or Hive:
// React Native: simple cache wrapper
import AsyncStorage from '@react-native-async-storage/async-storage';
async function cachedFetch(key: string, fetcher: () => Promise<any>) {
try {
const fresh = await fetcher();
await AsyncStorage.setItem(key, JSON.stringify(fresh));
return fresh;
} catch {
const cached = await AsyncStorage.getItem(key);
return cached ? JSON.parse(cached) : null;
}
}
Paginating Large Playlists and Libraries
Users with thousands of saved tracks will hit pagination limits. MusicAPI returns paginated responses with cursor-based navigation. Implement infinite scroll by tracking the pagination cursor:
async function loadMore(service: string, cursor?: string) {
const params: Record<string, string> = { service, limit: '50' };
if (cursor) params.cursor = cursor;
const { data } = await client.get('/favorites', { params });
return {
items: data.items,
nextCursor: data.pagination?.nextCursor,
hasMore: data.pagination?.hasMore ?? false,
};
}
Handling Network Errors Gracefully on Mobile
Mobile networks are unreliable. Implement retry logic with exponential backoff and respect rate limits:
async function fetchWithRetry(fn: () => Promise<any>, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'] || 1;
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (attempt === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
FAQ
Do I need separate API keys for React Native and Flutter?
No. MusicAPI uses a single API key per application. The same key works for both platforms. Manage keys in your MusicAPI dashboard.
Does MusicAPI provide native SDKs for iOS and Android?
MusicAPI is a REST API, not a native SDK. This is intentional: REST works identically across React Native, Flutter, and any other framework that can make HTTP requests. No native module linking required.
How do I handle OAuth redirects on mobile?
Use an in-app browser (not WebView) to open MusicAPI's auth URL. Register a custom URL scheme (yourapp://auth/callback) in your app config, and catch the redirect using deep link handlers (react-native-inappbrowser-reborn for RN, uni_links for Flutter).
Can I use MusicAPI with Expo?
Yes. MusicAPI is a standard REST API, so it works with Expo's fetch or any HTTP library. The only native module dependency is for OAuth (in-app browser), which is available in Expo via expo-web-browser and expo-linking.
What happens when a user's streaming service token expires?
MusicAPI handles token refresh automatically. If a service token expires, the API refreshes it on the next request. Your mobile app never sees expired token errors. See the authorization docs for details.
How do I support multiple streaming services in the same app?
After a user connects each service, pass the service parameter to specify which one to query. You can also omit the service parameter on search endpoints to query all connected services at once.
Is there a rate limit for mobile apps?
Yes. MusicAPI applies rate limits per API key. For mobile apps, implement client-side debouncing on search and cache responses to stay well within limits.
