Skip to main content

Core

npm (scoped) Codecov

Motivation

The Spotifly core library is a lightweight wrapper for the Spotify Web API. Why would you use it?

  • Strong TypeScript and IntelliSense support
    • All function parameters and return values are strongly typed
  • A neatly organized, intuitive API
    • Sticks closely to the semantics of the Web API
  • Convenience methods for limited and paginated endpoints
    • Automatically retreive all items from paginated and limited endpoints
  • Automatic authentication
    • Provide your credentials once and have every request authenticate automatically with Spotify
  • 100% future-proof
    • Today's wrapper API supports the Spotify endpoints of tomorrow
  • Testing with confidence
    • Creating stubs for the core lib is a piece of cake. Read along for testing recipes!

Although the core library is very lightweight, it may be overkill if you're only interested in one or two Web API endpoints. However, for many other use cases, including long-running apps, it's a great fit.

Usage

Installation

Node.js ≥ 18 is required. Installing the types for the Web API is highly recommended!

npm install @spotifly/core @types/spotify-api

Example

import Spotifly from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
accessToken: 'abc123',
});

// Deriving the genres of a track via its artists
await spotifyClient.Tracks.getTrack('5nHc8CmiPllMzHbJhhx3KS')
.then(res => {
return res.data.artists.map(artist => artist.id);
})
.then(artistIds => {
return spotifyClient.Artists.getSeveralArtists(artistIds);
})
.then(res => {
const genres = res.data.artists.map(artist => artist.genres).flat();
console.log(genres); // [ 'livetronica', 'munich electronic' ]
});

// Getting all of a user's saved tracks
await spotifyClient.Tracks.getAllUsersSavedTracks()()
.then(res => {
return res.map(res => res.data.items).flat();
})
.then(library => {
console.log(library.length); // 70
});

// Searching for an item
await spotifyClient.Search.search({ query: 'eminem', type: 'album' }).then(
res => {
console.log(res.data.albums?.items[0]?.name); // 'The Eminem Show'
},
);

Authentication

Every Spotify client is created through a call to initialize. A client is bound to the authentication method it was instantiated with. There are multiple ways to instantiate a client:

  1. With a Spotify client id, client secret and refresh token - This method will automatically generate an initial access token and refresh it when it's about to expire after 1 hour. This is great for apps that need to run independently.
import Spotifly from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
clientId: process.env.SPOTIFY_CLIENT_ID || '',
clientSecret: process.env.SPOTIFY_CLIENT_SECRET || '',
refreshToken: process.env.SPOTIFY_REFRESH_TOKEN || '',
});
  1. With a Spotify access token - This method will simply attach the provided access token to requests to Spotify. No secrets are involved but requests may fail once the token has expired.
import Spotifly from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
accessToken: 'abc123',
});

Both methods return a Spotify client with the same methods.

tip

Usually, a refresh token only needs to be generated once. You can generate one here using the Authorization Code Flow.

Supported Endpoints and Methods

Web API Reference.

  • ✅ - Fully supported
  • 〽️ - Partial support - work in progress
  • ❌ - Currently not supported
EndpointSupport
Albums
Audiobooks
Artists
Categories
Chapters
Episodes
Genres
Markets
Player
Playlists〽️
Search
Shows
Tracks
Users
  • Playlists, missing: Replace Playlist Items. I don't understand how this endpoint needs to be called in order to "replace" playlist items. Reordering items, however, is supported.
  • Audiobooks and Chapters: Not yet available in many regions.
