// caso-studio

Echoes

Un autoritratto musicale live sulla Spotify Web API — now playing, top artist e ascolti recenti — con Next.js App Router e TypeScript, senza login per i visitatori.

Next.jsTypeScriptTailwind v4GSAPSpotify API
Echoes

Cos'è

Echoes trasforma un account Spotify personale in una dashboard pubblica: cosa sto ascoltando ora, i top artist su finestre temporali diverse e lo storico recente. Tutto renderizzato server-side, aggiornato in tempo reale, senza login per chi visita.

È anche il progetto con cui ho consolidato React, Next.js (App Router) e TypeScript in strict mode, venendo dal mondo Vue/Nuxt.

Il problema

Volevo uno spazio live che raccontasse i miei gusti musicali senza esporre OAuth ai visitatori e senza un backend complesso. Spotify non concede accesso multi-utente a scala "portfolio": serve un modello single-account, server-side, con token che non raggiungono mai il browser.

Come funziona

  • Autenticazione una tantum con refresh token long-lived legato a un solo account Spotify
  • Exchange on-demand in access token short-lived, cache in memoria con margine di sicurezza di 60s prima della scadenza
  • Ogni chiamata a Spotify passa da route interne dell'app: il browser parla solo con Echoes
  • Dati "lenti" (top artist, storico) fetchati in SSR e cachati per minuti/ore, chiave per combinazione (timeRange, limit) — N visitatori costano al massimo una richiesta upstream per intervallo
  • Now playing: unico pezzo client-side, poll leggero ogni 8 secondi verso una route interna
  • Time-range e limiti vivono nella query string (?range=…&limit=…): ogni vista è condivisibile e renderizzata dal server

La cache è success-only con stale-if-error: un errore non viene mai scritto in cache, e se un refetch fallisce servo l'ultimo valore buono invece di una pagina rotta.

try {
  const accessToken = await getAccessToken();
  const response = await fetch(topArtistsUrl, {headers: {Authorization: `Bearer ${accessToken}`}});
  const artistsResponse: TopArtistsResponse = await response.json();
  cachedTopArtists.set(cacheKey, {value: artistsResponse.items, fetchedAt: Date.now()});
  return artistsResponse.items;
} catch (error) {
  // Stale-if-error: yesterday's top artists beat an error page.
  if (cached) return cached.value;
  throw error;
}

Dettagli di cui vado fiero

Il polling rispetta il tab. Quando la pagina è in background le richieste si fermano; al ritorno rifetcha subito e riparte. Niente traffico sprecato, niente dato vecchio al rientro.

const onVisibilityChange = () => {
  if (document.hidden) {
    stop();
  } else {
    fetchNowPlaying();
    start();
  }
};
document.addEventListener("visibilitychange", onVisibilityChange);

Niente flash di loading. Il primo payload del now playing arriva da un Server Component asincrono e viene passato al client come initialData dentro un Suspense: lo skeleton esiste solo per lo streaming iniziale, mai come stato client.

Tipi stretti ai confini. I payload sono discriminated union (idle | active | error, traccia o episodio podcast via kind), e le shape grezze di Spotify restano private nel modulo server: fuori escono solo tipi di dominio con i campi effettivamente usati.

Il podio dei top tracks — slot renderizzati 02 | 01 | 03 con riser animati da ScrollTrigger:

Il processo

Prima del codice ho scritto una UI spec: direzione "on-air listening booth" — una cabina d'ascolto notturna, non l'ennesima dashboard Spotify — con palette carbone-blu e un solo arancione caldo riservato al LED "on air", ai focus e al tab attivo. Niente verde Spotify, niente estetiche di default.

Il 90% del codice l'ho scritta a mano: l'AI ha fatto solo da reviewer, mai da autore. Era il vincolo che mi ero dato per imparare davvero React e Next.js, non per farmeli generare. Gli unici punti in cui ho ceduto alla generazione tramite AI sono stati quelli in cui il concetto era già stato assimilato e volevo procedere in velocità.

Tutte le animazioni GSAP sono dietro gsap.matchMedia("(prefers-reduced-motion: no-preference)"), con lo stato CSS a riposo uguale allo stato finale: chi riduce il motion vede la pagina già completa.

Cosa ho imparato

  • Il passaggio mentale da Vue a React: reattività esplicita con gli hook al posto di ref e computed, dipendenze dichiarate invece che tracciate
  • Server Components e Suspense come modello di rendering, non solo ottimizzazione
  • TypeScript strict come strumento di design: le discriminated union costringono a decidere i casi limite prima, non in produzione

Limiti consapevoli

Spotify ha chiuso Recommendations, Audio Features, Audio Analysis e Related Artists alle nuove app (fine 2024): Echoes non dipende da quegli endpoint. Non c'è ancora un database — è uno snapshot live. In roadmap: job schedulato che salva snapshot dei top ascolti per tracciare come cambia il gusto nel tempo.

_casi-studio
Apri la palette per cercare file e navigare