Back to blog
September 20, 2024

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.

  1. Import repo
  2. Set root directory to frontend
  3. 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
  1. 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:

  1. In Vercel: Settings → Domains → Add e-shop.stackbp.es
  2. In Hostinger DNS: Add CNAME record pointing to cname.vercel-dns.com
  3. 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. 🚀

Back to blog