// 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.
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.
Link
- Live: echoes.giovannimanara.dev
- Codice: github.com/GioManara96/echoes