Crea tu propio Spotify Wrapper
En este post aprenderemos cómo crear un producto basado en una API pública y algunos de los riesgos que conlleva a nivel de negocio. En concreto veremos dos tipo de autentificación para el consumo de la API, client credentials y OAuth PKCE: para ello construiremos, una barra de búsqueda de la lista de álbums del artista seleccionado con react-vite, y un Spotify Wrapper personalizado con vainilla js.
Este post se basaba en una red social musical https://github.com/spariva/Music-Life que hice como TFG, donde podías crear playlists añadiendo parámetros personalizados, afinando así el algoritmo de recomendación interno de Spotify. Para mi sorpresa, a la hora de preparar la charla, me daba error 404, dado el error fui a comprobar los endpoints correspondientes en la documentación oficial: https://developer.spotify.com/ donde advertían de estar «deprecated» junto a esta noticia:
Ahora imaginemos que hubiera ido un paso más allá, creando una startup de esta idea. Sin previo aviso Spotify habría terminado mi negocio de la noche a la mañana, así como ha pasado a tantas startups de IA que eran un mero envoltorio de la API de OpenAI. De esto, he aprendido el problema de la dependencia tecnológica, en este caso toda mi red social depende de la API de Spotify.
Entonces, plan B, vamos a la lista de endpoints, y vemos cuáles han dejado. Tengo la idea de crear un Spotify Wrapper personalizado:
Para ello necesitaré como mínimo la información del usuario y las listas de las canciones que más escucha. Compruebo que dichos endpoints siguen en activo, y veo el tipo de respuesta en el botón de try it out. Para el buscador de álbums me pide como parámetro de búsqueda el id del artista, por tanto para recuperarlo necesitaré el endpoint de buscar artista.
Ahora, sabemos qué endpoints necesitamos, pero nos advierten en la documentación que dichos endpoints requiere un token que Spotify te da. Para conseguirlo debemos elegir uno de los 4 posibles métodos de autentificación.
Para el buscador:
No necesitamos información del usuario ni modificar nada de sus datos, por lo cual nos decantamos por el client/credentials https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow el cual intercambia a través de un endpoint el clientId, y el clientSecret que nos proporcionó Spotify en el dashboard, por un token de acceso con el que realizar las consultas.

Por otro lado, para el token que permita darnos información del usuario necesitamos elegiremos el PKCE ya que vamos a guardar dicho token en el buscador, y necesitamos el extra de seguridad que este método proporciona.
Antes de empezar necesitamos crear una app en el dashboard de Spotify Developer; donde nos darán el clientId, clientSecret y configuraremos a qué url navegará Spotify tras realizar con éxito el intercambio de dichos secretos por el token de acceso (en nuestro caso ponemos localhost y el puerto a utilizar, pero en caso de una web desplegada sería la propia url, por ejemplo «www.musiclife.es/perfil»).
En Spotify for Developers inicia sesión con tu cuenta de Spotify. Si no tienes una, puedes crear una a través del sitio web de Spotify. Después de iniciar sesión, dirígete al Dashboard seleccionando tu imagen de perfil en la esquina superior derecha de la pantalla y luego «Dashboard».


Dale una descripción a tu aplicación y una URI de redirección. Como estamos creando una aplicación para nuestro uso personal, inserta la redirección como http://localhost/.
Solo usaremos la API Web para este tutorial, así que asegúrate de marcarla como la API que planeamos usar.

Al fin, abrimos Visual Studio Code, en la consola de nuestra elección creamos un proyecto de vite: Usaremos Vite, que es una herramienta que nos permite crear y ejecutar aplicaciones React de tamaño pequeño a mediano. Puede instalarse como un paquete desde un gestor de paquetes de Node.
Para verificar si Node está instalado, ejecuta el siguiente comando en la terminal: node -v
Ahora que hemos asegurado que Node está instalado, ¡es hora de comenzar a usar npm! Podemos usar npm para instalar Vite. Escribe lo siguiente en la terminal y presiona enter: npm install vite@latest
Esto descargará la versión más reciente de Vite.
Ejecuta el siguiente comando: npm create vite@latest
Esto comenzará con una serie de preguntas que deberás responder para crear la aplicación. Elige cómo llamar al proyecto, se te pedirá que selecciones qué tipo de aplicación deseas crear. Usa las teclas de flecha para seleccionar «React» y presiona enter. Luego, se te preguntará si deseas configurar la aplicación React con TypeScript o JavaScript regular. Vamos a elegir solo JavaScript. Pero espera, ¿qué pasa con este «SWC»? SWC significa «Speedy Web Compiler» y permite que tu código se procese un poco más rápido. Para este tutorial, no importa si lo seleccionas o no.
Después de este punto, todo debería estar listo y Vite te mostrará las instrucciones para ejecutar tu aplicación React. Primero, usa el comando cd para entrar en la nueva carpeta de Vite que creaste: cd nombredetuapp. A continuación, ejecuta el siguiente comando para instalar las dependencias: npm install y comprueba con npm run dev que el proyecto funciona correctamente en el localhost.
En el archivo .env guardaremos en local nuestros secretos más terribles. Y añadiremos una línea al archivo de .gitignore que diga .env para que al subir a git, ignore dicho archivo y así no acaben nuestras claves en manos ajenas.

