Skip to content

About

Full-stack MERN e-commerce app with a React storefront and admin dashboard, Express/MongoDB API, Stripe payments, Cloudinary uploads, and JWT auth.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LetShopping

A full-stack e-commerce application with a React storefront, an admin dashboard, and a Node.js/Express API backed by MongoDB. Payments are handled by Stripe, product images by Cloudinary, and transactional email by Gmail/nodemailer.

This repository contains two apps:

Folder Description Stack
backend/ REST API, authentication, orders, payments, admin operations Node.js, Express, Mongoose, JWT, Stripe, Cloudinary, Joi
frontend/ Customer storefront and admin dashboard React 18, Vite, Redux Toolkit, React Router, Tailwind CSS, Stripe.js

The two apps are independent packages that run side by side.

Live app: https://letshoppin.netlify.app/

Background

Originally built in 2024 as a learning project, then refactored with security hardening, validation, and automated tests.

Features

  • Email/password registration with email verification
  • JWT authentication and role-based authorization (customer / admin)
  • Product, category, banner, review, cart, and order management
  • Stock validation and inventory adjustments on order place / cancel / activate
  • Stripe PaymentIntent flow with server-side payment verification
  • Admin dashboard for customers, products, categories, banners, orders, and reviews
  • Optional read-only demo admin and demo customer logins with automatic expiry
  • Cloudinary image hosting
  • Contact form and password reset emails
  • Starter categories, products, and banners seeded on first run
  • Input validation (Joi), rate limiting, Helmet, and CORS allow-listing
  • Automated API tests using the Node.js built-in test runner

Project structure

letshopping/
├── backend/
│   ├── app.js                  # Express app (middleware + routes, no listening)
│   ├── server.js               # Connects to MongoDB and starts the HTTP server
│   ├── config/                 # DB connection, seeding, demo account cleanup
│   ├── controllers/            # Request handlers
│   ├── middleware/             # Auth, admin, validation, rate limiting, errors
│   ├── models/                 # Mongoose schemas
│   ├── requests/               # Joi validation schemas
│   ├── routes/                 # Express routers
│   ├── scripts/                # Standalone seed scripts
│   ├── services/               # Cloudinary / multer configuration
│   ├── utils/                  # Email helper
│   └── tests/                  # node:test API tests
└── frontend/
    ├── src/
    │   ├── App-Routing/        # Route definitions and guards
    │   ├── Home/               # Home page
    │   ├── assets/             # Images and brand assets
    │   ├── components/         # Pages and UI components (customer + admin)
    │   ├── redux/              # Redux Toolkit slices
    │   ├── service/            # Client-side services
    │   └── styles/             # Global styles
    ├── vite.config.js
    └── vercel.json

Prerequisites

  • Node.js 20+ and npm
  • MongoDB 7+ running locally, or a MongoDB Atlas cluster
  • Stripe account (test keys are fine)
  • Cloudinary account
  • Gmail account with an App Password for sending email

Getting started

1. Backend

cd backend
npm install
cp .env.example .env
# edit .env with your values
npm run dev

The API starts on http://localhost:4000 by default.

Health check:

curl http://localhost:4000/api/status

2. Frontend

In a second terminal:

cd frontend
npm install
cp .env.example .env
# set VITE_APP_API_URL and VITE_STRIPE_PUBLISHABLE_KEY
npm run dev

The storefront runs on http://localhost:5173.

Make sure VITE_APP_API_URL uses http:// and matches the backend origin/port. Using https://localhost:... against a plain HTTP server causes browser Failed to fetch errors.

3. MongoDB

Local MongoDB (default):

LOCAL_MONGODB_URL=mongodb://127.0.0.1:27017/letshop

MongoDB Atlas:

CLOUD_MONGODB_URL=mongodb+srv://<user>:<password>@<cluster>/<database>

If you use Atlas, add your current public IP under Atlas → Network Access. An un-whitelisted IP fails during the TLS handshake with SSL alert number 80.

In development/test the backend connects to LOCAL_MONGODB_URL first and falls back to CLOUD_MONGODB_URL. In production (NODE_ENV=production) it prefers CLOUD_MONGODB_URL and falls back to LOCAL_MONGODB_URL. The common Atlas name MONGODB_URI is accepted as an alias for CLOUD_MONGODB_URL.

Environment variables

Backend (backend/.env)

