DEV Community

Cover image for Building a Full-Stack Finance Tracker: My MERN Journey (Real Problems & Fixes) 💰
SoftwareDev
SoftwareDev

Posted on

Building a Full-Stack Finance Tracker: My MERN Journey (Real Problems & Fixes) 💰

Ever wondered what it's really like to build a production-ready MERN stack app? Spoiler: It's not as smooth as the tutorials make it look! 😅

I spent 2 months building a personal finance tracker, and I'm sharing the real challenges I faced—especially with authentication, deployment, and dark mode—along with solutions that actually worked.

Live Demo: Finance Tracker

⚡ TL;DR

  • Built a production-ready MERN finance tracker
  • Solved real issues with JWT expiration, dark mode flicker, CORS, and recurring jobs
  • Deployed using Vercel + Render + MongoDB Atlas
  • Focused on real-world problems tutorials don’t cover

🎯 What I Built

A full-featured finance tracker with:

  • 💱 Multi-currency support with real-time conversion
  • 📊 Interactive analytics (charts, breakdowns, trends)
  • 💰 Smart budgets with alert thresholds
  • 🎯 Goal tracking with milestones
  • 🔄 Recurring transactions (salary, rent, subscriptions)
  • 🌓 Dark/Light mode with persistence
  • 🔐 Secure JWT authentication

Tech Stack:

  • Frontend: React 18, Vite, Tailwind CSS, Recharts
  • Backend: Node.js, Express.js, MongoDB Atlas
  • Deployment: Vercel (frontend) + Render (backend)

🔥 Challenge #1: Authentication That Actually Works

The Problem

Users were getting randomly logged out, expired tokens crashed the app, and I had no idea why.

The Solution

I built a multi-layered auth system that handles token lifecycle properly:

1. Smart Token Validation

export const isTokenExpired = (token) => {
  if (!token) return true;
  try {
    const payload = JSON.parse(atob(token.split(".")[1]));
    return payload.exp < Date.now() / 1000;
  } catch {
    return true;
  }
};
Enter fullscreen mode Exit fullscreen mode

2. Axios Interceptor for Auto-Logout

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.clear();
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

3. Periodic Token Checks

// Check every 5 minutes
useEffect(() => {
  const interval = setInterval(() => {
    const token = localStorage.getItem("token");
    if (token && isTokenExpired(token)) {
      logout();
    }
  }, 5 * 60 * 1000);
  return () => clearInterval(interval);
}, []);
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Never trust the client! Always validate tokens on both ends and handle expiration gracefully.


🌓 Challenge #2: Dark Mode Without the Flash

The Problem

The theme would flash white on page load, wouldn't persist, and sometimes ignored system preferences.

The Solution

A ThemeContext that handles everything properly:

