Skip to main content

How to Build a Cross-Platform Music App with a Single API Integration

Published on May 13, 2026

How to Build a Cross-Platform Music App with a Single API Integration

Building a music app that works across Spotify, Apple Music, YouTube Music, Tidal, Deezer, and more means dealing with 10+ different APIs, authentication flows, and data formats. Or it means using one API that handles all of them.

This post walks through building a cross-platform music app from scratch using a single API integration. You will set up authentication, fetch playlists and tracks, handle favorites, and ship a working app that connects to 12 streaming services without writing 12 separate integrations.

The Cross-Platform Music App Challenge

Every streaming service has its own API, its own OAuth flow, its own data shapes, and its own rate limits. Building a music app that supports even three services means maintaining three sets of credentials, three token refresh cycles, three response parsers, and three error-handling paths.

Here is what that looks like in practice:

ConcernPer-Service ApproachSingle API (MusicAPI)
Dev time to first integration2-4 weeks per service1-2 days for all services
Ongoing maintenanceFix breakage per service API updateOne integration, maintained upstream
Service coverage1-3 services (realistically)12+ services out of the box
Auth complexitySeparate OAuth app per service, each with its own scopes and token formatsOne auth flow, one token format
Rate limit handlingDifferent thresholds, headers, and backoff rules per serviceSingle predictable rate limit
Response normalizationYou build and maintain the mapping layerNormalized by default

The math is simple. If you support six services directly, you maintain six integrations. When Spotify changes their OAuth scopes (they did in 2024), you fix your Spotify integration. When Apple Music updates their token format, you fix your Apple integration. When YouTube deprecates an endpoint, you fix your YouTube integration. Each fix ships separately. Each one can break independently.

A single API layer absorbs these changes. You write your app logic once. The API provider handles the per-service differences.

Architecture Overview: One API Layer for 12 Services

The architecture is straightforward. Your app talks to MusicAPI. MusicAPI talks to the streaming services. Your app never touches a service-specific API directly.

┌─────────────────────┐
│   Your Music App    │
│  (Web / Mobile /    │
│   Desktop)          │
└────────┬────────────┘
         │  One API, one token format,
         │  one response shape
         ▼
┌─────────────────────┐
│     MusicAPI        │
│  (Unified Layer)    │
└────────┬────────────┘
         │  Handles OAuth, token refresh,
         │  normalization, rate limits
         ▼
┌────┬────┬────┬────┬────┬────┐
│ SP │ AM │ YT │ TI │ DZ │ +7 │
└────┴────┴────┴────┴────┴────┘
 Spotify  Apple  YouTube  Tidal  Deezer  ...

Your frontend or backend makes requests to MusicAPI endpoints. The response format is identical regardless of whether the user connected Spotify, Apple Music, or Tidal. Your rendering code, your state management, your caching layer: all of it works with one data shape.

This means you can add a new streaming service to your app by enabling it in your MusicAPI dashboard. No new code. No new OAuth registration. No new parser.

Step-by-Step: From Zero to Multi-Service Music App

Setting up MusicAPI and getting API keys

Start by creating a MusicAPI account and grabbing your API credentials. You need two things: an API key for server-side requests and a client configuration for the user-facing auth flow.

  1. Sign up at musicapi.com/pricing and pick a plan.
  2. Create an application in your dashboard. This gives you your API key and app ID.
  3. Configure your redirect URI for the OAuth callback. This is where users land after connecting their streaming account.

Your API key authenticates your server. The app ID identifies your application to the user-facing auth widget. Both are required before you make your first API call.

Check the intro docs for the full setup walkthrough with screenshots.

Implementing user authentication