Variable Required Description
NODE_ENV No development, production, or test
PORT No API port (default 5000, project default 4000)
LOCAL_MONGODB_URL One of Local MongoDB connection string
CLOUD_MONGODB_URL One of MongoDB Atlas connection string (MONGODB_URI also accepted)
JWT_SECRET Yes Secret used to sign JWTs. Use a long random value
BASE_URL No Backend base URL (fallback for email links)
FRONTEND_URL Yes Frontend URL used in verification and reset links
ALLOWED_ORIGINS Yes in production Comma-separated list of allowed browser origins
ADMIN_NAME / ADMIN_EMAIL / ADMIN_PWD No Admin account auto-created on startup if it does not exist
DEMO_ADMIN_NAME / DEMO_ADMIN_EMAIL / DEMO_ADMIN_PWD No Read-only demo admin login. Created on first demo login
DEMO_CUSTOMER_NAME / DEMO_CUSTOMER_EMAIL / DEMO_CUSTOMER_PWD No Demo customer login. Created on first demo login
DEMO_TTL_MINUTES No Minutes before a demo account and its cart, orders, and reviews are deleted (default 60)
SEED_CATEGORIES No Comma-separated category names seeded when the categories collection is empty (defaults to a built-in starter set)
EMAIL_USER Yes Gmail address used as the sender
EMAIL_PASS Yes Gmail App Password
CONTACT_EMAIL No Recipient for contact form messages (defaults to EMAIL_USER)
CLOUDINARY_NAME / CLOUDINARY_API_KEY / CLOUDINARY_API_SECRET Yes for uploads Cloudinary credentials
STRIPE_SECRET_KEY Yes for payments Stripe secret key

Frontend (frontend/.env)

Variable Required Description
VITE_APP_API_URL Yes Backend base URL, e.g. http://localhost:4000
VITE_STRIPE_PUBLISHABLE_KEY Yes Stripe publishable key
VITE_CLOUDINARY_CLOUD_NAME Yes for uploads Cloudinary cloud name
VITE_CLOUDINARY_UPLOAD_PRESET Yes for uploads Unsigned upload preset

Never commit real .env files. Use .env.example as the template.

Deployment

The recommended free-tier setup is Render (backend), Netlify (frontend), and MongoDB Atlas (database).

1. MongoDB Atlas

  1. Create a free M0 cluster and a database user.
  2. Under Network Access, add 0.0.0.0/0. Render's free tier uses dynamic egress IPs, so a fixed IP allow-list will block connections.
  3. Copy the driver connection string into CLOUD_MONGODB_URL (include the database name, e.g. /letshop).

2. Backend on Render

The repository includes render.yaml, so you can use New → Blueprint and point Render at this repo. It configures the backend/ root directory, npm ci, npm start, and the /api/status health check. Otherwise create a Web Service manually with the same settings.

Set these environment variables in the Render dashboard (the blueprint marks them as secrets):

Variable Example
NODE_ENV production
CLOUD_MONGODB_URL mongodb+srv://.../letshop
JWT_SECRET a long random string (do not reuse a dev value)
BASE_URL https://letshopping-api.onrender.com
FRONTEND_URL https://letshoppin.netlify.app
ALLOWED_ORIGINS https://letshoppin.netlify.app (comma-separate extra origins)
EMAIL_USER / EMAIL_PASS Gmail address + App Password
CONTACT_EMAIL contact recipient
CLOUDINARY_NAME / CLOUDINARY_API_KEY / CLOUDINARY_API_SECRET Cloudinary credentials
STRIPE_SECRET_KEY sk_live_... for real payments
ADMIN_NAME / ADMIN_EMAIL / ADMIN_PWD admin account created automatically on startup
DEMO_ADMIN_NAME / DEMO_ADMIN_EMAIL / DEMO_ADMIN_PWD optional read-only demo admin (login page shows a button when set)
DEMO_CUSTOMER_NAME / DEMO_CUSTOMER_EMAIL / DEMO_CUSTOMER_PWD optional demo customer (login page shows a button when set)
DEMO_TTL_MINUTES optional demo lifetime in minutes (default 60)
SEED_CATEGORIES optional comma-separated category names (only used when none exist)

Do not set PORT; Render injects it. render.yaml sets NODE_VERSION=22, and package.json requires Node >=20 <23.

