Migrating E-commerce: From Vanilla JS to Next.js
Why I rewrote my entire e-commerce platform and what I learned from starting with vanilla JavaScript first
Migrating E-commerce: From Vanilla JS to Next.js -- ecommerce-bp-v2 still in progress but well, running somehow
🛒 The Original Sin: Building E-commerce in Vanilla JS
Picture this: me, fresh into web development, thinking "frameworks are overkill, I'll just use vanilla JavaScript." Built an entire e-commerce platform - product catalog, shopping cart, checkout flow - all in plain HTML, CSS, and JS.
It worked. Until it didn't. So that, you can find out the new scaled project on going at the gitHub
And the best part? This whole genius idea came from me following a SuperSimpleDev course — you know, the guy who teaches JavaScript fundamentals in the friendliest, most beginner-proof way imaginable… perfect for someone as brilliantly clueless as me.
🤔 Why Vanilla JS First Was Actually Good
Before you judge, let me defend my past self:
I Learned the Fundamentals
- DOM manipulation:
querySelector,addEventListener,appendChild- the hard way - Event handling: Event delegation, bubbling, preventing defaults
- State management: Managing cart state without Redux or Zustand forced me to understand what state actually is
- Async operations: Fetch API, promises, async/await - no abstractions hiding what's happening
- localStorage: Client-side persistence before I knew what SSR meant
I Understood What Frameworks Solve
You don't appreciate React until you've manually updated the DOM for the 47th time. You don't appreciate Next.js routing until you've written your own SPA router with window.location.hash.
Real talk: Starting with vanilla JS gave me a foundation that makes me better at using frameworks now. I know what's happening under the hood.
💀 When Vanilla JS Became a Problem
Around 1,000 lines of code, things got messy:
The Cart Logic Nightmare
// Update cart in 5 different places
function addToCart(product) {
// Update localStorage
// Update DOM
// Update cart count
// Update total price
// Show notification
// Sync with backend (maybe?)
// Pray it all works
}
Every time I touched cart logic, something else broke. No single source of truth.
The Performance Wall
- Manually manipulating DOM on every update = laggy UI
- No virtual DOM = full page repaints
- No code splitting = 300KB JavaScript bundle loaded upfront
- No optimization = slow initial load, slow interactions
The SEO Problem
Single Page App with vanilla JS = zero SEO. Google seeing empty HTML. Products not indexable. Dead on arrival for organic traffic.
The Maintainability Crisis
// Find this function in 3,000 lines of code
// Good luck
function updateProduct() { /* ... */ }
No component structure. No file organization. Just files named script.js, main.js, app.js. Which one has the cart logic? ¯_(ツ)_/¯
🚀 The Next.js Rewrite
Decided to rebuild with Next.js + Strapi (headless CMS). Best decision I made.
What Changed
Before (Vanilla JS):
// products.html
<div id="products"></div>
<script>
fetch('/api/products')
.then(res => res.json())
.then(products => {
const container = document.getElementById('products')
products.forEach(product => {
const div = document.createElement('div')
div.innerHTML = `
<h3>${product.name}</h3>
<p>${product.price}</p>
<button onclick="addToCart(${product.id})">Add</button>
`
container.appendChild(div)
})
})
</script>
After (Next.js):
// app/products/page.tsx
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
Cleaner. Server-side rendered. SEO-friendly. Type-safe.
Wins from the Migration
✅ Performance
- 60% faster initial load (code splitting + SSR)
- Smooth interactions (React virtual DOM)
- Image optimization built-in (Next.js Image component)
✅ SEO
- Server-side rendering = crawlable HTML
- Dynamic meta tags per product
- Structured data automatically generated
✅ Developer Experience
- TypeScript catching errors before runtime
- Component-based architecture (reusable, testable)
- Hot reload during development
- File-based routing (no more manual route setup)
✅ Maintainability
/app
/products
page.tsx ← Products page
[id]/page.tsx ← Single product
/cart
page.tsx ← Cart page
/checkout
page.tsx ← Checkout flow
/components
ProductCard.tsx
CartItem.tsx
Everything has its place. Easy to find. Easy to update.
🌐 The Deployment Saga: A Comedy of Errors
Building locally is one thing. Deploying to production? That's where the real fun begins.
The Architecture
Here's what I ended up with - all free tier, perfect for a portfolio project:
Architecture Overview
Frontend
Platform: Vercel
Reason: Free, optimized for Next.js, instant deploys
Backend
Platform: Render
Reason: Free tier (problem: spins down after 15min inactivity)
Database
Platform: Aiven (PostgreSQL)
Reason: Free tier, keeps data separated from the backend
The SQLite to PostgreSQL Migration Drama
Strapi uses SQLite by default for local development. Cute. But production needs PostgreSQL.
Problem #1: I had 42 products in SQLite, PostgreSQL was empty.
Problem #2: The pg package was installed in the root directory instead of backend/. Strapi couldn't find it.
# Wrong - installed in root
cd ecommerce-bp-v2
pnpm add pg
# Right - installed in backend where Strapi lives
cd backend
pnpm add pg
Problem #3: Even with correct config, Strapi kept showing products that didn't exist in PostgreSQL. Ghost data. Turns out: aggressive caching + wrong package location = confusion. Installed DBeaver to look for the tables (found 0). Looking for the ghost data half an hour till IA found my prob because it would have taken me at least an hour myself.
The Fix: Clean rebuild + proper package installation:
cd backend
pnpm add pg
Remove-Item -Recurse -Force .cache, build
pnpm run build
pnpm run develop
Finally saw Database: postgres in the logs. Victory.
Migrating the Data
Strapi has export/import commands. Who knew?
# Step 1: Switch to SQLite, export data
# (edit .env to DATABASE_CLIENT=sqlite)
npx strapi export --no-encrypt -f backup (now on my commands treasure chest)
# Step 2: Switch to PostgreSQL, import data
# (edit .env to DATABASE_CLIENT=postgres)
npx strapi import -f backup.tar.gz --force (also, pr0 cmd)
84 products transferred. Beautiful.
The Render Deployment
Created a Web Service on Render, pointed to GitHub repo. First deploy failed immediately.
Error: The upload folder doesn't exist or is not accessible
Strapi needs a public/uploads folder. Doesn't exist in a fresh deploy.
Fix: Modified the build command:
pnpm install && mkdir -p public/uploads && pnpm run build
Second deploy: success. Strapi running in production.
Important caveat: Render free tier spins down after 15 minutes of inactivity. First request after sleep takes ~30-60 seconds. For a portfolio project, that's fine. Just warn visitors.
The Vercel Deployment
Frontend was easier. Vercel just works with Next.js.
- Import repo
- Set root directory to
frontend - Add environment variables:
NEXT_PUBLIC_STRAPI_URL=https://ecommerce-bp-v2.onrender.com
NEXT_PUBLIC_STRAPI_API_URL=https://ecommerce-bp-v2.onrender.com/api
- Deploy
Two minutes later: live site. (It's incredible how these tools work now compared to the 90s.)
Custom Domain Setup
Already had stackbp.es for my portfolio. Added subdomain e-shop.stackbp.es:
- In Vercel: Settings → Domains → Add
e-shop.stackbp.es - In Hostinger DNS: Add CNAME record pointing to
cname.vercel-dns.com - Wait for SSL certificate (automatic)
Done. Professional URL for a portfolio project.
Deployment Lessons Learned
1. Check where your packages are installed Monorepos are tricky. Backend packages go in backend. Sounds obvious. Wasn't.
2. Environment variables are everything
Local .env doesn't go to GitHub. Configure them again in Render/Vercel.
3. Free tiers have tradeoffs
- Render: Spins down (cold starts)
- Aiven: Limited connections
- Vercel: Limited bandwidth
For portfolio/learning: perfect. For real users: consider paid tiers.
4. Database migrations are scary but doable Export, switch config, import. Test locally first. Have backups.
5. The upload folder gotcha
Strapi needs public/uploads to exist. Create it in your build command for platforms without persistent storage.
🎯 What I'd Do Differently
Keep the Vanilla JS Version as Learning
I don't regret building it in vanilla first. It taught me fundamentals. But I'd move to a framework sooner - maybe after 500 lines instead of 3,000.
Start with TypeScript
Migrated to TS halfway through the Next.js version. Should've started with it. The types caught so many bugs.
Use a Design System
Built custom components for everything. Should've used shadcn/ui or similar from the start. Don't reinvent styled buttons.
Plan the Data Model Better
Rushed into coding. Should've spent more time designing the database schema and API structure. Paid for it later with weird workarounds.
Plan Deployment Earlier
Built everything locally, then scrambled to deploy. Should've deployed a "Hello World" version first to understand the platform quirks.
💡 Lessons Learned
Frameworks Exist for a Reason
They solve real problems:
- Routing
- State management
- Performance optimization
- SEO
- Developer experience
Don't avoid them to "learn fundamentals" forever. Learn fundamentals, then use the tools.
Migration is Easier Than You Think
I was scared to rewrite. Thought it'd take months. Took 2 weeks for the core functionality. Another week for polish.
Key: Don't rewrite everything at once. Migrate page by page. Keep the old version running until the new one is ready.
TypeScript is Worth It
The migration to Next.js also meant adopting TypeScript. Game changer. Autocomplete, type checking, refactoring confidence. Never going back to plain JS for serious projects.
Deployment is Part of Development
"It works on my machine" isn't enough. Deploy early, deploy often. Understand your hosting platform's quirks before you have 3,000 lines of code to debug.
🔧 The Stack Evolution
Version 1 (Vanilla): e-commercebp
- HTML/CSS/JavaScript
- localStorage for cart
- Fetch API for products
- Manual DOM manipulation
Version 2 (Next.js): ecommerce-bp-v2
- Next.js 14 + TypeScript
- React for UI
- Strapi for content management
- PostgreSQL on Aiven
- Tailwind CSS for styling
- Deployed: Vercel (frontend) + Render (backend)
📊 Results
Before:
- PageSpeed score: 45/100
- Time to Interactive: 4.2s
- Cart bugs: Weekly
- SEO ranking: Nowhere
- Deployment: None (local only)
After:
- PageSpeed score: 92/100
- Time to Interactive: 1.1s
- Cart bugs: Rare
- SEO ranking: Actually showing up in search
- Deployment: Live at e-shop.stackbp.es
🎬 Final Thoughts
Building the vanilla JS version first wasn't a mistake - it was education. But shipping it to production was. Use vanilla JS to learn, but use frameworks to ship.
The deployment journey? Frustrating at times, but worth it. Nothing beats seeing your project live with a real URL. And as a tribute to SuperSimpleDev - the course that started it all - every painful debugging session was worth it.
Key takeaway: Modern frameworks aren't bloat. They're accumulated wisdom from thousands of developers solving the same problems you'll eventually face. Use them.
Would I recommend starting with vanilla JS? For learning - yes. For building anything you plan to maintain - no. Go straight to Next.js (or React, Vue, Svelte - doesn't matter, just use something).
The best code is code you don't have to write. Frameworks handle routing, optimization, and best practices so you can focus on building features.
Keep shipping. Learn from mistakes. Migrate when needed. Deploy it live. Repeat. 🚀