Skip to main content

🎧 Spotify Web API client for js/ts runtime environments

Works with
It is unknown whether this package works with Bun
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Browsers
JSR Score
88%
Published
2 months ago (1.1.0)

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.

npm install @soundify/web-api
// deno.json
{
	"imports": {
		"@soundify/web-api": "https://deno.land/x/soundify/mod.ts"
	}
}

Install from JSR registry

deno add @soundify/web-api

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; // 401

		const message = typeof error.body === "string"
			? error.body
			: error.body?.error.message;
		console.error(message); // "Invalid access token"

		error.response.headers.get("Date"); // You can access the response here

		console.error(error);
		// SpotifyError: 401 Unauthorized (https://api.spotify.com/v1/me) : Invalid access token
		return;
	}

	// If it's not a SpotifyError, then it's some type of network error that fetch throws
	// Or can be DOMException if you abort the request
	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,
	// wait only if it's less than a minute
	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";

// if you don't have access token yet, you can pass null to first argument
const client = new SpotifyClient(null, {
	// but you have to provide a function that will return a new access token
	refresher: () => {
		return Promise.resolve("YOUR_NEW_ACCESS_TOKEN");
	},
});

const me = await getCurrentUser(client);
// client will call your refresher to get the token
// and only then make the request
console.log(me);

// let's wait some time to expire the token ...

const me = await getCurrentUser(client);
// client will receive 401 and call your refresher to get new token
// you don't have to worry about it as long as your refresher is working
console.log(me);

Pagination

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", {
			// you can find the max limit for specific endpoint
			// in spotify docs or in the jsdoc comments of this property
			limit: 50,
			offset,
		}),
);

// iterate over all tracks in the playlist
for await (const track of playlistIter) {
	console.log(track);
}

// or collect all tracks into an array
const allTracks = await playlistIter.collect();
console.log(allTracks.length);

// Want to get the last 100 items? No problem
const lastHundredTracks = new PageIterator(
	(offset) =>
		getPlaylistTracks(
			client,
			"37i9dQZEVXbMDoHDwVN2tF",
			{ limit: 50, offset },
		),
	{ initialOffset: -100 }, // this will work just as `Array.slice(-100)`
).collect();
import { getFollowedArtists, SpotifyClient } from "@soundify/web-api";
import { CursorPageIterator } from "@soundify/web-api/pagination";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN");

// loop over all followed artists
for await (
	const artist of new CursorPageIterator(
		(opts) => getFollowedArtists(client, { limit: 50, after: opts.after }),
	)
) {
	console.log(artist.name);
}

// or collect all followed artists into an array
const artists = await new CursorPageIterator(
	(opts) => getFollowedArtists(client, { limit: 50, after: opts.after }),
).collect();

// get all followed artists starting from Radiohead
const artists = await new CursorPageIterator(
	(opts) => getFollowedArtists(client, { limit: 50, after: opts.after }),
	{ initialAfter: "4Z8W4fKeB5YxbusRsdQVPb" }, // let's start from Radiohead
).collect();

Other customizations

import { SpotifyClient } from "@soundify/web-api";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN", {
	// You can use any fetch implementation you want
	// For example, you can use `node-fetch` in node.js
	fetch: (input, init) => {
		return fetch(input, init);
	},
	// You can change the base url of the client
	// by default it's "https://api.spotify.com/"
	beseUrl: "https://example.com/",
	middlewares: [(next) => (url, opts) => {
		// You can add your own middleware
		// For example, you can add some headers to every request
		return next(url, opts);
	}],
});

Contributors ✨

All contributions are very welcome ❀️ (emoji key)

Artem Melnyk
Artem Melnyk

🚧
danluki
danluki

πŸ’»
Andrii Zontov
Andrii Zontov

πŸ›

Examples

Example 1

import { SpotifyClient } from "@soundify/web-api";

// with access token
const client = new SpotifyClient("YOUR_ACCESS_TOKEN");

// with automatic token refreshing
const client = new SpotifyClient(null, {
  // your custom refresher here
  refresher: () => Promise.resolve("NEW_ACCESS_TOKEN"),
});

const res = await client.fetch("/v1/me");
const user = await res.json();

Endpoints

Functions that utilize the SpotifyClient to make requests to the Spotify Web API.

Example 2

import { SpotifyClient, getCurrentUser } from "@soundify/web-api";

const client = new SpotifyClient("YOUR_ACCESS_TOKEN");
const user = await getCurrentUser(client);

// How endpoint functions are built
const getCurrentUser = async (client: SpotifyClient) => {
  const res = await client.fetch("/v1/me");
  return await res.json();
}
Built and signed on
GitHub Actions
View transparency log

Add Package

deno add @soundify/web-api

Import symbol

import * as mod from "@soundify/web-api";

Add Package

npx jsr add @soundify/web-api

Import symbol

import * as mod from "@soundify/web-api";

Add Package

yarn dlx jsr add @soundify/web-api

Import symbol

import * as mod from "@soundify/web-api";

Add Package

pnpm dlx jsr add @soundify/web-api

Import symbol

import * as mod from "@soundify/web-api";

Add Package

bunx jsr add @soundify/web-api

Import symbol

import * as mod from "@soundify/web-api";