DEV Community

Cover image for NextAuth and Spotify API: A 2025 Dev’s Guide
Capucine Trossat
Capucine Trossat

Posted on

NextAuth and Spotify API: A 2025 Dev’s Guide

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 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next you can add NextAuth to the project:

#npm
npm install next-auth@beta
#yarn
yarn add next-auth@beta
Enter fullscreen mode Exit fullscreen mode

Finally it is necessary to run:

npx auth secret
Enter fullscreen mode Exit fullscreen mode

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 :)

Image description

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:

Image description

Once your app is created, you can into the settings and retrieve you client ID as well as your client secret.

Image description

Image description

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
Enter fullscreen mode Exit fullscreen mode

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],
})

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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() {

}
Enter fullscreen mode Exit fullscreen mode

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>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
} 

...
Enter fullscreen mode Exit fullscreen mode

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

    }
  }
})
Enter fullscreen mode Exit fullscreen mode

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 the auth() function. You can even test it out if you want: log out, login, and your session object in page.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')
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
)

Enter fullscreen mode Exit fullscreen mode

You can now visit your localhost page to see your favorite songs displayed! 🎉

Image description

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
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode
  • 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');
Enter fullscreen mode Exit fullscreen mode

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*',
  };
Enter fullscreen mode Exit fullscreen mode

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 using getToken(). It then checks whether the token's expiration time has passed:
  if (Date.now() > token.expires_at * 1000) {
    console.log('Token needs refreshing');
  }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)