Snakeoil Audio
In fase di rifinitura

- Next.js
- Tailwind
- Next-Auth
- bcrypt
- Upstash Redis
- @upstash/ratelimit
- MongoDB
- Mongoose
- MDX
- React Hook Form
- Zod
- Zustand
- Tipo
- Accademico
- Categoria
- Applicazione Web
- Data
- 2025-12
DEMO E CODICE SORGENTE DISPONIBILI A BREVE
Dopo qualche mese lontano dal codice avevo iniziato a costruire un piccolo store usando React ed Express, come esercizio pratico.
Poi mi sono imbattuto in Next.js: era un framework nuovo per me, ma con la familiarità di React, quindi ho deciso di rifare tutto da zero. Volevo capire come si pensa un’app fullstack quando frontend, API e database vivono nello stesso progetto — e, sì, anche usare l'altro mio linguaggio preferito dopo Typescript: il sarcasmo.
La UI è volutamente essenziale: il focus del progetto è la logica applicativa.
1. Perché rifarlo con Next.js
Lavorare con Next.js mi ha costretto a ripensare il flusso che conoscevo da React + Express. Frontend e backend non sono più due entità separate: qui routing, rendering e API coesistono nella stessa repository e questo ha reso, per me, il processo di costruzione più rapido e intuitivo.
Non cercavo la soluzione più elegante del mondo: volevo un progetto che funzionasse end-to-end, con autenticazione, gestione del carrello e persino una logica di licenze che impedisse acquisti duplicati. Tutto il resto — grafica, CMS, pagamenti reali — era deliberatamente fuori scope.
2. Architettura e scelta sul carrello
L’architettura è essenziale:
- Client: pagine prodotto dinamiche con MDX, gestione dello stato del carrello (Zustand), interazioni UI e persistenza locale.
- Server (API Routes): validazione dei dati e controllo sugli acquisti duplicati.
- Database (MongoDB): persistenza di utenti e ordini.
- Proxy: rate limiting con Upstash Redis.
Sul carrello ho preso una decisione pragmatica: è persistente solo in localStorage tramite middleware persist. Ogni prodotto viene aggiunto solo se non già presente, evitando duplicati lato client.
Ho valutato la sincronizzazione server-side, ma il sito vende solamente quattro prodotti: sincronizzare il carrello mi sarebbe sembrato overengineering. Per evitare vendite duplicate, però, quando un utente effettua il login il server verifica lo storico ordini e rimuove dal carrello locale eventuali prodotti già posseduti. Non è una sincronizzazione completa, ma è sufficiente per il perimetro del progetto e mantiene l’architettura leggera.
// src/lib/useCart.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { CartState } from "../types/cart-types";
export const useCart = create<CartState>()(
persist<CartState>(
(set, get) => ({
items: [],
addItem: (item) => {
const existing = get().items.find((i) => i.id === item.id);
if (!existing) {
set({ items: [...get().items, item] });
}
},
removeItem: (id) => {
set({ items: get().items.filter((i) => i.id !== id) });
},
reset: () => set({ items: [] }),
}),
{ name: "cart-storage" }
)
);
3. Checkout, validazione e regole di business
La parte che mi ha dato più soddisfazione è stata impostare la regola “one-time purchase” per le licenze. Per ogni prodotto viene verificata la presenza nello storico ordini dell'utente. Se un prodotto è già posseduto dall'utente, quest'ultimo non potrà aggiungerlo nuovamente al carrello. Nel caso di un utente guest, al momento del login (necessario per completare l’acquisto) i prodotti già posseduti verranno automaticamente rimossi dal carrello. Avrei potuto implementare anche un toast di notifica ma ho deciso che sarebbe stata una feature più ui-oriented e quindi fuori scope per questo progetto.
// verifica lato server se l’utente possiede già il prodotto
const ownsProduct = !!(await Order.exists({ user: session.user.id, status: "paid", "items.item": productData.id }));
Per la validazione dei form di registrazione e di login ho usato Zod come fonte unica di verità: gli stessi schemi vengono impiegati nei form client-side e nelle API. Questo ha semplificato molto il debugging e mi ha evitato sorprese dovute a payload malformati.
4. Sicurezza e limiti
Anche in un progetto dimostrativo ho voluto occuparmi di qualche aspetto pratico di sicurezza: rate limiting con Upstash Redis, hashing delle password con bcrypt, e NextAuth per l’autenticazione. Quando qualcuno supera la soglia di richieste, mostro una pagina dedicata invece di rincorrere l’errore tecnico: è una piccola attenzione UX che rende l’app più presentabile (rinforzando la politica aziendale notoriamente poco incline alla generosità).

5. Cosa ho imparato
Da questo progetto ho imparato che la documentazione spesso lascia tanti dubbi e che le scelte semplici sono utili se giustificate. Costruire un flusso end-to-end ti mette davanti a problemi reali (e frustranti) che non emergono in tutorial filtrati. SnakeOil Audio non sarà perfetto ma mi ha divertito, mi ha rimesso in moto e mi ha dato una visione più concreta di cosa significhi lavorare su un’app fullstack con Next.js.