sabato 11 ottobre 2025

Corso di JavaScript & Programmazione Web: 10 – Progetto completo di un sito di e-commerce

 10 – Progetto completo di un sito di e-commerce

1) Visione generale e requisito minima

Obiettivo: sito commerciale per commerciante locale con catalogo, search, carrello, checkout, gestione ordini, pannello amministrativo per inventario e analisi vendite. Interazione ricca in client con JavaScript (React) e backend REST/GraphQL in Node.js. Pagamenti con Stripe Checkout (semplice e sicuro). Database: MongoDB (flessibile, JSON-like), caching Redis opzionale.

Principali caratteristiche:

  • Catalogo prodotti (categoria, tagging, filtri)

  • Carrello persistente (localStorage + server-side per utenti loggati)

  • Checkout con Stripe (Checkout Session + webhook per conferma ordine)

  • Account cliente (registro, login JWT, storico ordini)

  • Area admin (login, CRUD prodotti, gestione ordini, report)

  • SEO friendly (SSR / pre-rendering per pagine prodotto) — usare SSG/SSR su Vercel o prerender per SEO

  • PWA (manifest + service worker) per offline caching e push notifications opzionale

  • Accessibilità (a11y), performance (images optim, lazy loading), security (rate-limit, helmet, CORS, CSP)

  • Internazionalizzazione (i18n) e multi-currency opzionali


2) Stack tecnico consigliato

  • Frontend: React (Vite), React Router, Context/Redux (Context + useReducer sufficiente), Fetch/Axios, Stripe JS (@stripe/stripe-js), Tailwind CSS o CSS modulare

  • Backend: Node.js + Express, TypeScript consigliato ma qui fornisco JS per leggibilità

  • DB: MongoDB Atlas (o locale), Redis opzionale per sessioni/caching

  • Pagamenti: Stripe Checkout + Webhook per conferma

  • Autenticazione: JWT + refresh tokens (o Auth0 se vuoi esternalizzare)

  • Hosting: Frontend su Vercel o Netlify; backend su Render/Heroku/AWS Elastic Beanstalk o container Docker su cloud

  • CI/CD: GitHub Actions per build/test/deploy

  • Monitoraggio: Sentry (error tracking), Prometheus/Grafana o NewRelic

  • Search: ElasticSearch se grande catalogo, altrimenti filtri MongoDB + indexing


3) Struttura progetto (sintesi)

merchant-site/
├─ backend/
│  ├─ src/
│  │  ├─ index.js           # Express app entry
│  │  ├─ routes/
│  │  │  ├─ products.js
│  │  │  ├─ auth.js
│  │  │  ├─ orders.js
│  │  │  └─ stripeWebhook.js
│  │  ├─ controllers/
│  │  ├─ models/
│  │  │  ├─ Product.js
│  │  │  ├─ User.js
│  │  │  └─ Order.js
│  │  └─ utils/
│  ├─ .env
│  └─ Dockerfile
├─ frontend/
│  ├─ src/
│  │  ├─ main.jsx
│  │  ├─ App.jsx
│  │  ├─ pages/
│  │  │  ├─ Home.jsx
│  │  │  ├─ Product.jsx
│  │  │  ├─ Cart.jsx
│  │  │  ├─ Checkout.jsx
│  │  │  └─ Admin/
│  │  ├─ components/
│  │  └─ hooks/
│  ├─ index.html
│  └─ vite.config.js
├─ docker-compose.yml
└─ README.md

4) Codice di riferimento — Backend essenziale (Express + Stripe webhook)

Nota: sostituisci le chiavi process.env.* e gli URL con i tuoi valori. Testa con Stripe test keys. Verifica webhook signing secret con stripe.webhooks.constructEvent.

backend/src/index.js

// index.js (Express minimal)
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const mongoose = require('mongoose');
require('dotenv').config();

const productsRouter = require('./routes/products');
const ordersRouter = require('./routes/orders');
const stripeWebhook = require('./routes/stripeWebhook');

const app = express();

// security
app.use(helmet());
app.use(cors({ origin: process.env.FRONTEND_URL }));
app.use(express.json({ limit: '10kb' }));

// rate limit
const limiter = rateLimit({ windowMs: 60*1000, max: 100 });
app.use(limiter);

// connect DB
mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser:true, useUnifiedTopology:true })
  .then(()=> console.log('MongoDB connected'))
  .catch(err => { console.error(err); process.exit(1); });

// routes
app.use('/api/products', productsRouter);
app.use('/api/orders', ordersRouter);

// stripe webhook: must use raw body, so mount separately
app.post('/webhook', express.raw({ type: 'application/json' }), stripeWebhook);

// global error
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'internal error' });
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, ()=> console.log(`API listening ${PORT}`));

backend/src/routes/products.js

const express = require('express');
const Product = require('../models/Product');
const router = express.Router();

