How to use the Spotify API with NextJS 15, the simple way!

When I developed my portfolio, I wanted to display my Spotify listening live. And after a few hours of searching. I managed to get to the bottom of it, and here's how I did it.

Result

Here's the result you'll get at the end of this article.

sans musique

avec musique

Spotify

I'm busy writing down in a clear and concise way, the easiest way to get your Spotify API keys and your TOKEN. Of these, it's the TOKEN that may seem the most complex to retrieve if you're not familiar with the process. However, once this step has been completed, recovery simple and hassle-free.

Environment

Add the three variables SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET and SPOTIFY_REFRESH_TOKEN to your environment file. and SPOTIFY_REFRESH_TOKEN.

.env
SPOTIFY_CLIENT_ID=""
SPOTIFY_CLIENT_SECRET=""
SPOTIFY_REFRESH_TOKEN=""

Typing

We will now create three files in src/types/spotify/ named entities.d.ts, entity.d.ts & request.d.ts. & request.d.ts

src/lib/spotify/entities.d.ts
import type { SpotifyEntity, Image } from './entity';

interface Album extends SpotifyEntity {
  type: 'album';
  popularity: number;
}

interface Artist extends SpotifyEntity {
  type: 'artist';
  popularity: number;
}

export interface Track extends SpotifyEntity {
  type: 'track';
  popularity: number;
  duration_ms: number;
  album: Album;
  artists: Array<Artist>;
  preview_url: string;
  is_playable: boolean;
  is_local: boolean;
}

export interface ReadableTrack {
  name: string;
  artist: string;
  album: string;
  previewUrl: string;
  url: string;
  image?: Image;
  hdImage?: Image;
  duration: number;
}
src/types/spotify/entity.d.ts
export type SpotifyEntityType = 'album' | 'artist' | 'playlist' | 'track';
export type SpotifyEntityUri = `spotify:${SpotifyEntityType}:${string}`;

export interface Image {
  url: string;
  height?: number | null;
  width?: number | null;
}

export interface SpotifyEntity {
  id: string;
  name: string;
  href: string;
  uri: SpotifyEntityUri;
  type: SpotifyEntityType;
  images: Array<Image>;
  external_urls: { spotify: string };
}
src/types/spotify/request.d.ts
import type { ReadableTrack, Track } from './entities';
import type { SpotifyEntity } from './entity.d';

export interface SpotifyResponse<T extends SpotifyEntity | PlayHistoryObject> {
  href: string;
  next?: string | null;
  previous?: string | null;
  limit: number;
  offset: number;
  total: number;
  items: Array<T>;
}

export interface ErrorResponse {
  error: {
    status: number;
    message: string;
  };
}

export interface NowPlayingResponse {
  is_playing: boolean;
  item: Track;
}

export interface PlayHistoryObject {
  track: Track;
  played_at?: string;
  context?: unknown | null;
}

export type NowPlayingAPIResponse = {
  track?: ReadableTrack | null;
  isPlaying: boolean;
} | null;

Spotify library

We're now going to create a file in src/lib/ named spotify.ts and put all our business logic in it.

src/lib/spotify.ts
import type {ErrorResponse, NowPlayingResponse, PlayHistoryObject, SpotifyResponse,} from '@/types/spotify/request.d';

const serialize = (obj: Record<string | number, string | number | boolean>) => {
  const str = [];
  for (const p in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, p)) {
      str.push(`${encodeURIComponent(p)}=${encodeURIComponent(obj[p])}`);
    }
  }
  return str.join('&');
};

const buildSpotifyRequest = async <T>(
  endpoint: string,
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
  body?: Record<string, unknown>,
): Promise<T | ErrorResponse> => {
  const {access_token: accessToken} = await getAccessToken().catch(null);
  if (!accessToken) {
    return {
      error: {message: 'Could not get access token', status: 401},
    };
  }
  const response = await fetch(endpoint, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
    method,
    body: body && method !== 'GET' ? JSON.stringify(body) : undefined,
  });
  try {
    const json = await response.json();
    if (response.ok) return json as T;
    return json as ErrorResponse;
  } catch {
    return {
      error: {
        message: response.statusText || 'Server error',
        status: response.status || 500,
      },
    };
  }
};

const clientId = process.env.SPOTIFY_CLIENT_ID || '';
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET || '';
const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN || '';

const basic = btoa(`${clientId}:${clientSecret}`);
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';

const getAccessToken = async (): Promise<{ access_token?: string }> => {
  try {
    const response = await fetch(TOKEN_ENDPOINT, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${basic}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: serialize({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
      }),
      next: {
        revalidate: 0,
      },
    });

    return response.json();
  } catch {
    return {access_token: undefined};
  }
};

const NOW_PLAYING_ENDPOINT = 'https://api.spotify.com/v1/me/player/currently-playing';
export const getNowPlaying = async () => buildSpotifyRequest<NowPlayingResponse>(NOW_PLAYING_ENDPOINT);

const RECENTLY_PLAYED_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played?limit=1';
export const getRecentlyPlayed = async () => buildSpotifyRequest<SpotifyResponse<PlayHistoryObject>>(RECENTLY_PLAYED_ENDPOINT,);

Component

We're now going to create a component and set up our logic so that the Spotify API is called to update the data on the client side.

src/components/spotify.tsx
import {useRequest} from "@/hooks/use-request";
import type {NowPlayingAPIResponse} from "@/types/spotify/request";

export function CurrentlyListenSpotify() {
  const {data} = useRequest<NowPlayingAPIResponse>('api/spotify/now-playing');
  const {track} = data || {isPlaying: false};

  return (
    <div className="space-y-8">
      {data?.isPlaying ? (
        <a
          href={track?.url}
          target={'_blank'}
          className="select-none"
          title={`En train d'écouter ${track?.name} par ${track?.artist} sur Spotify`}
          rel="noreferrer"
        >
          <div className="flex flex-row-reverse items-center justify-between gap-2">
            <Image
              src={track?.image?.url as string}
              alt={`Couverture d'album : '${track?.album}' par '${track?.artist}'`}
              width={56}
              height={56}
              quality={50}
              className="size-6 rounded border"
            />
            <div className="flex flex-col">
              <div className="font-semibold">{track?.artist}</div>
              <span className="inline-flex"></span>
              <p className="text-xs text-gray-500">{track?.name}</p>
            </div>
          </div>
        </a>
      ) : (
        <div className="flex flex-row-reverse items-center justify-between gap-2">
          <SpotifyIcon />

          <div className="flex flex-col">
            <div className="font-semibold">Rien n'est écouté</div>
            <span className="inline-flex"></span>
            <p className="text-xs text-muted-foreground">Spotify</p></div>
        </div>
      )}
    </div>
  )
}

Finally

All that's left is to add your component where you want it to be displayed.

import {CurrentlyListenSpotify} from "@/components/spotify";

export function App () {
  return (
    <div>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>

      <CurrentlyListenSpotify/>
    </div>
  )
}