Core
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
- Yarn
- pnpm
npm install @spotifly/core @types/spotify-api
yarn add @spotifly/core @types/spotify-api
pnpm add @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:
- 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 || '',
});
- 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.
Usually, a refresh token only needs to be generated once. You can generate one here using the Authorization Code Flow.
Supported Endpoints and Methods
- ✅ - Fully supported
- 〽️ - Partial support - work in progress
- ❌ - Currently not supported
Endpoint | Support |
---|---|
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.
- Install
jest-mock-extended
.
- npm
- Yarn
- pnpm
npm install -D jest-mock-extended
yarn add --dev jest-mock-extended
pnpm add -D jest-mock-extended
- Mock whatever methods you are using using the
mockDeep
helper and inject this object wheneverinitialize
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);
});