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:
import { SpotifyCard } from "spotify-now-playing-card"
import "spotify-now-playing-card/dist/styles.css"
export default function Footer() {
return <SpotifyCard apiUrl="/api/spotify" />
}
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
You can see the full implementation and more details on my personal blog post. 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 to `dist/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. You can see the original implementation in footer of my website.
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:
- 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. I write more about similar topics on my personal blog where I share my experiences building web applications.
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 :)
If you found this post useful, you can check original post, check out more articles on my personal blog where I write about Nodejs, React, TypeScript, and web development in general!
You can also check out my GitHub for more projects and open source contributions.

Top comments (0)