Skip to main content

Integrating Music Streaming APIs in React Native and Flutter Apps

Published on August 8, 2026

Integrating Music Streaming APIs in React Native and Flutter Apps

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:

FactorNative SDKs (per service)Unified REST API
Setup time per service2-4 weeks1-2 days (total)
Services supported1 per SDK12+ with one integration
Cross-platform code sharingMinimal (separate iOS/Android)Full (same HTTP calls)
Maintenance burdenSDK updates per platform per serviceOne API version to track
Auth implementationCustom per serviceSingle OAuth flow
Offline playbackYes (SDK-managed)No (streaming only)
React Native / Flutter supportRequires native modulesWorks 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:

  1. Custom URL schemes or universal links for redirect URIs (e.g., musicapp://auth/callback)
  2. In-app browser (not WebView) for the consent screen, as most services block embedded WebViews
  3. 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:

  1. Open the auth URL in an in-app browser
  2. Catch the redirect back to your app
  3. 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.