User authentication is the part that takes the longest when you build direct integrations. Each service has its own OAuth 2.0 implementation (or in Apple Music's case, a token-based approach that is not standard OAuth). Scopes differ. Token lifetimes differ. Refresh mechanisms differ.

MusicAPI wraps all of this into one authentication flow.

Here is the process:

  1. Initialize authentication. Your app calls MusicAPI's auth endpoint with the target service (Spotify, Apple Music, etc.) and your redirect URI. MusicAPI returns a URL to redirect the user to.
// Initialize auth for any supported service
const response = await fetch('https://api.musicapi.com/api/v1/auth/initialize', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    service: 'spotify', // or 'apple', 'youtube', 'tidal', 'deezer', etc.
    redirectUri: 'https://yourapp.com/callback'
  })
});

const { authUrl } = await response.json();
// Redirect user to authUrl
window.location.href = authUrl;
  1. Handle the callback. After the user authorizes your app on their streaming service, they are redirected back to your app with an authorization code. Send this code to MusicAPI to complete the connection.
// In your /callback route handler
const { code } = req.query;

const tokenResponse = await fetch('https://api.musicapi.com/api/v1/auth/callback', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    code,
    redirectUri: 'https://yourapp.com/callback'
  })
});

const { connectionId, service } = await tokenResponse.json();
// Store connectionId — this represents the user's linked account
  1. Use the connection. The connectionId is your handle for all future requests on behalf of this user. MusicAPI stores and refreshes the underlying service tokens automatically. You never touch a Spotify refresh token or an Apple Music developer token directly.

The same three steps work for every supported service. Change the service parameter, and the rest of the flow is identical.

Fetching playlists and tracks

With the user connected, you can fetch their playlists and tracks. The API returns the same response shape whether the data comes from Spotify, Apple Music, or any other service.

Get the user's playlists:

const playlists = await fetch(
  `https://api.musicapi.com/api/v1/playlists?connectionId=${connectionId}`,
  {
    headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
  }
);

const { data } = await playlists.json();
// data is an array of playlist objects, normalized across all services

Each playlist object contains the same fields regardless of service: id, name, description, trackCount, artwork, and owner. No conditional logic for "if Spotify, check images[0].url; if Apple Music, check attributes.artwork.url."

Get tracks from a specific playlist:

const tracks = await fetch(
  `https://api.musicapi.com/api/v1/playlists/${playlistId}/tracks?connectionId=${connectionId}`,
  {
    headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
  }
);

const { data: trackList } = await tracks.json();
// Each track: { id, title, artist, album, duration, artwork, isrc, ... }

Want to see the exact response shapes? Check the endpoint reference for playlists. You can also test these endpoints interactively: try fetching playlists from Apple Music or getting playlist tracks from Spotify.

Handling playback and favorites

Favorites (liked/saved tracks) follow the same pattern. One endpoint, one response format, every service.

Get a user's favorite tracks:

const favorites = await fetch(
  `https://api.musicapi.com/api/v1/favorites?connectionId=${connectionId}`,
  {
    headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
  }
);

const { data: favoriteTracks } = await favorites.json();

You can also write data back. Creating a playlist works across services with the same request body:

const newPlaylist = await fetch(
  'https://api.musicapi.com/api/v1/playlists',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      connectionId,
      name: 'My Cross-Platform Playlist',
      description: 'Created via MusicAPI'
    })
  }
);

This creates a playlist on whatever service the user connected. Create a Spotify playlist, an Apple Music playlist, or a Deezer playlist with the exact same code.

Code Walkthrough: A Minimal Cross-Platform Music App

Here is a complete Express.js backend that handles authentication for any streaming service and exposes playlist/track endpoints. This is a working starting point you can build on.

import express from 'express';

const app = express();
app.use(express.json());

const MUSICAPI_BASE = 'https://api.musicapi.com/api/v1';
const API_KEY = process.env.MUSICAPI_KEY;
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/callback';

const headers = {
  'Authorization': `Bearer ${API_KEY}`,
  'Content-Type': 'application/json'
};

// Store connections in memory (use a database in production)
const userConnections = new Map();