En el .env haremos clave valor tal que así: .env
VITE_CLIENT_ID=YOUR CLIENT ID HERE
VITE_CLIENT_SECRET=YOUR CLIENT SECRET HERE
Y para poder acceder a ese valor desde la app usaremos la siguiente fórmula:
// App.jsx
const clientId = import.meta.env.VITE_CLIENT_ID;
const clientSecret = import.meta.env.VITE_CLIENT_SECRET;
- Importaciones:
./App.css: Importa los estilos CSS para el componente.- useState y useEffect de React: Hooks para manejar el estado y los efectos secundarios en componentes funcionales.
- Componentes de React Bootstrap: para construir la interfaz de usuario.
- Variables de Entorno:
- clientId y clientSecret: Se obtienen de las variables de entorno definidas en el archivo .env para autenticar con la API de Spotify.
import './App.css'
import { useState, useEffect } from "react";
import { FormControl, InputGroup, Container, Button, Row, Card } from "react-bootstrap";
- Estados:
- searchInput: Almacena el texto de búsqueda ingresado por el usuario.
- accessToken: Almacena el token de acceso obtenido de la API de Spotify.
- albums: Almacena los álbumes obtenidos de la API de Spotify.
function App() {
//? Estados: Se definen tres estados usando useState
const [searchInput, setSearchInput] = useState("");
const [accessToken, setAccessToken] = useState("");
const [albums, setAlbums] = useState([]);
- useEffect:
- Son dos parámetros, el primero la función a ejecutar, el segundo, cuándo se ejecuta. En este caso cuando el componente se monta (debido al array de dependencias vacío
[]que no está vigilando ninguna en específico). - Configura los parámetros de autenticación authParams para hacer una solicitud
POSTa la API de Spotify, que nos dará con el uso de fetch un token de acceso, que se guarda en el estado con setAccessToken.
- Son dos parámetros, el primero la función a ejecutar, el segundo, cuándo se ejecuta. En este caso cuando el componente se monta (debido al array de dependencias vacío
useEffect(() => {
let authParams = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body:
"grant_type=client_credentials&client_id=" +
clientId +
"&client_secret=" +
clientSecret,
};
fetch("https://accounts.spotify.com/api/token", authParams)
.then((result) => result.json())
.then((data) => {
setAccessToken(data.access_token);
});
}, []);
- Función search:
- Es una función asíncrona que se ejecuta al buscar un artista.
- Configura los parámetros de la solicitud para hacer una solicitud
GETa la API de Spotify. - Hace una solicitud fetch para obtener el ID del artista basado en el searchInput.
- Usa el ID del artista para hacer otra solicitud y obtener los álbumes del artista.
- Guarda los álbumes en el estado albums con setAlbums.
async function search() {
let artistParams = {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + accessToken,
},
};
// Get Artist
const artistID = await fetch(
"https://api.spotify.com/v1/search?q=" + searchInput + "&type=artist",
artistParams
)
.then((result) => result.json())
.then((data) => {
return data.artists.items[0].id;
});
//? Albums
await fetch(
"https://api.spotify.com/v1/artists/" +
artistID +
"/albums?include_groups=album&market=US&limit=50",
artistParams
)
.then((result) => result.json())
.then((data) => {
console.log(data);
console.log(data.items);
setAlbums(data.items);
});
}
- Renderizado: (usando componentes de React Bootstrap previamente importados)
- Entrada de búsqueda:
- Un Container que envuelve un InputGroup con FormControl para la entrada de búsqueda y un Button para iniciar la búsqueda.
- El FormControl tiene eventos onKeyDown para manejar la entrada del usuario y ejecutar la búsqueda cuando se presiona «Enter» y onChange para setear el Input.
- Resultados de la búsqueda:
- Otro Container que envuelve un row donde se mapean los álbumes obtenidos y se renderizan como Card de Bootstrap.
- Cada Card muestra la imagen del álbum, el nombre, la fecha de lanzamiento y un botón con un enlace al álbum en Spotify.
- Entrada de búsqueda:
- Exportación: Se exporta el componente para que pueda ser utilizado en otras partes de la aplicación.
return (
<>
<Container>
<InputGroup>
<FormControl
placeholder="Search For Artist"
type="input"
aria-label="Search for an Artist"
onKeyDown={(event) => {
if (event.key === "Enter") {
search();
}
}}
onChange={(event) => setSearchInput(event.target.value)}
style={{
width: "300px",
height: "35px",
borderWidth: "0px",
borderStyle: "solid",
borderRadius: "5px",
marginRight: "10px",
paddingLeft: "10px",
}}
/>
<Button onClick={search} variant='primary'>Search</Button>
</InputGroup>
</Container>
<Container>
<Row
style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-around",
alignContent: "center",
}}
>
{/* Mapear el state albums */}
{albums.map((album) => {
return (
<Card
key={album.id}
style={{
backgroundColor: "white",
margin: "10px",
borderRadius: "5px",
marginBottom: "30px",
}}
>
<Card.Img
width={200}
src={album.images[0].url}
style={{
borderRadius: "4%",
}}
/>
<Card.Body>
<Card.Title
style={{
whiteSpace: "wrap",
fontWeight: "bold",
maxWidth: "200px",
fontSize: "18px",
marginTop: "10px",
color: "black",
}}
>
{album.name}
</Card.Title>
<Card.Text
style={{
color: "black",
}}
>
Release Date: <br /> {album.release_date}
</Card.Text>
<Button
href={album.external_urls.spotify}
style={{
backgroundColor: "black",
color: "white",
fontWeight: "bold",
fontSize: "15px",
borderRadius: "5px",
padding: "10px",
}}
>
Album Link
</Button>
</Card.Body>
</Card>
);
})}
</Row>
</Container>
</>
);
}
El resultado final sería este:
Por otro lado nuestro Spotify Wrapper, (hecho con html y js vainilla para centrarnos en la API y el proceso de OAuth) en vez de client credentials utiliza el PKCE, donde generamos de forma aleatoria un código y un «challenge» con ese código, usando SHA-256 y codificación Base64 URL-safe.
Al hacer la primera petición de OAuth (el user es dirigido a una pantalla donde pone su cuenta de Spotify). Si todo sale bien, se le redirige adonde hayas indicado en el redirect Uris, del dashboard de spotify. En este caso a localhost como vimos antes.
Pero, qué pasaría si ahora con ese token ligado a mi cuenta, me hackeasen con CSRF (en vez de volver a mi sitio web, el user vuelve a un clon que no me pertence y capturan así el token). Para que esto no pase, la url ya no es solo localhost, sino que hay un parámetro extra, code, con un valor asociado al challenge que hicimos antes de enviar la petición, y así evitamos sustos innecesarios.
Por si no se viera bien: la línea donde añadimos el scope, le decimos qué permisos queremos tener, según ello podremos acceder o no a ciertos endpoints: params.append(«scope», «user-read-private user-read-email user-top-read playlist-modify-private playlist-modify-public»); Y también cabe mencionar que guardamos el verifier en el localStorage para más tarde contrastarlo.
Vemos como recuperamos el id del .env, y de la url los parámetros, en concreto el code. Si no hay dicho parámetro llamamos a redirectToAuthCodeFlow. Y una vez realizada la función, ya habría dicho code en la url y llamamos a las funciones que por orden nos van a: canjear id y code por el token de acceso, que usaremos para hacer peticiones a las apis en las siguientes funciones, recuperando nuestra información de usuario, nuestras canciones y artistas favoritas.
Por último llamaremos a tres métodos que crean elementos en el DOM rellenando con dichos datos el html.
Pongamos que todo ha salido bien y ya tenemos el code. Ahora lo canjearemos por el token junto con nuestro clientId, si ambos parámetros son válidos y la petición es correcta obtendremos nuestro token de uso limitado (1 hora).
Dado que estamos haciendo peticiones a una API lo haremos de forma asíncrona con async, await y fetch (también podríamos usar axios u otras opciones). Y parseamos la respuesta con la función json().
Ahora eligiendo cada endpoint en base a la documentación y usando el token de autentificación en los parámetros; llamamos al endpoint que devuelve toda la info del user del token, y la de sus canciones y artistas favs.
Ahora que tenemos la información solo tenemos que pintarla, pero nuestro html es un mero esqueleto con clases de bootstrap:
Usaremos document.createElement(«tag») para crear elementos, luego les añadiremos clases, cambiaremos innerText, setAttribute etc y cuando estén listos con getElementById y appendChild insertaremos dichos elementos allá donde queremos en nuestro html, añadiendo dichos elementos al DOM.
Las funciones que añaden canciones y artistas tienen como extra el hecho de que recorreremos su array con un forEach (por cada artista, por cada canción), y que en las canciones uniremos con comas si es que hay más de un artista de esa canción con cardText.innerText = track.artists.map(artist => artist.name).join(«, «);
function populateTopTracks(tracks) {
const topTracks = document.getElementById("topTracks");
tracks.items.forEach(track => {
const cardElement = document.createElement("div");
cardElement.className = "card";
cardElement.style.width = "18rem";
const imgElement = document.createElement("img");
imgElement.className = "card-img-top";
imgElement.src = track.album.images[0].url; // Assuming the track object has album images
imgElement.alt = track.name;
const cardBody = document.createElement("div");
cardBody.className = "card-body";
const cardTitle = document.createElement("h5");
cardTitle.className = "card-title";
cardTitle.innerText = track.name;
const cardText = document.createElement("p");
cardText.className = "card-text";
cardText.innerText = track.artists.map(artist => artist.name).join(", "); // Assuming the track object has artists
const cardLink = document.createElement("a");
cardLink.className = "btn btn-primary";
cardLink.href = track.external_urls.spotify; // Assuming the track object has external URLs
cardLink.innerText = "Go to track";
cardBody.appendChild(cardTitle);
cardBody.appendChild(cardText);
cardBody.appendChild(cardLink);
cardElement.appendChild(imgElement);
cardElement.appendChild(cardBody);
topTracks.appendChild(cardElement);
});
}
function populateTopArtists(artists) {
const topArtists = document.getElementById("topArtists");
artists.items.forEach(artist => {
const cardElement = document.createElement("div");
cardElement.className = "card";
cardElement.style.width = "18rem";
const imgElement = document.createElement("img");
imgElement.className = "card-img-top";
imgElement.src = artist.images[0].url; // Assuming the artist object has images
imgElement.alt = artist.name;
const cardBody = document.createElement("div");
cardBody.className = "card-body";
const cardTitle = document.createElement("h5");
cardTitle.className = "card-title";
cardTitle.innerText = artist.name;
const cardText = document.createElement("p");
cardText.className = "card-text";
cardText.innerText = `Followers: ${artist.followers.total} | Popularity: ${artist.popularity}`;
const genreList = document.createElement("ul");
genreList.className = "list-group list-group-flush";
artist.genres.forEach(genre => {
const genreItem = document.createElement("li");
genreItem.className = "list-group-item";
genreItem.innerText = genre;
genreList.appendChild(genreItem);
});
const cardLink = document.createElement("a");
cardLink.className = "btn btn-primary";
cardLink.href = artist.external_urls.spotify;
cardLink.innerText = "Go to artist";
cardBody.appendChild(cardTitle);
cardBody.appendChild(cardText);
cardBody.appendChild(cardLink);
cardElement.appendChild(imgElement);
cardElement.appendChild(cardBody);
cardElement.appendChild(genreList);
topArtists.appendChild(cardElement);
});
Por último, espero que os haya servido este post y como menciono al final del vídeo cualquier duda podéis contactarme, así como usar o ver el código que he ido acumulando desde que empecé a jugar con la API de Spotify.
Muchas gracias =)
Autor/a: Maki Spariva Mirón Olona
Máster: Desarrollo Web Full Stack + MultiCloud
Centro: Tajamar Tech
Año académico: 2024-2025
GitHub: https://github.com/spariva/
Linkedin: https://www.linkedin.com/in/spariva/
Recursos código: https://github.com/spariva/Spoti-API/