// GET /api/products?search=&category=&page=
router.get('/', async (req, res, next) => {
  try {
    const { search, category, page = 1, limit = 20 } = req.query;
    const q = {};
    if (search) q.$text = { $search: search }; // requires text index
    if (category) q.category = category;
    const products = await Product.find(q)
      .skip((page-1)*limit).limit(Number(limit)).lean();
    res.json(products);
  } catch (e) { next(e); }
});

// GET single
router.get('/:id', async (req, res, next) => {
  try {
    const p = await Product.findById(req.params.id).lean();
    if (!p) return res.status(404).json({error:'not found'});
    res.json(p);
  } catch (e) { next(e); }
});

module.exports = router;

backend/src/routes/orders.js (estratto creazione ordine + Stripe Checkout session)

const express = require('express');
const router = express.Router();
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET);
const Order = require('../models/Order');
const Product = require('../models/Product');

// Create Checkout Session
router.post('/create-checkout-session', async (req, res, next) => {
  try {
    const { items, customerEmail, successUrl, cancelUrl } = req.body;
    // items = [{ productId, qty }]
    const line_items = [];
    for (const it of items) {
      const p = await Product.findById(it.productId).lean();
      if (!p) return res.status(400).json({ error: 'invalid product' });
      line_items.push({
        price_data: {
          currency: 'eur',
          product_data: { name: p.title, images: [p.imageUrl] },
          unit_amount: Math.round(p.price*100),
        },
        quantity: it.qty,
      });
    }

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      mode: 'payment',
      line_items,
      customer_email: customerEmail,
      success_url: successUrl,
      cancel_url: cancelUrl,
      metadata: { merchantOrderRef: 'tempRef' }
    });

    // optionally create pending order in DB
    const order = await Order.create({
      status: 'pending',
      stripeSessionId: session.id,
      items,
      total: line_items.reduce((s,l)=> s + (l.price_data.unit_amount/100)*l.quantity, 0)
    });

    res.json({ id: session.id, url: session.url, orderId: order._id });
  } catch (e) { next(e); }
});

module.exports = router;

backend/src/routes/stripeWebhook.js

// stripe webhook handler
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET);
const Order = require('../models/Order');

module.exports = async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    console.error('Webhook signature verification failed.', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    // fulfill order
    const order = await Order.findOne({ stripeSessionId: session.id });
    if (order) {
      order.status = 'paid';
      order.paidAt = new Date();
      order.paymentIntent = session.payment_intent;
      await order.save();
      // send confirmation email (enqueue job)
    }
  }
  res.json({received: true});
};

Sicurezza webhook: mantieni il STRIPE_WEBHOOK_SECRET segreto; non usa body parser JSON su quella rotta (devi usare express.raw come sopra).


5) Frontend (React + Vite) — esempi chiave

frontend/src/main.jsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles.css';

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

frontend/src/App.jsx (router, CartContext)

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Product from './pages/Product';
import Cart from './pages/Cart';
import Checkout from './pages/Checkout';
import { CartProvider } from './contexts/CartContext';

export default function App(){
  return (
    <CartProvider>
      <Routes>
        <Route path="/" element={<Home/>}/>
        <Route path="/product/:id" element={<Product/>}/>
        <Route path="/cart" element={<Cart/>}/>
        <Route path="/checkout" element={<Checkout/>}/>
      </Routes>
    </CartProvider>
  );
}

frontend/src/contexts/CartContext.jsx

import React, { createContext, useReducer, useContext, useEffect } from 'react';

const CartStateContext = createContext();
const CartDispatchContext = createContext();

function reducer(state, action){
  switch(action.type){
    case 'INIT': return action.payload;
    case 'ADD': {
      const exists = state.find(i => i.productId === action.payload.productId);
      if (exists) return state.map(i => i.productId === action.payload.productId ? { ...i, qty: i.qty + action.payload.qty } : i);
      return [...state, action.payload];
    }
    case 'REMOVE':
      return state.filter(i => i.productId !== action.payload.productId);
    case 'UPDATE':
      return state.map(i => i.productId === action.payload.productId ? { ...i, qty: action.payload.qty } : i);
    case 'CLEAR':
      return [];
    default: throw new Error('Unknown action');
  }
}

export function CartProvider({children}){
  const [state, dispatch] = useReducer(reducer, [], () => {
    try { return JSON.parse(localStorage.getItem('cart')||'[]'); } catch(e){ return []; }
  });

  useEffect(()=> { localStorage.setItem('cart', JSON.stringify(state)); }, [state]);

  return (
    <CartStateContext.Provider value={state}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartStateContext.Provider>
  );
}

export const useCart = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

frontend/src/pages/Checkout.jsx (crea sessione Stripe)

import React from 'react';
import { useCart } from '../contexts/CartContext';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE);

