Creating a full-stack application from scratch is an excellent way to understand the complete development workflow. In this guide, we will walk you through building a calculator app with user authentication, which stores the user’s calculation history. The app is built using the MERN stack (MongoDB, Express.js, React.js, Node.js). Let’s break down how each part works and how it all comes together.
What is the MERN Stack?
The MERN stack consists of:
- MongoDB: A NoSQL database to store data (in this case, user data and calculation history).
- Express.js: A web framework for Node.js, used to handle HTTP requests and create APIs.
- React.js: A JavaScript library for building user interfaces, especially single-page applications.
- Node.js: A JavaScript runtime that allows us to run JavaScript on the server.
Our calculator app will allow users to sign up, log in, perform basic calculations, and view their calculation history. The backend will handle authentication and store calculation data, while the frontend will allow users to interact with the application.
Project Structure
Before we dive into the code, let's review the overall structure of the project:
calculator-app/
│
├── backend/
│ ├── config/
│ │ └── db.js # MongoDB connection setup
│ │
│ ├── controllers/
│ │ ├── authController.js # User authentication logic
│ │ └── calculationController.js # Calculation logic
│ │
│ ├── middleware/
│ │ └── authMiddleware.js # JWT verification middleware
│ │
│ ├── models/
│ │ ├── User.js # User model
│ │ └── Calculation.js # Calculation model
│ │
│ ├── routes/
│ │ └── api.js # API routes
│ │
│ └── server.js # Express server setup
│
└── frontend/
├── src/
│ ├── components/
│ │ ├── Calculator.js # Calculator component
│ │ └── Login.js # Login component
│ │
│ ├── App.js # Main app component
│ ├── index.js # Entry point for React
│ └── App.css # CSS styles
│
├── public/
│ └── index.html # HTML template
│
└── vite.config.js # Vite configuration
Backend Setup
The backend will manage user authentication and store calculation data. We will use MongoDB to store user information and calculation history, along with JSON Web Tokens (JWT) for authentication.
1. Creating the User Model (backend/models/User.js
)
The user model will handle user registration and login functionality. Each user has a username and a hashed password.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
module.exports = mongoose.model('User', userSchema);
2. Handling User Authentication (backend/controllers/authController.js
)
We’ll use bcrypt to hash passwords and jsonwebtoken to create JWT tokens for authenticated users. Here’s an example of how we register and log in a user.
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.register = async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ username, password: hashedPassword });
await newUser.save();
res.status(201).json({ message: 'User registered successfully' });
};
exports.login = async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
};
3. Storing Calculations (backend/models/Calculation.js
)
For every calculation a user performs, we’ll store the calculation expression and the result. Here's a basic model for storing calculation history:
const mongoose = require('mongoose');
const calculationSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
expression: { type: String, required: true },
result: { type: Number, required: true },
createdAt: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Calculation', calculationSchema);
4. Routes and Middleware (backend/routes/api.js
, backend/middleware/authMiddleware.js
)
We create routes for registering, logging in, performing calculations, and fetching calculation history. To protect certain routes, we use middleware to verify the user’s JWT token.
const express = require('express');
const { register, login } = require('../controllers/authController');
const { calculate, fetchHistory } = require('../controllers/calculationController');
const { verifyToken } = require('../middleware/authMiddleware');
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
router.post('/calculate', verifyToken, calculate);
router.get('/history', verifyToken, fetchHistory);
module.exports = router;
JWT Verification Middleware (backend/middleware/authMiddleware.js
):
const jwt = require('jsonwebtoken');
exports.verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) return res.sendStatus(403);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
5. Setting Up the Express Server (backend/server.js
)
Finally, set up the Express server to handle incoming requests and connect to the database.
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const apiRoutes = require('./routes/api');
dotenv.config();
connectDB();
const app = express();
app.use(express.json());
app.use('/api', apiRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Frontend Setup
For the frontend, we will use React to handle the user interface. We’ll use localStorage to store the JWT token and Axios to send API requests to our backend.
1. Handling Login (frontend/src/components/Login.js
)
When the user logs in, we store the JWT token in localStorage and redirect the user to the calculator page.
import React, { useState } from 'react';
const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async (e) => {
e.preventDefault();
const response = await fetch('/api/users/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
// Redirect to calculator page
} else {
// Handle login error
alert(data.message);
}
};
return (
<form onSubmit={handleLogin}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
};
export default Login;
2. Calculator Component (frontend/src/components/Calculator.js
)
The core of our frontend is the Calculator component. This component allows users to input expressions, calculate results, and send this data to the backend.
import React, { useState, useEffect } from 'react';
const Calculator = () => {
const [expression, setExpression] = useState('');
const [result, setResult] = useState(null);
const [history, setHistory] = useState([]);
const handleCalculate = async () => {
const response = await fetch('/api/calculations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({ expression }),
});
const data = await response.json();
setResult(data.result);
fetchHistory(); // Update history after calculation
};
const fetchHistory = async () => {
const response = await fetch('/api/history', {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
const data = await response.json();
setHistory(data);
};
useEffect(() => {
fetchHistory(); // Fetch history on component mount
}, []);
return (
<div>
<input
type="text"
value={expression}
onChange={(e) => setExpression(e.target.value)}
/>
<button onClick={handleCalculate}>Calculate</button>
{result !== null && <h3>Result: {result}</h3>}
<h4>Calculation History:</h4>
<ul>
{history.map((calc) => (
<li key={calc._id}>
{calc.expression} = {calc.result}
</li>
))}
</ul>
</div>
);
};
export default Calculator;
3. Main App Component (frontend/src/App.js
)
Finally, we will set up our main app component, which uses React Router to manage navigation between the login and calculator pages.
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Calculator from './components/Calculator';
import Login from './components/Login';
const App = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/calculator" element={<Calculator />} />
</Routes>
</Router>
);
};
export default App;
4. Entry Point (frontend/src/index.js
)
This is the entry point of the React application where we render our app.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './App.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
5. HTML Template (frontend/public/index.html
)
Finally, set up the HTML template for the app.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculator App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Final Thoughts
Congratulations! You've built a full-stack calculator application using the MERN stack. This project covers essential topics like user authentication, API development, and CRUD operations, providing a solid foundation for future projects.
You can further enhance this app by adding features such as password recovery, advanced calculation capabilities, or even real-time updates. Experiment and explore different functionalities to make the app more robust!
Running the Application
To run this application:
-
Backend:
- Navigate to the
backend
directory and run:
npm install npm start
- Navigate to the
-
Frontend:
- Navigate to the
frontend
directory and run:
npm install npm run dev
- Navigate to the
Make sure you have the necessary environment variables set up (like MONGODB_URI
and JWT_SECRET
) to successfully connect to MongoDB and authenticate users.
With this detailed guide, you should have a clear understanding of how to build a full-stack application with the MERN stack. Enjoy coding!
Top comments (0)