Detailed Support Table
  • Albums
    • Albums.checkAllUsersSavedAlbums
    • Albums.checkUsersSavedAlbums
    • Albums.getAlbum
    • Albums.getAlbumTracks
    • Albums.getAllAlbumTracks
    • Albums.getAllAlbums
    • Albums.getAllUsersSavedAlbums
    • Albums.getNewAlbumReleases
    • Albums.getSeveralAlbums
    • Albums.getUsersSavedAlbums
    • Albums.removeAllUsersSavedAlbums
    • Albums.removeUsersSavedAlbums
    • Albums.saveAlbumsForUser
    • Albums.saveAllAlbumsForUser
  • Artists
    • Artists.getAllArtists
    • Artists.getAllArtistsAlbums
    • Artists.getArtist
    • Artists.getArtistsAlbums
    • Artists.getArtistsRelatedArtists
    • Artists.getArtistsTopTracks
    • Artists.getSeveralArtists
  • Categories
    • Categories.getCategory
    • Categories.getSeveralCategories
  • Episodes
    • Episodes.beta.checkAllUsersSavedEpisodes
    • Episodes.beta.checkUsersSavedEpisodes
    • Episodes.beta.getAllUsersSavedEpisodes
    • Episodes.beta.getUsersSavedEpisodes
    • Episodes.beta.removeAllUsersSavedEpisodes
    • Episodes.beta.removeUsersSavedEpisodes
    • Episodes.beta.saveAllEpisodesForUser
    • Episodes.beta.saveEpisodesForUser
    • Episodes.getAllEpisodes
    • Episodes.getEpisode
    • Episodes.getSeveralEpisodes
  • Genres
    • Genres.getAvailableGenreSeeds
  • Markets
    • Markets.getAvailableMarkets
  • Player
    • Player.addToQueue
    • Player.getAvailableDevices
    • Player.getCurrentlyPlayingTrack
    • Player.getPlaybackState
    • Player.getRecentlyPlayedTracks
    • Player.getUsersQueue
    • Player.pausePlayback
    • Player.seekToPosition
    • Player.setPlaybackVolume
    • Player.setRepeatMode
    • Player.skipToNext
    • Player.skipToPrevious
    • Player.startOrResumePlayback
    • Player.togglePlaybackShuffle
    • Player.transferPlayback
  • Playlists
    • Playlists.addPlaylistItems
    • Playlists.changePlaylist
    • Playlists.createPlaylist
    • Playlists.getAllCurrentUsersPlaylists
    • Playlists.getAllPlaylistItems
    • Playlists.getAllUsersPlaylists
    • Playlists.getCategoryPlaylists
    • Playlists.getCurrentUsersPlaylists
    • Playlists.getFeaturedPlaylists
    • Playlists.getPlaylist
    • Playlists.getPlaylistCoverImage
    • Playlists.getPlaylistItems
    • Playlists.getUsersPlaylists
    • Playlists.removePlaylistItems
    • Playlists.reorderPlaylistItems
    • Playlists.uploadCustomPlaylistCoverImage
  • Search
    • Search.search
  • Shows
    • Shows.checkAllUsersSavedShows
    • Shows.checkUsersSavedShows
    • Shows.getAllShowEpisodes
    • Shows.getAllShows
    • Shows.getAllUsersSavedShows
    • Shows.getSeveralShows
    • Shows.getShow
    • Shows.getShowEpisodes
    • Shows.getUsersSavedShows
    • Shows.removeAllUsersSavedShows
    • Shows.removeUsersSavedShows
    • Shows.saveAllShowsForUser
    • Shows.saveShowsForUser
  • Tracks
    • Tracks.checkAllUsersSavedTracks
    • Tracks.checkUsersSavedTracks
    • Tracks.getAllAudioFeatures
    • Tracks.getAllTracks
    • Tracks.getAllUsersSavedTracks
    • Tracks.getAudioAnalysis
    • Tracks.getAudioFeatures
    • Tracks.getRecommendations
    • Tracks.getSeveralAudioFeatures
    • Tracks.getSeveralTracks
    • Tracks.getTrack
    • Tracks.getUsersSavedTracks
    • Tracks.removeAllUsersTracksForUser
    • Tracks.removeUsersSavedTracks
    • Tracks.saveAllTracksForUser
    • Tracks.saveTracksForUser
  • Users
    • Users.checkFollowsAllArtists
    • Users.checkFollowsAllUsers
    • Users.checkFollowsArtists
    • Users.checkFollowsUsers
    • Users.checkUsersFollowPlaylist
    • Users.followAllArtists
    • Users.followAllUsers
    • Users.followArtists
    • Users.followPlaylist
    • Users.followUsers
    • Users.getAllUsersTopArtists
    • Users.getAllUsersTopTracks
    • Users.getCurrentUsersProfile
    • Users.getUsersFollowedArtists
    • Users.getUsersProfile
    • Users.getUsersTopArtists
    • Users.getUsersTopTracks
    • Users.unfollowAllArtists
    • Users.unfollowAllUsers
    • Users.unfollowArtists
    • Users.unfollowPlaylist
    • Users.unfollowUsers
  • future
    • future.request

