Hi, Capucine here. In this article we'll go over how to connect and retrieve data from Spotify API using NextAuth 5 and Nextjs 15.
In practice that means by the end of it, you will have a page that allow a user to connects themselves using Spotify, and then retrieve some simple data to display.
Let's get started !
Table of content
- Setting up your Nexts project
- Creating a Spotify App
- Your
.env.local
file - Configuring Auth.js for Spotify
- Handling Signin and Signout
- Setting up an API
- Displaying user data with SWR
- Implementing Refresh Token
- Going further
- Conclusion
Setting up your Nextjs project
We'll start by creating our new NextJS project as well as install some more dependencies. First of all let's run the following command:
npx create-next-app@latest
You are free to give your project a cool name and choose whatever configuration best fits you. I will personally be using the following:
What is your project named? spotify-nextauth ✔ Would you like to use TypeScript? No / Yes ✔ Would you like to use ESLint? No / Yes ✔ Would you like to use Tailwind CSS? No / Yes ✔ Would you like your code inside a `src/` directory? No / Yes ✔ Would you like to use App Router? (recommended) No / Yes ✔ Would you like to use Turbopack for next dev? No / Yes ✔ Would you like to customize the import alias (@/* by default)? No / Yes
Now you can move down to your newly created folder with:
cd your-project-name
Next you can add NextAuth to the project:
#npm
npm install next-auth@beta
#yarn
yarn add next-auth@beta
Finally it is necessary to run:
npx auth secret
It will generate a random value for your NEXTAUTH_SECRET, a variable used for encryption, and put it in your .env.local
file. For more information check out the documentation
And you should now be good to go !
Creating a Spotify App
Unto creating your very own Spotify App. Head over to the Spotify dashboard for developers and log in with your Spotify account. Click on the 'Create app' button.
A Spotify account is required :)
You can give your app a name and a description. Fill in the 'Website' field with :
http://localhost:3000/
and 'Redirect URLs' :
http://localhost:3000/api/auth/callback/spotify
For wich API to use, We'll go with the WebAPI for this tutorial. You will also have to agree with the terms of service. By the end it should look something like this:
Once your app is created, you can into the settings and retrieve you client ID as well as your client secret.
Your .env.local
file
Now that you've created your Spotify App and obtained your client id and secret you can add those to your .env.local
file at the root of your project. It shoud look something like this:
AUTH_SECRET="jGTHwGl6ZdTp1fsXwI2oNinf4BH8PU3BFhA31QEV8Is=" #Added by `npx auth`.
AUTH_SPOTIFY_ID=your-client-id
AUTH_SPOTIFY_SECRET=your-client-secret
The naming pattern of your client id and client secret is important. For Auth.js will automatically pick them when configuring the Spotify Provider. For more information check out the documentation
Additionally if you wish to use a custom base path for your Next.js application, you can add the NEXTAUTH_URL
variable. More info here
Configuring Auth.js for Spotify
Time has now come to start coding a bit ! Yeah !! Let's start by creating a auth.js
file at the root of your project, with the following content:
// @/auth.js
import NextAuth from "next-auth"
import Spotify from "next-auth/providers/spotify"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Spotify],
})
In this file we define our NextAuth configuration wich for now only include declaring Spotify as a provider. If your enviroment variables are done correctly Auth.js will automatically import the id and secret for the Spotify provider. We then export the configuration to be accesible all across your project.
The auth.js
file is one of the more important change going from NextAuth.js v4 to Auth.js (v5). Learn more about it.
Inside of your app folder, create a new Route Handler for the api/auth/[...nextauth] directory.
// @/app/api/auth/[...nextauth]/route.js
import { handlers } from "@/auth" // Referring to the auth.js we just created
export const { GET, POST } = handlers
This file initialize NextAuth and export it for the GET and POST request of the route, using the configuration of auth.js
Handling Signin and Signout
In this guide we'll try to keep our frontend very minimalistic. That's why we'll only use the default page.js file today.
Open app/page.js
you'll see there's quite a lot of stuff in there created by the Nextjs quickstart, you can clean it all. Your file should look something like this:
// @/app/page.js
export default function Home() {
}
Now let's import and call the auth()
function, the more conveniant replacement of getServerSession()
. This function will allows to determine if our user is logged in or not; And if they are, give us acces to their information, while remaining on server side.
Quick note: Using auth() will change our page to dynamic rendering, but will not turn it into a client components. See
// @/app/page.js
import { auth } from "@/auth"
export default async function Home() {
const session = await auth()
if (session) {
return (
<h1>You are logged in !!!</h1>
)
} else {
return (
<h1>You are NOT logged in...</h1>
)
}
}
Don't forget to make your function async ;)
Finally let's add the sign in and sign out button. This would usually require to create a client component for interactivity but with a ingenious usage of the form HTML element, it is not needed.
// @/app/page.js
import { auth, signIn, signOut } from "@/auth"
export default async function Home() {
const session = await auth()
if (session) {
return (
<div>
<h1>You are logged in !!!</h1>
<form
action={async () => {
"use server"
await signOut()
}}
>
<button type="submit">Sign Out</button>
</form>
</div>
)
} else {
return (
<div>
<h1>You are NOT logged in...</h1>
<form
action={async () => {
"use server"
await signIn("spotify")
}}
>
<button type="submit">Sign in</button>
</form>
</div>
)
}
}
Now comes the time of truth, you can run npm run dev
, go to your localhost:3000, and if all goes well you should be able to log in and out of your spotify account. If it doesn't make sure that you have everything set up correctly, also beware that only the account with wich you created your spotify app will be allowed to connect to the aplication.
You can also use the session
variable to obtain some basic data of the user. You can play around with things like this:
...
if(session) {
return (
<h1>Hi {session.user.name}, you are logged in.</h1>
)
}
...
Setting up an API
Now that you succesully connected yourself, it is time to set up your own API to be able to communicate and retrieve data from the Spotify API. First let's update our Auth.js configuration:
// @/auth.js
import NextAuth from "next-auth"
import Spotify from "next-auth/providers/spotify"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Spotify({
authorization: {
url: "https://accounts.spotify.com/authorize",
params: {
scope: "user-top-read",
},
},
})
],
callbacks: {
async jwt({token, account}) {
//first time login
if(account) {
return {
...token,
access_token: account.access_token,
expires_at: account.expires_at,
refresh_token: account.refresh_token,
}
}
return token
},
async session({ session, token }) {
session.access_token = token.access_token
return session
}
}
})
We do two things here:
- First of all we add an authorization and scope string for our provider, granting us acces to part of the Spotify API.
- Secondly we uses callbacks to save our tokens and expiration time. We then then in the session callback make
acces_token
accesible with theauth()
function. You can even test it out if you want: log out, login, and your session object inpage.js
should now contain access tokens.
And now that our acces token are accesible let's create our own API that will retrieve user data from Spotify's. Create a new Route Handler for the app/api/user/top/tracks directory.
// @/app/api/user/top/tracks/route.js
import { auth } from "@/auth"
export async function GET() {
const session = await auth()
//checks if user is logged in
if(session) {
//make the fetch call to spotify's API
const response = await fetch(`https://api.spotify.com/v1/me/top/tracks`,
{
headers: {
'Authorization' : `Bearer ${session.access_token}`
}
});
//return the data
const data = await response.json()
return Response.json(await data)
} else {
return Response.json('you must be logged in')
}
}
This is quite simple but effective. When the API is called, it retrieves the session using auth()
and sends the request to the Spotify API with the appropriate Authorization Bearer token. The response is then returned.
By using this additional interface instead of fetching data directly within our pages, we gain the flexibility to fetch data from anywhere in our application without worrying about accessing the session or inadvertently exposing sensitive information—especially in client components.
Displaying user data with SWR
With our API now functional, let’s display user data dynamically on the page. We’ll use SWR for client-side data fetching, which makes caching, revalidation, and error handling effortless. For more on data fetching on the client in Nextjs 15 check out the official docs.
Install SWR with the following commands:
#npm
npm i swr
#yarn
yarn add swr
Note: If you're using React 19, you might need to downgrade to ensure compatibility
Next, create a new client component. It will handle data fetching and dynamically display the results:
// @/app/songs.js
'use client'
import useSWR from 'swr'
export function Songs() {
const fetcher = (...args) => fetch(...args).then(res => res.json())
const { data, error, isLoading } = useSWR('/api/user/top/tracks', fetcher)
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
// Render data
return (
<ul>
{data.items.map(({ name, artists, id }) => (
<li key={id} >{name}</li>
))}
</ul>
)
}
Here’s what’s happening:
- Fetcher Function: We create a fetcher function, which is just a wrapper of the native fetch.
-
SWR Integration: The
useSWR
hook retrieves data from our API, providing states like isLoading and error. - Rendering the Data: Once the data is successfully fetched, we iterate over it using map() and render each item dynamically as a list element.
For a cleaner look, you can add some simple styling:
...
return (
<ul className='flex flex-col gap-2 my-8'>
{data.items.map(({ name, artists, id }) => (
<li key={id} className='flex flex-col'>
<span className='text-white'>{name}</span>
<span className='text-gray-400'>{artists[0].name}</span>
</li>
))}
</ul>
)
You can now visit your localhost page to see your favorite songs displayed! 🎉
Implementing Refresh Token
This is the final stretch: setting up a refresh token mechanism to keep your app connected to the Spotify API.
Why Implement a Refresh Token you might ask ? Well, for security reasons, Access Tokens have a short lifespan. For exemple Spotify's is only 20 minute, it might run out while your user is still browsing your page !! To maintain access without forcing users to log in repeatedly, a refresh token is used to request a new access token when the original one expires. This ensures a seamless user experience and keeps your application functional.
Unfortunately, there's currently a bug with Auth.js when it comes to refreshing tokens using Next.js's app router. You can read more about it here.
Since we can’t rely on the simple built-in method, we’ll use middleware instead. First, create a file called middleware.js
at the root of your project (not in the app folder):
// @/middleware.js
import { encode, getToken} from 'next-auth/jwt';
import { NextResponse } from 'next/server';
//cookie name changes depending on context
const sessionCookie = process.env.NEXTAUTH_URL?.startsWith('https://')
? '__Secure-authjs.session-token'
: 'authjs.session-token';
//refreshing token logic here
async function refreshToken(token) {
const basic = Buffer.from(`${process.env.AUTH_SPOTIFY_ID}:${process.env.AUTH_SPOTIFY_SECRET}`).toString('base64');
try {
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: token.refresh_token,
})
});
const newTokens = await response.json()
if (!response.ok) throw newTokens
console.log('Refresh token succufully updated');
return {
...token,
access_token: newTokens.access_token,
expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
refresh_token: newTokens.refresh_token
}
} catch (error) {
console.error("Error refreshing access token:", error)
token.error = "RefreshTokenError"
return token
}
}
export const config = {
matcher: '/api/user/:path*',
};
// Middleware logic here
export async function middleware(request) {
console.log('Middleware triggered for API route:', request.nextUrl.pathname);
const token = await getToken({ req: request, secret:process.env.AUTH_SECRET});
const response = NextResponse.next();
//refresh token logic here
if (Date.now() > token.expires_at * 1000) {
console.log('token need to be refreshed');
const newToken = await refreshToken(token)
const newSessionToken = await encode({
secret: process.env.AUTH_SECRET,
token: newToken,
salt: sessionCookie,
})
response.cookies.set(sessionCookie, newSessionToken)
}
return response
}
This is quite the block of code, but let's try to walk you throught it:
- Session Cookie Name: To find and update the user's session token securely, we need to set the correct cookie name based on whether the app is running over HTTPS. Auth.js uses different names for secure and non-secure environments:
const sessionCookie = process.env.NEXTAUTH_URL?.startsWith('https://')
? '__Secure-authjs.session-token'
: 'authjs.session-token';
-
Token Refresh Logic:The
refreshToken()
function is responsible for getting a new access token from Spotify when the current one expires. It starts by authenticating the request with Spotify by encoding your client ID and secret in Base64, as required by their API:
const basic = Buffer.from(`${process.env.AUTH_SPOTIFY_ID}:${process.env.AUTH_SPOTIFY_SECRET}`).toString('base64');
A POST request is then sent to the /api/token endpoint. If the request succeeds, the response provides a new access token, its expiration time, and a new refresh token. These are then used to return the updated token.
In the event of a failure, the function logs the error and flags the token with a RefreshTokenError.
- Configuration: The config object ensures that the middleware only runs for specific routes:
export const config = {
matcher: '/api/user/:path*',
};
This helps avoid unnecessary middleware processing for other routes, optimizing the performance of your application.
-
Middleware Execution: The
middleware()
function is fairly straightfoward: when a request is made, the middleware retrieves the current token usinggetToken()
. It then checks whether the token's expiration time has passed:
if (Date.now() > token.expires_at * 1000) {
console.log('Token needs refreshing');
}
If the token is expired, the middleware calls refreshToken() to obtain a new access token. The updated token is then encoded and stored as a cookie to maintain the session:
const newSessionToken = await encode({
secret: process.env.AUTH_SECRET,
token: newToken,
salt: sessionCookie,
});
response.cookies.set(sessionCookie, newSessionToken);
This setup ensures that API requests remain authenticated without user interruption by automatically updating the token when necessary.
And with that you should have a functionning refresh token mechanism ! Although using middleware might be more complex than the standard approach, it provides a robust solution in the current context. With this setup, your app is now well-equipped to maintain secure and persistent user sessions !
Going further
While this guide aimed to keep things simple, there’s plenty of room to extend your Spotify-connected app into something even more robust. Here are some ideas to consider:
Separate Connected and Disconnected States:
Consider creating distinct pages for when a user is connected versus not connected. With Next.js's parallel rendering, you can efficiently render these different states, ensuring a smoother user experience.Enhanced UI for User Data:
Improve the visual presentation of the data retrieved from Spotify. Experiment with different layouts, animations, or even integrate a design library to make your app more visually appealing.Advanced API Requests:
Extend your API to handle more complex queries, such as accepting dynamic range parameters to fetch specific segments of user data. This could allow for features like custom time ranges or pagination.Build a Music Player:
Take it a step further by integrating a music player into your app. This could let users preview tracks directly from your interface, making for a more interactive experience.
These improvements provide a great opportunity to deepen your understanding of Next.js and NextAuth; with the potential to transform your project from a simple demo into a fully-featured, interactive application !
Conclusion
In this guide, we built a complete Spotify-connected app using a robust tech stack using NextAuth 5 and Next.js 15. We covered everything from creating your Spotify app and configuring environment variables to integrating secure authentication with NextAuth 5, fetching user data with SWR, and even implementing a refresh token mechanism via middleware. With these modern tools at your disposal, you now have a solid foundation to expand your application’s functionality and enhance the user experience.
Happy coding !!
Top comments (0)