// Step 1: Start the auth flow for any service
app.get('/connect/:service', async (req, res) => {
  const { service } = req.params; // 'spotify', 'apple', 'youtube', etc.

  const response = await fetch(`${MUSICAPI_BASE}/auth/initialize`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ service, redirectUri: REDIRECT_URI })
  });

  const { authUrl } = await response.json();
  res.redirect(authUrl);
});

// Step 2: Handle the OAuth callback
app.get('/callback', async (req, res) => {
  const { code } = req.query;

  const response = await fetch(`${MUSICAPI_BASE}/auth/callback`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ code, redirectUri: REDIRECT_URI })
  });

  const { connectionId, service } = await response.json();

  // In production, associate this with the authenticated user
  const userId = req.session?.userId || 'demo-user';
  if (!userConnections.has(userId)) {
    userConnections.set(userId, {});
  }
  userConnections.get(userId)[service] = connectionId;

  res.redirect(`/dashboard?connected=${service}`);
});

// Step 3: Get playlists from any connected service
app.get('/api/playlists/:service', async (req, res) => {
  const userId = req.session?.userId || 'demo-user';
  const connectionId = userConnections.get(userId)?.[req.params.service];

  if (!connectionId) {
    return res.status(400).json({ error: 'Service not connected' });
  }

  const response = await fetch(
    `${MUSICAPI_BASE}/playlists?connectionId=${connectionId}`,
    { headers }
  );

  const data = await response.json();
  res.json(data);
});

// Step 4: Get tracks from a specific playlist
app.get('/api/playlists/:playlistId/tracks', async (req, res) => {
  const { playlistId } = req.params;
  const { connectionId } = req.query;

  const response = await fetch(
    `${MUSICAPI_BASE}/playlists/${playlistId}/tracks?connectionId=${connectionId}`,
    { headers }
  );

  const data = await response.json();
  res.json(data);
});

// Step 5: Get user's favorite tracks
app.get('/api/favorites/:service', async (req, res) => {
  const userId = req.session?.userId || 'demo-user';
  const connectionId = userConnections.get(userId)?.[req.params.service];

  if (!connectionId) {
    return res.status(400).json({ error: 'Service not connected' });
  }

  const response = await fetch(
    `${MUSICAPI_BASE}/favorites?connectionId=${connectionId}`,
    { headers }
  );

  const data = await response.json();
  res.json(data);
});

// Step 6: Get user profile from any service
app.get('/api/profile/:service', async (req, res) => {
  const userId = req.session?.userId || 'demo-user';
  const connectionId = userConnections.get(userId)?.[req.params.service];

  if (!connectionId) {
    return res.status(400).json({ error: 'Service not connected' });
  }

  const response = await fetch(
    `${MUSICAPI_BASE}/user/profile?connectionId=${connectionId}`,
    { headers }
  );

  const data = await response.json();
  res.json(data);
});

app.listen(3000, () => console.log('Music app running on port 3000'));

This is roughly 100 lines of code. It supports every streaming service MusicAPI connects to. Adding a new service means zero code changes: users visit /connect/tidal or /connect/deezer, and the same auth flow, the same playlist endpoint, and the same response format handle everything.

Compare this to building the same functionality with direct integrations. Spotify alone requires registering an OAuth app, implementing PKCE or authorization code flow, handling token refresh with their specific expiration rules, and parsing their nested response format. Multiply that by each additional service. The code above replaces all of it.

Scaling Considerations: Rate Limits, Caching, and Error Handling

A working prototype and a production app differ in three areas: rate limiting, caching, and error handling.

Rate limits

Each streaming service enforces different rate limits. Spotify uses sliding windows with Retry-After headers. Apple Music returns 429 responses with no standard backoff header. YouTube has per-project daily quotas.

When you build direct integrations, you write separate rate-limiting logic for each service. With MusicAPI, you work against one rate limit. Your retry logic is a single function:

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After') || 1;
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      continue;
    }

    return response;
  }
  throw new Error('Rate limit exceeded after retries');
}