The admin account is seeded on every backend start. If ADMIN_EMAIL already exists as an admin, nothing happens; if it exists as a customer, seeding is skipped with a warning (change the email to create the admin).

Product categories are seeded once, only when the categories collection is empty. Set SEED_CATEGORIES to override the built-in starter set; seeded images use placeholder URLs that you can replace from the admin dashboard.

Products are seeded once, only when the products collection is empty. The starter catalog fills up to 12 products across the available categories and gives each one an active discount.

Banners are seeded once, only when the banners collection is empty. Three starter banners are created with blank images so an admin can upload artwork later. You can also run npm run seed:banners from backend/.

Demo accounts are optional and created on first login. Each demo login starts a fresh 1-hour session (configurable with DEMO_TTL_MINUTES), and the demo admin can view data but cannot create, edit, or delete anything. A cleanup job runs every minute while the backend is awake and once on startup; it restores stock and permanently deletes expired demo users along with their carts, orders, and reviews. On hosts that sleep idle services (such as Render's free tier), cleanup resumes on the next wake.

3. Frontend on Netlify

netlify.toml builds frontend/ with Vite and rewrites all routes to index.html for client-side routing. The live deployment is https://letshoppin.netlify.app/. Set these environment variables in Site configuration → Environment variables (Vite inlines them at build time, so redeploy after changing them):

Variable Example
VITE_APP_API_URL https://letshopping-api.onrender.com
VITE_STRIPE_PUBLISHABLE_KEY pk_live_...
VITE_CLOUDINARY_CLOUD_NAME your-cloud-name
VITE_CLOUDINARY_UPLOAD_PRESET your-unsigned-preset

Production notes

  • Render's free web service sleeps after inactivity; the first request can take ~50s to wake.
  • Restrict the Cloudinary unsigned upload preset (allowed formats, max size, folder) because it is public by design.
  • Rotate any credentials that have been used in development before going live.
  • Run npm run build in frontend/ before deploying to catch build errors.

Available scripts

Backend

Command Description
npm run dev Start the API with file watching
npm start Start the API
npm test Run the API test suite
npm run test:watch Re-run tests on change
npm run seed:banners Seed the starter banners when the collection is empty
npm run build No-op placeholder (the backend needs no build step)

Frontend

Command Description
npm run dev Start the Vite dev server
npm run build Build for production
npm run preview Preview the production build
npm run lint Run ESLint

Testing

The backend uses the built-in Node.js test runner (node:test) and fetch, so no extra test dependencies are required.

cd backend
npm test

The tests:

  • run against a local MongoDB instance (127.0.0.1:27017)
  • create and drop isolated letshop_test_* databases
  • skip real email sending (NODE_ENV=test)
  • cover registration/login, password reset/change, JWT protection, cart stock rules, order creation, stock restoration, ownership checks, admin-only routes, payment-intent authorization, validation, 404s, and CORS

If MongoDB is not running on the default port, start it before running the tests.

API overview

All endpoints are prefixed with /api.

Auth and users (/api/users)

Method Path Access Description
POST /register Public Register a customer
POST /login Public Log in and receive a JWT
GET /demo-accounts Public Demo credentials shown on the login page (when configured)
GET /validation-schema Public Registration field rules used by the frontend
GET /verify-email/:token Public Verify an email address
POST /request-password-reset Public Request a reset link
POST /reset-password/:token Public Reset the password
GET /me Auth Current user
GET /profile Auth Current user profile
PUT /update-profile Auth Update own profile
PUT /change-password Auth Change own password
DELETE /delete-account Auth Delete own account
GET /admin/customers Admin List customers
PUT /admin/customers/:id/activate Admin Activate a customer
PUT /admin/customers/:id/deactivate Admin Deactivate a customer
PUT /admin/customers/:id Admin Update a customer
DELETE /admin/customers/:id Admin Delete a customer

Products (/api)

Method Path Access Description
GET /products Public List products (supports categoryName, search)
GET /products/:id Public Product details + reviews
POST /search Public Search products by keyword
GET /category/:category Public Products in a category
POST /products Admin Create a product
PUT /products/:id Admin Update a product
DELETE /products/:id Admin Delete a product
GET /products/low-stock Admin Low-stock products
PUT /products/:id/increase-stock Admin Increase stock
PUT /products/:id/decrease-stock Admin Decrease stock

Cart (/api)

Method Path Access Description
POST /cart Auth Add an item (validates stock)
GET /cart Auth Get the current cart
DELETE /cart/:itemId Auth Remove an item

Orders (/api/orders)

Method Path Access Description
POST /place-order Auth Create an order from the cart
GET /myorders Auth Current user's orders
GET /myorders/status Auth Current user's orders by status
PUT /:id/cancel Owner/Admin Cancel and restore stock
PUT /:id/activate Owner/Admin Reactivate a canceled order
DELETE /:id Owner/Admin Delete a canceled order
POST /create-payment-intent Owner/Admin Create a Stripe PaymentIntent
POST /confirm-payment Owner/Admin Verify payment with Stripe and mark paid
GET / Admin All orders
GET /status Admin All orders by status
PUT /:id/status Admin Update order status
PUT /update-payment-status/:orderId Admin Manually set payment status

Other

Method Path Access Description
GET /api/banners Public List banners
GET /api/banners/:id Public Get a banner
POST/PUT/DELETE /api/banners/... Admin Manage banners
GET /api/categories Public List categories
GET /api/categories/:id Admin Get category details
POST/PUT/DELETE /api/categories/... Admin Manage categories
GET /api/products/:id/reviews Public Product reviews
POST /api/products/:id/review Auth Add a review
PUT /api/products/:productId/reviews/:id Owner Update a review
DELETE /api/products/:productId/reviews/:id Owner/Admin Delete a review
GET /api/admin/reviews Admin All reviews
GET /api/products/:productId/global-rating Public Average rating
POST /api/upload Admin Upload an image to Cloudinary
POST /api/contact Public Send a contact message

Protected endpoints expect Authorization: Bearer <token>.

Payment flow

  1. Customer places an order (POST /api/orders/place-order). The server computes totals from product prices, validates stock, decrements it, and clears the cart.

  2. Frontend requests a PaymentIntent (POST /api/orders/create-payment-intent) with only the orderId. The server looks up the order and computes the amount itself; the client never supplies the amount.

  3. Stripe.js confirms the card payment in the browser.

  4. Frontend calls POST /api/orders/confirm-payment. The server retrieves the PaymentIntent from Stripe and verifies:

    • the payment status is succeeded
    • the PaymentIntent belongs to this order
    • the paid amount matches the order total

    Only then is the order marked as paid.

Security notes

Implemented in this codebase:

  • Order cancellation, activation, deletion, and payment operations enforce ownership or admin rights (no IDOR)
  • Payment amounts are computed server-side and verified with Stripe
  • Password reset and email verification tokens are stored hashed; reset links point to the frontend
  • Password change no longer double-hashes the password
  • Input validation with Joi on authentication, profile, orders, cart, products, reviews, stock, payments, and contact
  • Rate limiting on login, registration, password reset, and contact submissions
  • Helmet, CORS allow-listing, and a centralized error handler
  • Passwords and tokens are never returned by user endpoints
  • Order placement validates and atomically adjusts stock with rollback on failure

Remaining recommendations for production:

  • Prefer HttpOnly, Secure cookies (or short-lived access tokens + refresh tokens) over localStorage
  • Add a Stripe webhook for payment_intent.succeeded as a second source of truth
  • Use a shared store (Redis) for rate limiting when running more than one instance
  • Add pagination to product/order listing endpoints
  • Add a request-id logger and structured logging
  • Enable MongoDB transactions if you migrate to a replica set (current stock updates use atomic conditional updates instead)
  • Add integration tests for Stripe and email providers using their test modes/mocks

Alternative deployments

The live app is hosted on Netlify, but the included files also support other hosts:

  • Backend: the backend/Dockerfile builds a Node 22 image. Set all backend env vars in your host (Fly.io, Koyeb, Render, etc.). At minimum set NODE_ENV=production, ALLOWED_ORIGINS, FRONTEND_URL, a strong JWT_SECRET, MongoDB, Stripe, Cloudinary, and email credentials.
  • Frontend: frontend/vercel.json rewrites SPA routes to index.html. Set VITE_APP_API_URL, VITE_STRIPE_PUBLISHABLE_KEY, VITE_CLOUDINARY_CLOUD_NAME, and VITE_CLOUDINARY_UPLOAD_PRESET in the Vercel project settings, then deploy.

License

ISC

About

Full-stack MERN e-commerce app with a React storefront and admin dashboard, Express/MongoDB API, Stripe payments, Cloudinary uploads, and JWT auth.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages