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 constripe.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_SECRETsegreto; non usa body parser JSON su quella rotta (devi usareexpress.rawcome 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