DEV Community

Cover image for Building a Full-Stack Calculator App with MERN: A Step-by-Step Guide
Siddhant
Siddhant

Posted on

Building a Full-Stack Calculator App with MERN: A Step-by-Step Guide

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 });
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
  });
};
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. Backend:

    • Navigate to the backend directory and run:
     npm install
     npm start
    
  2. Frontend:

    • Navigate to the frontend directory and run:
     npm install
     npm run dev
    

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)