Introduction
In my personal website I had a small Spotify “Now Playing” widget in the footer, I find it usefull to have it there so I can show which music I am currently listening on Spotify and who knows, maybe some uknown visitor likes the song so at least I can do in this case is recommend some good music :) It was working fine, but had two issues:
- It was tightly coupled to the project (Next.js API + Tailwind classes)
- I couldn't easily share or reuse it as I wanted to use it in my other projects, it was not really practical to copy and paste the code into each project especially when I wanted to make some changes.
So I came to the conclusion that I need to create a npm package that I can use in my other projects and that I can easily share or reuse. Anyway maybe someone else would find it usefull so that was one more plus for this idea. In the worst case scenario I can use it without any effort in other projects and that's the main reason why I decided to create a npm package and make my life easierr.
In this post I will go through the entire process:
- how I extracted the widget into a pure React + TypeScript component so its reusable
- how I created the Spotify API layer that I can use in multiple projects
- how I packaged everything into an npm package with better DX (CSS, TypeScript types, setup CLI)
The goal is that by the end you can install the package with less code and less effort and do:
API layer – without it there is no music 🎧
The first part is the standard Spotify API flow with a refresh token. First and foremost we need to get the access token from Spotify, sou you should have a Spotify account and you should be able to get your credentials from Spotify developer portal. After that we have credentials we can start with the API layer.
For Next.js App Router (app/api/spotify/route.ts) it looks like this:
import { NextResponse } from "next/server" const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET const SPOTIFY_REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN let accessToken: string | null = null let tokenExpiry = 0 async function getAccessToken(): Promise<string> { if (accessToken && Date.now() < tokenExpiry) return accessToken if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET || !SPOTIFY_REFRESH_TOKEN) { throw new Error("Spotify credentials not configured") } const response = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: "Basic " + Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64"), }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: SPOTIFY_REFRESH_TOKEN, }), }) if (!response.ok) { throw new Error(`Failed to get access token: ${await response.text()}`) } const data = await response.json() accessToken = data.access_token tokenExpiry = Date.now() + 55 * 60 * 1000 // ~55 min return accessToken! }
After that we need to return the format that the package expects:
export async function GET() { try { const token = await getAccessToken() const response = await fetch("https://api.spotify.com/v1/me/player/currently-playing", { headers: { Authorization: `Bearer ${token}` }, }) if (response.status === 204 || !response.ok) { return NextResponse.json({ isPlaying: false }) } const data = await response.json() if (!data.item) return NextResponse.json({ isPlaying: false }) return NextResponse.json({ isPlaying: data.is_playing, title: data.item.name, artist: data.item.artists.map((a: any) => a.name).join(", "), album: data.item.album.name, albumImageUrl: data.item.album.images[0]?.url, songUrl: data.item.external_urls.spotify, }) } catch (error) { console.error("Spotify API error:", error) return NextResponse.json({ isPlaying: false }) } }
This API endpoint is the only dependency that the package has from your project.
React + TypeScript component
In the package the component looks like this:
export interface SpotifyCardProps { apiUrl?: string fallbackUrl?: string className?: string styles?: { container?: string link?: string image?: string title?: string artist?: string album?: string icon?: string } showAlbum?: boolean notPlayingText?: string refreshInterval?: number }
const SpotifyCard: React.FC<SpotifyCardProps> = ({ apiUrl = "/api/spotify", fallbackUrl = "https://open.spotify.com", className = "", styles = {}, showAlbum = true, notPlayingText = "Currently not listening", refreshInterval = 30000, ...props }) => { const { data, error } = useSWR<SpotifyData>(apiUrl, fetcher, { refreshInterval, revalidateOnFocus: true, }) const defaultStyles = { container: "spotify-card-container", link: "spotify-card-link", image: "spotify-card-image", title: "spotify-card-title", artist: "spotify-card-artist", album: "spotify-card-album", icon: "spotify-card-icon", } const merged = { /* merging default + custom styles */ } const isPlaying = data?.isPlaying const titleClass = isPlaying ? "spotify-card-title-gradient" : "" return ( <div className={merged.container} {...props}>{/* ... image, title, album, icon, gradient title ... */}</div> ) }
Important decisions:
- SWR for caching + polling
- TypeScript types (
SpotifyCardProps,SpotifyData) are exported todist/index.d.ts - props for styles so people can customize it with Tailwind or plain CSS
CSS that works everywhere (without Tailwind)
The original widget was pure Tailwind and i find it not really practical as my other project were not using Tailwind so there were no benefits in using it. For the npm package this is not desirable so I decided to use plain CSS as it can be used more easily in variety of projects.
So the solution was a small CSS file in the package:
.spotify-card-container { color: #94a3b8; border: 1px solid #262626; border-radius: 0.5rem; width: 100%; margin: 0.5rem 1.25rem; }
In the Rollup configuration I just copy src/styles.css to dist/styles.css and the user can import it like this:
import "spotify-now-playing-card/dist/styles.css"
If you want full Tailwind control – simply pass your classes through the styles prop.
Packaging with Rollup + TypeScript setup
Short, rollup.config:
export default { input: "src/index.ts", output: [ { file: packageJson.main, format: "cjs", sourcemap: true, exports: "named" }, { file: packageJson.module, format: "esm", sourcemap: true }, ], plugins: [ resolve({ browser: true }), commonjs(), typescript({ tsconfig: "./tsconfig.json", declaration: true, declarationDir: "./dist", }), copy({ targets: [{ src: "src/styles.css", dest: "dist" }] }), ], external: ["react", "react-dom", "react/jsx-runtime", "axios", "swr", "react-icons/si"], }
This gives:
Build output
- dist/index.js (CJS)
- dist/index.esm.js (ESM)
- dist/index.d.ts + SpotifyCard.d.ts (tipovi)
- dist/styles.css
Small CLI for easier setup
For better DX I added a bin script:
"bin": { "spotify-card-setup": "bin/setup.js" }
The user can then run:
npx spotify-now-playing-card-setup
The script:
- asks for
clientId,clientSecret,refreshToken - creates the corresponding API endpoint (App router or Pages router)
- writes the values to
.env.local
Less copy/paste, less errors.
Usage in the application
Minimal code example in Next.js app:
import { SpotifyCard } from "spotify-now-playing-card" import "spotify-now-playing-card/dist/styles.css" export default function Footer() { return ( <footer> <SpotifyCard apiUrl="/api/spotify" /> </footer> ) }
With custom styles:
<SpotifyCard apiUrl="/api/spotify" styles={{ container: "bg-gray-900/80 rounded-2xl border border-gray-700", title: "text-white text-lg font-semibold", artist: "text-gray-300", }} showAlbum notPlayingText="Currently off Spotify" />
Conclusion
If you have some custom widget in your project that you want to share with others, probably someone else would find it useful. And if you don't want to share it, at least you can use it in your other projects without any effort I highly recommend you to do it. With a little extra effort around the API, DX and packaging, you can turn it into a real npm package that lives outside of one project. In my case it was the Spotify card – in your case it can be anything: GitHub card, Dev.to feed, custom loader… Please let me know if you have any questions or suggestions on how to improve the package or the code, as well as how to make it more usefull to the masses. Hope you enjoyed and plese check it out on npm :)