One retry function. Works for every service behind MusicAPI. No per-service branching.

Caching

Cache aggressively. Playlist metadata changes infrequently. Track metadata almost never changes. User profiles update rarely.

A sensible caching strategy:

  • Playlists list: Cache for 5 minutes. Users do not create playlists every minute.
  • Playlist tracks: Cache for 5 minutes. Track additions are infrequent.
  • User profile: Cache for 1 hour. Profile data is nearly static.
  • Favorites: Cache for 2 minutes. Users may add favorites more frequently.

Use Redis, an in-memory Map for small apps, or your framework's built-in cache. Key by connectionId + endpoint so each user's data is cached independently.

import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 300 }); // 5 min default

async function cachedFetch(cacheKey, url, options, ttl) {
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const response = await fetchWithRetry(url, options);
  const data = await response.json();
  cache.set(cacheKey, data, ttl);
  return data;
}

Error handling

Errors fall into three categories:

  1. Auth errors (401/403). The user's connection expired or was revoked. Prompt them to reconnect. MusicAPI handles token refresh automatically, so these are rare, but users can revoke access from their streaming service settings.

  2. Not found (404). The playlist or track was deleted on the source service. Handle gracefully in your UI.

  3. Service unavailable (502/503). The upstream service is down. Show a "try again later" message. This is out of your control regardless of whether you integrate directly or through an API layer.

async function handleApiResponse(response) {
  if (response.ok) return response.json();

  if (response.status === 401) {
    throw new Error('CONNECTION_EXPIRED');
  }
  if (response.status === 404) {
    throw new Error('RESOURCE_NOT_FOUND');
  }
  if (response.status >= 500) {
    throw new Error('SERVICE_UNAVAILABLE');
  }

  const error = await response.json();
  throw new Error(error.message || 'Unknown error');
}

FAQ

How long does it take to build a cross-platform music app with MusicAPI?

A basic integration (auth, playlists, tracks, favorites) takes one to two days. The code walkthrough in this post is a functional starting point. Most of the time goes into your app's UI and business logic, not the streaming service integration. Compare this to two to four weeks per service when building direct integrations.

Which streaming services does MusicAPI support?

MusicAPI connects to 12+ streaming services, including Spotify, Apple Music, YouTube Music, Tidal, Deezer, Amazon Music, and others. New services are added regularly. When a new service launches, your app supports it without code changes.

Do I need to register separate OAuth apps with each streaming service?

No. MusicAPI handles the OAuth complexity for each service. You register one application with MusicAPI, configure your redirect URI, and the unified auth flow works for every service. You never manage Spotify client secrets, Apple Music developer tokens, or YouTube API keys directly.

Can I access the raw tokens from each streaming service?

Yes. If you need the original access tokens from a connected service (for features MusicAPI does not cover), you can request the original auth tokens through the API. This gives you a fallback path for edge cases without abandoning the unified integration.

How does MusicAPI handle differences in data between services?

MusicAPI normalizes responses into a consistent JSON format. A playlist from Spotify and a playlist from Apple Music return identical field names, data types, and structure. Duration is always in milliseconds. Artwork always includes URL, width, and height. Artist references always include both ID and name. Check the supported features page for a field-by-field breakdown.

What happens when a streaming service updates or deprecates their API?

MusicAPI maintains the integrations upstream. When Spotify changes their API (as they did with their OAuth scopes update), MusicAPI's team updates the integration. Your app continues to work without changes. This is one of the main advantages over direct integrations: you shift the maintenance burden to a dedicated team.

Can I build a playlist transfer feature with this approach?

Yes. Fetch tracks from a playlist on one service, then create a new playlist and add those tracks on another service. MusicAPI's normalized track data (including ISRC codes) makes matching songs across platforms straightforward. See the playlist creation endpoints and playlist generator tutorial for implementation details.


Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.