Response Schema

The core library uses axios for all HTTP requests to Spotify. Each method call returns a DataResponse object with request metadata. Convenience methods will return a DataResponse array. data will hold the return value of interest.

type DataResponse<Data = unknown> = {
data: Data;
statusCode: number;
} & Pick<AxiosResponse, 'headers'>;

Both DataResponse and DataPromise are exported so you don't have to infer them. The latter simply wraps the former in a Promise.

import type { DataResponse, DataPromise } from '@spotifly/core';

Future

Today's client supports the Spotify endpoints of tomorrow. The API features a future.request function that acts as an escape hatch for everything that is not yet or incorretly supported by this library. Requests made through future.request always include the Authorization header.

import Spotifly from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
accessToken: 'abc123',
});

await spotifyClient.future
.request<MyResponseType>({ url: 'me/new-releases' })
.then(console.log); // typeof MyResponseType

Error Handling

The regular error object as defined by the Web API has the following shape:

type SpotifyRegularErrorObject = {
status: number;
message: string;
};

You can use the isError helper to safely access the error object in case something goes wrong.

import Spotifly from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
accessToken: 'abc123',
});

try {
await spotifyClient.Tracks.getTrack('thistrackdoesnotexist');
} catch (e) {
if (Spotifly.isError(e)) {
console.log(e.response?.data.error.status); // 404
console.log(e.response?.data.error.message); // 'Not Found'
}
}

See Web API / Response Schema.

Convenience Methods

Methods that contain all in their name are convenience methods for (offset-based) paginated and limited endpoints. Limited endpoints expect a maximum amount of items when queried.

Examples are Tracks.getAllUsersSavedTracks (paginated) and Tracks.getAllTracks (limited). The Spotify Web API returns at most 50 items when asked for a user's saved tracks and allows no more than 50 track ids when querying for track catalog information.

You can equivalently call the corresponding non-convenience methods Tracks.getUsersSavedTracks and Tracks.getSeveralTracks with the same arguments. However, you would have to manually handle pagination and chunking of ids. Convenience methods take care of this for you.

Usage

All convenience methods are aware of their specific limitations. They know how many items they can request from Spotify at once and will max out each request, taking care of chunking item ids and handling paginated endpoints.

Non-convenience/ordinary methods do not have such logic. They do not check or chunk requests and it's up to you to make sure you're calling the endpoint according to its usage.

Callbacks

The following example returns all of a user's saved tracks in a flat array. Convenience methods have a different signature: They are curried and return a function which optionally accepts a callback. Each time a new chunk of items is fetched, the response wrapped in a DataResponse<T> is passed to the callback as the only argument. See the helper types section below for more.

If the callback returns a promise, it is awaited. The eventually returned value is ignored.

import Spotifly, { DataResponse } from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
accessToken: 'abc123',
});

const allTracks = await spotifyClient.Tracks.getAllUsersSavedTracks()(
async response => {
// DataResponse<SpotifyApi.UsersSavedTracksResponse>
console.log(`fetched ${response.data.items.length} items!`);
},
).then(allResponses => {
return allResponses.map(({ data }) => data.items).flat();
});

