Comment utiliser l'API de Spotify avec NextJS 15 ?

Lorsque j'ai dévéloppé mon portfolio, j'ai voulu afficher mon écoute Spotify en direct. Et après quelque heure de recherche. J'ai réussi à en voir le bout et voici comment j'ai fait.

Résultat obtenu

Voici le résultat que vous allez obtenir à la fin de cet article.

sans musique

avec musique

Spotify

Je suis occupé à écrire de manière claire et concise, la méthode la plus simple pour obtenir vos clés d'API Spotify ainsi que votre TOKEN. Parmi ces éléments, c'est le TOKEN qui peut sembler le plus complexe à récupérer lorsqu'on n'est pas familier avec le processus. Cependant, une fois cette étape accomplie, la récupération devient simple et sans souci.

Environnement

Ajouter dans votre fichier d'environnement les trois variables SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET et SPOTIFY_REFRESH_TOKEN.

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

Typage

Nous allons maintenant créer trois fichiers dans src/types/spotify/ nommés entities.d.ts, entity.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;

Librairie spotify

Nous allons maintenant créer dans src/lib/ un fichier nommé spotify.ts et mettre toute notre logique métier dedans.

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

Composant

Nous allons maintenant créer un composant et mettre notre logique pour que l'API de Spotify soit call pour mettre à jour la donnée côté client.

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

Enfin

Il ne vous reste plus qu'à ajouter votre composant à l'endroit que vous souhaitez l'afficher.

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

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

      <CurrentlyListenSpotify/>
    </div>
  )
}