How I Built My First MERN Stack App: Lessons From the Trenches
How I Built My First MERN Stack App: Lessons From the Trenches
Every developer remembers their first real full-stack project. Not the tutorial ones where everything works perfectly — the real one, where nothing works, Stack Overflow becomes your homepage, and you wonder why you chose this career.
This is that story.
The Idea
I wanted to build a simple task management app — nothing fancy. Users could create accounts, add tasks, mark them done, delete them. Classic CRUD. How hard could it be?
Narrator: It was very hard.
Setting Up the Stack
The MERN stack — MongoDB, Express.js, React, Node.js — is one of the most popular full-stack combinations for JavaScript developers. The appeal is obvious: JavaScript everywhere, from the database queries to the UI. One language to rule them all.
Backend First: Node.js + Express
I started with the server. Set up Express, connected MongoDB using Mongoose, built out the REST API endpoints:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
mongoose.connect(process.env.MONGO_URI);
app.use(express.json());
app.use('/api/tasks', require('./routes/tasks'));
app.listen(5000, () => console.log('Server running'));
Looked clean. Felt good. Then I tried to connect it to my React frontend and got my first CORS error.
The CORS Wall
If you've never seen a CORS error, consider yourself lucky. It's the browser's way of saying "I don't trust this server." The fix is simple once you know it — add the cors middleware — but when you're new, it feels like a personal attack.
const cors = require('cors');
app.use(cors({ origin: 'http://localhost:3000' }));
Problem solved. First real lesson: most errors have simple fixes. The hard part is knowing what to search for.
Building the React Frontend
I used Create React App for the frontend. Set up basic components: Login, Register, Dashboard, TaskList, TaskCard.
State management was my next headache. I started with prop drilling — passing data down through multiple component levels. By the third level, I wanted to quit.
Enter Context API. It saved my sanity.
const TaskContext = createContext();
export const TaskProvider = ({ children }) => {
const [tasks, setTasks] = useState([]);
const fetchTasks = async () => {
const res = await axios.get('/api/tasks');
setTasks(res.data);
};
return (
<TaskContext.Provider value={{ tasks, fetchTasks }}>
{children}
</TaskContext.Provider>
);
};
Authentication: The Hard Part
JWT authentication was where I spent the most time. The concept is simple: user logs in, server sends back a token, frontend stores the token and sends it with every protected request.
The implementation? Less simple.
I learned about:
- Storing tokens in localStorage vs httpOnly cookies (security tradeoff)
- Token expiry and refresh flows
- Protected routes in React
- Middleware for verifying tokens on the backend
// Backend middleware
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'Unauthorized' });
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return res.status(403).json({ message: 'Invalid token' });
req.user = decoded;
next();
});
};
Deployment Nightmares
Building locally is one thing. Deploying is another universe.
I deployed the backend to Railway and the frontend to Vercel. The process involved:
- Setting up environment variables correctly (not the same as localhost)
- Updating CORS to allow the production frontend URL
- Fixing MongoDB Atlas network access settings
- Debugging why the app worked locally but not in production (answer: environment variables)
What I'd Do Differently
Looking back, here's what I wish I'd known:
1. Plan your data models first. I changed my Mongoose schemas three times. Each change required data migration headaches. Spend 30 minutes designing your schema before writing a single line of code.
2. Error handling from day one. My first version had basically no error handling. When things broke in production, I had no idea why. Add try-catch everywhere and send meaningful error responses.
3. Use Postman before building the frontend. Test every API endpoint thoroughly before touching React. It saves hours of debugging where you're not sure if the problem is frontend or backend.
4. Environment variables are not optional. Never hardcode API URLs, database connections, or secret keys. Use .env files from day one.
The Result
It worked. It was messy, over-engineered in some places and under-engineered in others, but it worked. Users could register, log in, create tasks, and delete them.
More importantly: I understood why every piece existed. That's the real value of building something from scratch.
Key Takeaways
- CORS errors are normal. Learn to recognize and fix them quickly.
- Plan data models before writing code.
- JWT auth is powerful but has nuances — understand the security tradeoffs.
- Deployment always has surprises. Expect it and budget time for it.
- Build broken things. That's how you actually learn.
Want to build something together or need a MERN developer for your project? Get in touch.