You can do pretty much anything with the chunk - e.g., log something to the console or send the items somewhere else.

Convenience methods simply wrap their ordinary counterpart and resolve all responses in a single array. This is reflected in their return types: If the ordinary methods returns DataResponse<T>, the convenience method returns Array<DataResponse<T>>:

import Spotifly, { DataResponse } from '@spotifly/core';

const spotifyClient = Spotifly.initialize({
accessToken: 'abc123',
});

type UserTracks = DataResponse<SpotifyApi.UsersSavedTracksResponse>;

const allTracks: UserTracks[] =
await spotifyClient.Tracks.getAllUsersSavedTracks()();

const someTracks: UserTracks = await spotifyClient.Tracks.getUsersSavedTracks();

If you want to define your callback function separately, there's a helper type which can infer the correct signature (DataCallback).

Helper Types

The SpotifyClient type mirrors the structure of an initialized client. DataCallback can be used to infer the signature of the callback to a curried function (convenience method). Both types can be useful in some scenarios:

import { DataCallback, initialize, SpotifyClient } from '@spotifly/core';

type Callback = DataCallback<SpotifyClient['Tracks']['getAllUsersSavedTracks']>;

const callback: Callback = response => {
console.log(response.data.items[0].added_at);
};

const spotifyClient = initialize({
accessToken: 'abc123',
});

// Signature is correct!
await spotifyClient.Tracks.getAllUsersSavedTracks()(callback);

Testing Recipes

The core library can be mocked nicely with Jest and the TypeScript-friendly jest-mock-extended extension. The following examples assume you have already installed @types/spotify-api and setup Jest accordingly for your project.

  1. Install jest-mock-extended.
npm install -D jest-mock-extended
  1. Mock whatever methods you are using using the mockDeep helper and inject this object whenever initialize is called in the implementation. Using the types from @types/spotify-api, it's very easy to create solid mock responses.
import Spotifly, { DataResponse, SpotifyClient } from '@spotifly/core';
import { mockDeep } from 'jest-mock-extended';

const audioFeatures: SpotifyApi.AudioFeaturesObject = {
acousticness: 0.00242,
analysis_url: 'https://api.spotify.com/v1/audio-analysis/2takc7B',
danceability: 0.585,
duration_ms: 237040,
energy: 0.842,
id: '2takc7B',
instrumentalness: 0.00686,
key: 9,
liveness: 0.0866,
loudness: -5.883,
mode: 0,
speechiness: 0.0556,
tempo: 118.211,
time_signature: 4,
track_href: 'https://api.spotify.com/v1/tracks/2takc7B',
type: 'audio_features',
uri: 'spotify:track:2takc7B',
valence: 0.428,
};

type MockResponse = DataResponse<SpotifyApi.MultipleAudioFeaturesResponse>;

const mockResponse = (length: number): MockResponse => {
return {
data: {
audio_features: Array.from({ length }, () => audioFeatures),
},
statusCode: 200,
headers: {},
};
};

const mockSpotify = mockDeep<SpotifyClient>();

// Return a mocked client whenever the client is initialized
jest.spyOn(Spotifly, 'initialize').mockReturnValue(mockSpotify);

// Mocking an ordinary method
mockSpotify.Tracks.getSeveralAudioFeatures.mockImplementation(ids => {
return Promise.resolve(mockResponse(ids.length));
});

// Mocking a convenience method
mockSpotify.Tracks.getAllAudioFeatures.mockImplementation(ids => {
return cb => {
if (cb) cb(mockResponse(ids.length));
return Promise.resolve([mockResponse(ids.length)]);
};
});

test('my function', async () => {
// Your code
function getData() {
const client = Spotifly.initialize({ accessToken: 'abc123' });
return client.Tracks.getSeveralAudioFeatures(['2takc7B', '6hsak']);
}
// Assert anything!
const res = await getData();
expect(res.data.audio_features).toHaveLength(2);
});