export default function Checkout(){
  const cart = useCart();

  async function handleCheckout(){
    const res = await fetch(`${import.meta.env.VITE_API_URL}/api/orders/create-checkout-session`, {
      method:'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({
        items: cart,
        customerEmail: '', // if logged in use user email
        successUrl: `${window.location.origin}/success`,
        cancelUrl: `${window.location.origin}/cart`
      })
    });
    const data = await res.json();
    if (data.url) {
      // redirect to stripe
      window.location.href = data.url;
    } else {
      alert('Errore checkout');
    }
  }

  return (
    <div>
      <h1>Checkout</h1>
      <button onClick={handleCheckout}>Paga con carta (Stripe)</button>
    </div>
  );
}

Questo flusso usa Stripe Checkout (redirect). Più complesso: integrazione con Payment Intents e elements per pagamento embedded.


6) Modelli Mongoose (esempi)

backend/src/models/Product.js

const mongoose = require('mongoose');
const schema = new mongoose.Schema({
  title: { type: String, required: true, text: true },
  description: String,
  price: { type: Number, required: true },
  imageUrl: String,
  sku: String,
  stock: { type: Number, default: 0 },
  category: String,
  tags: [String],
  createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Product', schema);

backend/src/models/Order.js

const mongoose = require('mongoose');
const schema = new mongoose.Schema({
  items: [{ productId: String, qty: Number, price: Number }],
  total: Number,
  customerEmail: String,
  status: { type: String, enum: ['pending','paid','shipped','cancelled'], default: 'pending' },
  stripeSessionId: String,
  paymentIntent: String,
  createdAt: { type: Date, default: Date.now },
  paidAt: Date
});
module.exports = mongoose.model('Order', schema);

7) Docker & deploy (sintesi)

Dockerfile (backend)

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_ENV=production
EXPOSE 4000
CMD ["node", "src/index.js"]

docker-compose.yml (sviluppo locale)

version: '3.8'
services:
  db:
    image: mongo:6
    restart: unless-stopped
    volumes: - dbdata:/data/db
    ports: - "27017:27017"
  backend:
    build: ./backend
    environment:
      - MONGODB_URI=mongodb://db:27017/merchant
      - STRIPE_SECRET=${STRIPE_SECRET}
      - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
    ports:
      - "4000:4000"
    depends_on: [db]
volumes:
  dbdata:

Deploy:

  • Frontend: Vercel/Netlify (ottimo per React + prerender SEO)

  • Backend: Render, Heroku, Railway, o container su AWS ECS / Google Cloud Run

  • Use TLS (Let's Encrypt), CDN per immagini (Cloudflare, DigitalOcean Spaces, AWS S3 + CloudFront)


8) Sicurezza & compliance (obbligatorio)

  • Non memorizzare dati sensibili della carta (usa Stripe).

  • HTTPS obbligatorio.

  • Proteggi webhook (firmatura).

  • Helmet (security headers), CSP (content-security-policy) attenuato per script esterni (Stripe).

  • Rate limiting, input validation (Joi o express-validator), logging.

  • GDPR: policy cookie, consenso, gestione richieste utenti (data export / delete).

  • Password hashing: bcrypt + salting; usa 2FA per admin se possibile.


9) SEO, accessibilità, performance

  • Pagine prodotto prerenderate / SSR (Next.js) o generazione statica (SSG) per SEO.

  • Schema.org JSON-LD per prodotti (price, availability, sku).

  • Metadati OpenGraph per condivisione social.

  • WL: immagini ottimizzate (WebP), lazy loading loading="lazy".

  • Lighthouse: mira a 90+ mobile/desktop.

  • A11y: aria labels, semantic HTML, contrast ratios.


10) Testing, monitoraggio, analytics

  • Unit tests backend (Jest + supertest), frontend (Vitest / React Testing Library).

  • E2E: Playwright / Cypress per flusso checkout.

  • Monitor: Sentry per error tracking; Google Analytics / Plausible per traffico.

  • Backup DB giornaliero, disaster recovery doc.


11) Funzionalità avanzate JavaScript che puoi sfruttare

  • Realtime inventory con WebSocket / Socket.io (aggiorna stock in tempo reale)

  • Client-side search avanzata con Fuse.js per fuzzy search istantanea

  • Progressive Web App: service worker, offline caching del catalogo + carrello

  • Client-side rendering + streaming: caricamento progressivo immagini/skeletons per UX

  • Dynamic imports e code splitting con React.lazy per performance

  • Headless CMS (Strapi/Sanity) come backend per contenuti non tecnici

  • Personalizzazione JS: recommendations (collaborative filtering) eseguite in Node o client-side con cached models

  • Machine Learning inference: raccomandazioni prodotto con endpoint Python/TF o semplice scoring JS


Nessun commento:

Posta un commento

Corso Fondamenti di Informatica e Reti: 6 Reti di computer e Internet

Reti di computer e Internet Introduzione Prova a pensare alla vita quotidiana senza reti informatiche: niente messaggi WhatsApp, niente m...