Friday, August 8, 2025
How to Use Spotify API with Next.js Server Actions
8 min read
Spotify's Web API allows developers to display real-time information about what a user is currently listening to.
In this tutorial, we'll build a Spotify Currently Playing in Next.js 15 using Server Actions in `actions.ts`
to securely handle Spotify credentials and API calls.
Setting Up Your Next.js Project
Before we start, ensure you already have Node.js and npm installed. After that, follow these steps to set up your Next.js project:
-
Create a new Next.js app:
bashnpx create-next-app@latest next-spotify cd next-spotify
-
Create a
`.env.local`
file in the root of your project to store sensitive information like your Spotify credentials.SPOTIFY_CLIENT_ID=your_spotify_client_id SPOTIFY_CLIENT_SECRET=your_spotify_client_secret SPOTIFY_REFRESH_TOKEN=your_refresh_token
-
Create a folder structure:
`src/app/actions.ts`
for server actions`src/app/page.tsx`
for the main page`src/app/components/Spotify.tsx`
for the client component
Creating a Spotify App
Before you can fetch currently playing data, you need to create a Spotify Developer App:
- Visit Spotify Developer Dashboard.
- Click Create an App and give it a name like Next.js Spotify and add a Redirect URI — for local development, you can use
`http://127.0.0.1:3000/api/callback`
.
- Save your changes.
- In the app settings, find your Client ID and Client Secret. You will need these to authenticate your requests.
- Save the
`Client ID`
and`Client Secret`
somewhere safe.
Getting a Refresh Token
To access the Spotify API without asking the user to log in each time, we need a Refresh Token.
Authorize your app by visiting this URL in your browser (replace placeholders):
https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://127.0.0.1:3000/api/callback&scope=user-read-currently-playing
It will look like this:

Spotify will redirect to your callback URL with a `code`
parameter. Like this preview, then copy the `code`
from the URL:

Now, exchange it for tokens using curl:
curl -X POST https://accounts.spotify.com/api/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code&code=YOUR_CODE&redirect_uri=http://127.0.0.1:3000/api/callback&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET'
So, in that you should replace `YOUR_CODE`
, `YOUR_CLIENT_ID`
, and `YOUR_CLIENT_SECRET`
with your actual values.
The response will contain:
`access_token`
— short-lived, 1 hour`refresh_token`
— long-lived, can get new access tokens anytime
It will look like this:
{
"access_token": "BQCMCEb_a-p3gv0xBW2g5ktDv8RfmN1tqjWr5d0p7sznH-IC37yl07dO-H3Kg6j9s7ZHCF0Aja_G8nA9IGiqtfOnzvCKlD2ShZ_a_h6_Ph47OZzHScZe4jqDHQiolyDVZH0nK9qucdui1LBRJhaWD4Q8Tkt7HAs7EVUTv5bBX1KxcX6x5DgtsgCM4ZkbXpb5Kik6C4Ix6BK1byzpZb7ybwH3eAE",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "AQC2quhv2u8QDHeocg_f2jPlTMWlQRHVXuSbJx5GHba9ehKcbdSv8_liggExi9BLmYNlAgvFyxDdQ1Ltcwt_FESrfE2dtMGSynSVT8PgXQiSx02YiVGgEGGX0wE0T508ZCQ",
"scope": "user-read-currently-playing"
}
Store `refresh_token`
securely in your `.env.local`
.
Setting Environment Variables
In `.env.local`
, add:
SPOTIFY_CLIENT_ID=your_spotify_client_id
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
SPOTIFY_REFRESH_TOKEN=your_refresh_token
Creating Server Actions
Your `actions.ts`
already has MDX-related functions.
Let's extend it with Spotify helpers:
"use 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!;
const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
const NOW_PLAYING_ENDPOINT =
"https://api.spotify.com/v1/me/player/currently-playing";
async function getAccessToken(): Promise<string> {
const basic = Buffer.from(
`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`
).toString("base64");
const res = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: SPOTIFY_REFRESH_TOKEN,
}),
});
const data = await res.json();
if (!res.ok) throw new Error("Failed to get Spotify token");
return data.access_token;
}
export async function getNowPlaying() {
const accessToken = await getAccessToken();
const res = await fetch(NOW_PLAYING_ENDPOINT, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (res.status === 204 || res.status > 400) {
return { isPlaying: false };
}
const song = await res.json();
return {
isPlaying: song.is_playing,
title: song.item?.name,
artist: song.item?.artists.map((a: { name: string }) => a.name).join(", "),
album: song.item?.album.name,
albumImageUrl: song.item?.album.images[0].url,
songUrl: song.item?.external_urls.spotify,
};
}
Creating a Client Component
Server Actions can be imported into a client component for live updates.
Here's a `Spotify.tsx`
component:
"use client";
import { useEffect, useState } from "react";
import { getNowPlaying } from "@/app/actions";
import Image from "next/image";
export function Spotify() {
const [track, setTrack] = useState<{
isPlaying: boolean;
title?: string;
artist?: string;
album?: string;
albumImageUrl?: string;
songUrl?: string;
} | null>(null);
useEffect(() => {
const fetchTrack = async () => {
const data = await getNowPlaying();
setTrack(data);
};
fetchTrack();
const interval = setInterval(fetchTrack, 3000); // Poll every 3s
return () => clearInterval(interval);
}, []);
if (!track) {
return (
<div className="flex flex-col items-center justify-center">
<p className="text-sm tracking-normal font-semibold animate-bounce text-zinc-800 dark:text-zinc-200">
Loading
</p>
</div>
);
}
if (!track.isPlaying) {
return (
<div className="flex flex-col items-center justify-center">
<p className="text-sm tracking-normal font-semibold text-zinc-800 dark:text-zinc-200">
Spotify is Not Playing
</p>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center">
<Image
src={track.albumImageUrl as string}
alt={track.album as string}
width={300}
height={300}
className="rounded-full mt-4 mb-6 animate-spin [animation-duration:8s]"
/>
<a
href={track.songUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm tracking-normal font-semibold text-zinc-800 dark:text-zinc-200"
>
{track.title} by {track.artist}
</a>
</div>
);
}
Warning: before using this
`<Image/>`
component, ensure you use a remote image, you can provide a URL string for the src property. By configuring the`next.config.ts`
file, you can allow images from Spotify's CDN.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "i.scdn.co",
},
],
},
};
export default nextConfig;
It will be shown in your `page.tsx`
:
import { Spotify } from "@/components/Spotify";
export default function Page() {
return (
<div className="flex flex-col items-center justify-center min-h-screen font-mono">
<Spotify />
</div>
);
}
Running Your App
Run your Next.js app:
npm run dev
Open your browser at `http://localhost:3000`
to see currently playing track. It will show the preview of the currently playing track from your Spotify account.

You should see the currently playing track from your Spotify account, updating every 3 seconds.
Deployment Notes
When deploying (e.g., to Vercel):
- Ensure
`.env.local`
values are set in the project settings. - Server Actions keep your Spotify credentials safe on the server.
- The client only receives safe, minimal track data.
Conclusion
With this setup, your Next.js app can securely fetch and display real-time Spotify Currently Playing data using Server Actions in `actions.ts`
.
This approach keeps secrets safe, updates the UI every 3 seconds, and is easily extendable to other Spotify endpoints like `recently-played`
or `top-tracks`
.
You can check the full code on GitHub.