export function ThemeProvider({ children }) {
  const [isDarkTheme, setIsDarkTheme] = useState(() => {
    const saved = localStorage.getItem("theme");
    return saved === "dark" || 
      (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches);
  });

  useEffect(() => {
    if (isDarkTheme) {
      document.documentElement.classList.add("dark");
      document.documentElement.setAttribute("data-theme", "dark");
    } else {
      document.documentElement.classList.remove("dark");
      document.documentElement.setAttribute("data-theme", "light");
    }
    localStorage.setItem("theme", isDarkTheme ? "dark" : "light");
  }, [isDarkTheme]);

  return (
    <ThemeContext.Provider value={{ isDarkTheme, toggleTheme: () => setIsDarkTheme(!isDarkTheme) }}>
      {children}
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Magic:

  1. Initialize from localStorage FIRST, then system preferences
  2. Apply to <html> element before first render
  3. Use both dark class (Tailwind) and data-theme attribute

💱 Challenge #3: Multi-Currency Support

The Problem

Users wanted to track expenses in different currencies but view everything in their preferred currency.

The Solution

On-demand conversion that only runs when needed:

const convertedTransactions = await Promise.all(
  transactions.map(async (t) => {
    if (t.currency !== preferredCurrency) {
      try {
        const response = await axios.get(
          `https://api.exchangerate-api.com/v4/latest/${t.currency}`
        );
        const rate = response.data.rates[preferredCurrency];
        return {
          ...t._doc,
          amount: t.amount * rate,
          currency: preferredCurrency,
          originalAmount: t.amount, // Keep original!
          originalCurrency: t.currency,
        };
      } catch (error) {
        return t; // Fallback to original
      }
    }
    return t;
  })
);
Enter fullscreen mode Exit fullscreen mode

Optimization Tips:

  • Skip transactions already in preferred currency
  • Use Promise.all() for parallel processing
  • Always keep original values for reference
  • Graceful fallback if API fails

Future improvement: Add Redis caching to reduce API calls.


🔄 Challenge #4: Recurring Transactions

The Problem

Scheduling recurring transactions (monthly rent, weekly groceries) across server restarts and timezones.

The Solution

Using node-schedule with careful timezone handling:

const scheduleRecurringTransaction = (transaction, frequency, endDate, count) => {
  let rule = new schedule.RecurrenceRule();
  rule.tz = "UTC"; // Critical for consistency!

  if (frequency === "monthly") {
    rule.date = new Date(transaction.date).getDate();
    rule.hour = 9;
    rule.minute = 0;
  }
  // ... other frequencies

  let executionCount = 0;

  const job = schedule.scheduleJob({ start: startDate, rule, end: endDate }, async () => {
    if (count && executionCount >= count) {
      job.cancel();
      return;
    }

    await new Transaction({
      ...transactionData,
      isRecurring: false, // Prevent infinite loops!
    }).save();

    executionCount++;
  });

  return job;
};
Enter fullscreen mode Exit fullscreen mode

Known Limitation: Jobs don't persist across server restarts. For production, I'd use Bull or Agenda with Redis.


🚀 Challenge #5: Deployment Hell

The Problem

CORS errors everywhere, environment variables not working, MongoDB connection timeouts... the works.

The Solutions

Backend CORS (Render):

app.use(cors({
  origin: [
    "https://finance-tracker-gamma-eight.vercel.app",
    "http://localhost:5173", // Local dev
  ],
  credentials: true,
  methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
}));
Enter fullscreen mode Exit fullscreen mode

Frontend API Config (Vercel):

const API_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:5000";

const api = axios.create({
  baseURL: `${API_URL}/api`,
  timeout: 10000,
});
Enter fullscreen mode Exit fullscreen mode

MongoDB Atlas:

  • Whitelist Render's IPs or 0.0.0.0/0 (for free tier)
  • Use retry logic for connection

Deployment Checklist:

  • ✅ Set environment variables in platform dashboards
  • ✅ Update CORS with production URLs
  • ✅ Enable HTTPS on both services
  • ✅ Test auth flow end-to-end after deployment
  • ✅ Expect cold starts on free tiers (Render sleeps after 15 min)

📊 Cool Features Worth Mentioning

Budget Alerts

Automatically warn users when they reach 80% (configurable) of their budget:

const progress = totalSpent / budget.amount;

if (progress >= budget.alertThreshold) {
  alerts.push({
    message: totalSpent > budget.amount 
      ? `You're over budget on ${category}!`
      : `You've reached ${Math.round(progress * 100)}% of your budget.`,
    severity: totalSpent > budget.amount ? "high" : "medium",
  });
}
Enter fullscreen mode Exit fullscreen mode

Prevent Duplicate Goals

MongoDB compound index + pre-save validation:

goalSchema.index(
  { userId: 1, name: 1, targetAmount: 1, deadline: 1 },
  { unique: true }
);

goalSchema.pre("save", async function(next) {
  const duplicate = await this.constructor.findOne({
    userId: this.userId,
    name: this.name.trim(),
    targetAmount: this.targetAmount,
    deadline: this.deadline,
  });

  if (duplicate) {
    throw new Error("Goal already exists!");
  }
  next();
});
Enter fullscreen mode Exit fullscreen mode

🎓 Key Lessons Learned

  1. Always validate on both ends - Client-side validation is UX, server-side is security
  2. Plan deployment from day 1 - Don't treat it as an afterthought
  3. Error handling > features - Graceful failures make better UX than more features
  4. Context API is powerful but use wisely - Too many re-renders can kill performance
  5. Database indexes matter - Even small apps benefit from proper indexing
  6. Real-world apps are messy - Tutorials skip all the hard parts!

🔮 What's Next?

  • 🔌 WebSockets for real-time updates
  • 📧 Email notifications for budget alerts
  • 📱 React Native mobile app
  • 🏦 Bank account integration (Plaid API)
  • 📊 PDF report generation
  • 💹 Investment tracking

🎬 Conclusion

Building this taught me that production apps involve way more than CRUD operations. Authentication flows, deployment pipelines, error handling, timezone issues—every layer has its challenges.

The most valuable lesson? Start simple, iterate often, and always prioritize security and UX.

If you're building something similar, I hope this saves you some debugging time!

💬 If you’ve faced similar issues (auth, deployment, dark mode, scheduling),
drop a comment — I’d love to learn how you solved them.


Tags: #webdev #react #nodejs #mongodb #javascript #mernstack #tutorial

Top comments (3)

Collapse
 
martijn_assie_12a2d3b1833 profile image
Martijn Assie

Built this the hard way, and it shows!!
Really solid breakdown of the problems tutorials usually skip...
Auth, dark mode, deployment, all the boring-but-critical stuff is handled properly!
Nice touch calling out real limitations like job persistence, that’s rare and useful.

Collapse
 
its_dev_ profile image
SoftwareDev

Thanks a lot, Martijn! Really appreciate you noticing those details 🙌
I intentionally covered the boring but critical parts because that’s where most real projects break. Glad it was useful!

Collapse
 
adarshchimnani97 profile image
Adarsh Chimnani

This looks amazing. Can you share the github repo and any tutorial for step-by-step developing this app? Thank you.