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;
}
};
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);
}
);
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);
}, []);
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>
);
}
The Magic:
- Initialize from localStorage FIRST, then system preferences
- Apply to
<html>element before first render - Use both
darkclass (Tailwind) anddata-themeattribute
๐ฑ 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;
})
);
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;
};
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"],
}));
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,
});
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",
});
}
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();
});
๐ Key Lessons Learned
- Always validate on both ends - Client-side validation is UX, server-side is security
- Plan deployment from day 1 - Don't treat it as an afterthought
- Error handling > features - Graceful failures make better UX than more features
- Context API is powerful but use wisely - Too many re-renders can kill performance
- Database indexes matter - Even small apps benefit from proper indexing
- 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 (0)