Soundify is a lightweight and flexible library for interacting with the Spotify API, designed to work seamlessly with TypeScript and support all available runtimes.
Getting Started | Error handling | Token refreshing | Pagination
Installation
The package doesn't depend on runtime specific apis, so you should be able to
use it without any problems everywhere.
pnpm add @soundify/web-api
bun install @soundify/web-api
// deno.json
{
"imports": {
"@soundify/web-api": "https://deno.land/x/soundify/mod.ts"
}
}
Getting Started
Soundify has a very simple structure. It consists of a SpotifyClient
capable
of making requests to the Spotify API, along with a set of functions (like
getCurrentUser
) that utilize the client to make requests to specific
endpoints.
import { getCurrentUser, search, SpotifyClient } from "@soundify/web-api";
const client = new SpotifyClient("YOUR_ACCESS_TOKEN");
const me = await getCurrentUser(client);
console.log(me);
const result = await search(client, "track", "Never Gonna Give You Up");
console.log(result.tracks.items.at(0));
Compared to the usual OOP way of creating API clients, this approach has several
advantages. The main one is that it is tree-shakable. You only ship code you
use. This may be not that important for server-side apps, but I'm sure frontend
users will thank you for not including an extra 10kb of crappy js into your
bundle.
import {
getAlbumTracks,
getArtist,
getArtistAlbums,
getRecommendations,
SpotifyClient,
} from "@soundify/web-api";
const client = new SpotifyClient("YOUR_ACCESS_TOKEN");
const radiohead = await getArtist(client, "4Z8W4fKeB5YxbusRsdQVPb");
console.log(`Radiohead popularity - ${radiohead.popularity}`);
const pagingResult = await getArtistAlbums(client, radiohead.id, { limit: 1 });
const album = pagingResult.items.at(0)!;
console.log(`Album - ${album.name}`);
const tracks = await getAlbumTracks(client, album.id, { limit: 5 });
console.table(
tracks.items.map((track) => ({
name: track.name,
duration: track.duration_ms,
})),
);
const recomendations = await getRecommendations(client, {
seed_artists: [radiohead.id],
seed_tracks: tracks.items.map((track) => track.id).slice(0, 4),
market: "US",
limit: 5,
});
console.table(
recomendations.tracks.map((track) => ({
artist: track.artists.at(0)!.name,
name: track.name,
})),
);
Error handling 📛
import { getCurrentUser, SpotifyClient, SpotifyError } from "@soundify/web-api";
const client = new SpotifyClient("INVALID_ACCESS_TOKEN");
try {
const me = await getCurrentUser(client);
console.log(me);
} catch (error) {
if (error instanceof SpotifyError) {
error.status;
const message = typeof error.body === "string"
? error.body
: error.body?.error.message;
console.error(message);
error.response.headers.get("Date");
console.error(error);
return;
}
console.error("We're totally f#%ked!");
}
Rate Limiting 🕒
If you're really annoying customer, Spotify may block you for some time. To know
what time you need to wait, you can use Retry-After
header, which will tell
you time in seconds.
More about rate limiting↗
To handle this automatically, you can use waitForRateLimit
option in
SpotifyClient
. (it's disabled by default, because it may block your code for
unknown time)
const client = new SpotifyClient("YOUR_ACCESS_TOKEN", {
waitForRateLimit: true,
waitForRateLimit: (retryAfter) => retryAfter < 60,
});
Authorization
Soundify doesn't provide any tools for authorization, because that would require
to write whole oauth library in here. We have many other battle-tested oauth
solutions, like oauth4webapi or
oidc-client-ts. I just don't see a
point in reinventing the wheel 🫤.
Despite this, we have a huge directory of examples, including those for
authorization.
OAuth2 Examples↗
Token Refreshing
import { getCurrentUser, SpotifyClient } from "@soundify/web-api";
const refresher = () => {
return Promise.resolve("YOUR_NEW_ACCESS_TOKEN");
};
const accessToken = await refresher();
const client = new SpotifyClient(accessToken, { refresher });
const me = await getCurrentUser(client);
console.log(me);
const me = await getCurrentUser(client);
console.log(me);
To simplify the process of paginating through the results, we provide a
PageIterator
and CursorPageIterator
classes.
import { getPlaylistTracks, SpotifyClient } from "@soundify/web-api";
import { PageIterator } from "@soundify/web-api/pagination";
const client = new SpotifyClient("YOUR_ACCESS_TOKEN");
const playlistIter = new PageIterator(
(offset) => getPlaylistTracks(client, "37i9dQZEVXbMDoHDwVN2tF", {
limit: 50,
offset,
}),
);
for await (const track of playlistIter) {
console.log(track);
}
const allTracks = await playlistIter.collect();
console.log(allTracks.length);
const lastHundredTracks = new PageIterator(
(offset) => getPlaylistTracks(
client,
"37i9dQZEVXbMDoHDwVN2tF",
{ limit: 50, offset }
),
{ initialOffset: -100 },
).collect();
import { getFollowedArtists, SpotifyClient } from "@soundify/web-api";
import { CursorPageIterator } from "@soundify/web-api/pagination";
const client = new SpotifyClient("YOUR_ACCESS_TOKEN");
for await (const artist of new CursorPageIterator(
opts => getFollowedArtists(client, { limit: 50, after: opts.after })
)) {
console.log(artist.name);
}
const artists = await new CursorPageIterator(
opts => getFollowedArtists(client, { limit: 50, after: opts.after })
).collect();
const artists = await new CursorPageIterator(
opts => getFollowedArtists(client, { limit: 50, after: opts.after }),
{ initialAfter: "4Z8W4fKeB5YxbusRsdQVPb" }
).collect();
Other customizations
import { SpotifyClient } from "@soundify/web-api";
const client = new SpotifyClient("YOUR_ACCESS_TOKEN", {
fetch: (input, init) => {
return fetch(input, init);
},
beseUrl: "https://example.com/",
middlewares: [(next) => (url, opts) => {
return next(url, opts);
}],
});
Contributors ✨
All contributions are very welcome ❤️
(emoji key)