How to build full stack apps ready for production with Murran stack

by SkillAiNest

As a developer, we are always looking for more efficient tools. Marine Stack (Mongo DB, Express Dot JS, React, and Node Dot JS) stands for its Javascript -based nature, and offers a unanimous language in the entire application.

In this guide, you will create a full task manager app with user verification, safe routes, and full CRUD functionality, built on the back end with Front End and Express/Mongo DB.

This article will work as a First Guide to build, protect and deploy the death application, drawing on your own practical experience. Each section has a code you can run, and I will give comprehensive specifications along the way.

It doesn’t matter if you are just starting with the marin or want to equalize the knowledge of your architecture and production deployment – this article is designed to bring you from zero to production with confidence.

The table of content

Provisions

Before jumping into the project, here you will need to take the most of this tutorial.

Tolls and Tech stacks

You will use the following technologies in the entire project:

  • Node. JS and NPM – Basid Run Time and Package Manager

  • Express. JS – Web framework for node

  • Mongo DB Atlas -Cloud Hosted NosQL Database

  • Mangoes – ODM for Mongo DB

  • Reaction – Front & UI Library

  • Router’s reaction -Clite Side for Routing

  • Axios – To make API requests

  • JEST & SPERTEST – For the backbone tests

  • Test the library and Cyprus test – For the Front & Unit and E2E test

  • ESLANT + PRETER – for code formatting, lining

  • Laugh -Prey Committee Hooks Setting up

  • Helmet, JOI, Express Rate-Had, CORS – for greeting, verification, and the best ways

  • PM2 & nginx – For Backland Deployment

  • Tentry – To monitor the error

Deer and setup

  • Javascript, reaction, and node. The basic knowledge of JS

  • Familiar with REST APIS and HTTP Application/Response Floose

  • Gut and Gut Hub Account for Version Control

  • A Free Mangod B -Atlas Account

  • Nod dot j and npm installed locally (node ​​18+ suggested)

Project Setup: Lasted

A well -made plan is very important to maintain. We will adopt a clear separation between the front and the back end.

The project structure

This structure clearly illustrates the Front & (Client/) of reaction from Node Dot JS/Express DSS Back (Server/), which promotes modification and easy management.

my-mern-app/                # Root folder
├── client/                 # React frontend
│   ├── public/
│   ├── src/
│   │   ├── components/
│   │   ├── pages/
│   │   ├── App.js
│   │   └── index.js
│   └── package.json
├── server/                 # Node.js/Express.js backend
│   ├── config/
│   ├── controllers/
│   ├── models/
│   ├── routes/
│   ├── services/
│   ├── app.js
│   └── package.json

Code Quality: Lanting and Formatting

When you are making such a production grade application, a consistency is key. We will use Air BNB styling and essay with pretiat for automatic code quality and formatting.

To install these tools, run it in your terminal:

npm install --save-dev eslint prettier eslint-config-airbnb-base eslint-plugin-prettier

And here are some setups that have their recommended configurations:

This Configure Air BNB and PRETYS STAIL SOLDS SEEP ESLANTS FOR NOD DOT JS PROBLEMS Using, with customs rules to relax strict lunting obstacles like allowing console.log And disabling the names of the mandatory function.

.elintrc.js (server side example)

module.exports = {

  env: {

    node: true,

    commonjs: true,

    es2021: true,

  },

  extends: ("airbnb-base", "prettier"),

  plugins: ("prettier"),

  parserOptions: {

    ecmaVersion: 12,

  },

  rules: {

    "prettier/prettier": "error",

    "no-console": "off",

    "func-names": "off",

    "no-process-exit": "off",

    "class-methods-use-this": "off",

    "import/no-extraneous-dependencies": "off",

  },

};

.pretttierrc

This Config implemented permanent formatting: Add semiculous, use the trailing coma where it is correct, and prefer single prices for wire.

{

  "semi": true,

  "trailingComma": "all",

  "singleQuote": true

}

Version Control: Gut Accessories

The gut is inevitable. You can use feature branches and bridge requests to develop mutual cooperation, making it easier to work with fellow workers on major projects. Consider the use of Husky for pre -Committing Hooks to enforce lunch and testing.

Install Husky:

Install Husky to easily handle the gut hooks, which automatically make functions like lining and testing before you are minimizing.

npm install husky --save-dev

Package.json (Add the script)

These package.json File designated a node. JS sets the project my-mern-appAnd the structures a prepare Script for installing gut hooks using Hasky (V7). It is ready to add pre -committee automation, such as lining or testing.

{

  "name": "my-mern-app",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "prepare": "husky install"

  },

  "keywords": (),

  "author": "",

  "license": "ISC",

  "devDependencies": {

    "husky": "^7.0.0"

  }

}

Make Pre -Committing Hook

The command below has a pre -committe hook that ensures the quality of the code and prevents the errors from entering your code base.

npx husky add .husky/pre-commit "npm test && npm run lint"

Testing: to ensure tightness

Automatic testing is necessary. We will cover the test from the unit, integration, and end to the end.

Back & Testing (Node. JS/Express. JS)

You will use fun for unit testing and superstar for API integration test.

Install them like this:
npm install --save-dev jest supertest

You will use fun to write unit tests for your Javascript code and super test to test HTTP applications against your Express Dot JS API.

For example tests (server/test/ath.test.js):

This test suite uses a superstist to imitate user entry and login for login, emphasizing that the answers have the estimated status codes and features.

const request = require('supertest');

const app = require('../app'); 

describe('Auth API', () => {

  it('should register a new user', async () => {

    const res = await request(app)

      .post('/api/auth/register')

      .send({

        username: 'testuser',

        email: 'test@example.com',

        password: 'password123',

      });

    expect(res.statusCode).toEqual(201);

    expect(res.body).toHaveProperty('_id');

  });


  it('should login an existing user', async () => {

    const res = await request(app)

      .post('/api/auth/login')

      .send({

        email: 'test@example.com',

        password: 'password123',

      });

    expect(res.statusCode).toEqual(200);

    expect(res.headers('set-cookie')).toBeDefined();

  });

});

Front and Testing (Testing Library + Cyprus reaction)

You will use fun and reacting testing library for the unit/integration test, and the Cyprus for the E2E test.

You can install them like this:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest cypress

The reacting testing library will help you test your reaction components, and Cyprus will provide tests from the end of your front and application to the end.

For example component tests (client/SRC/components/button.Test Dot JS):

This unit uses a reacting test library to present the test button component and confirms that the specific text content is present in the output provided.

import React from 'react';

import { render, screen } from '@testing-library/react';

import Button from './Button';


test('renders button with text', () => {

  render(<Button>Click MeButton>);

  const buttonElement = screen.getByText(/Click Me/i);

  expect(buttonElement).toBeInTheDocument();

});

The following Cyprus tests are immersed in the full flow of user verification from registration to login and logout, emphasizing the URL’s expected changes and page content.

Example E2E Test (cypress/e2e/auth.cy.js)

describe('Authentication Flow', () => {

  it('should allow a user to register and login', () => {

    cy.visit('/register');

    cy.get('input(name="username")').type('e2euser');

    cy.get('input(name="email")').type('e2e@example.com');

    cy.get('input(name="password")').type('password123');

    cy.get('button(type="submit")').click();

    cy.url().should('include', '/dashboard');

    cy.contains('Welcome, e2euser');

    cy.get('button').contains('Logout').click();

    cy.url().should('include', '/login');

    cy.get('input(name="email")').type('e2e@example.com');

    cy.get('input(name="password")').type('password123');

    cy.get('button(type="submit")').click();

    cy.url().should('include', '/dashboard');

  });

});

How to build Task Manager

We will create a simple task manager with the verification and CRUD operation of the works so you can see how the whole thing comes together.

Passead enforcement (node. JS/Express. JS)

Dependent

Start by installing our primary backbone libraries: Express for Routing, Mango DB for dialogue, dutin V for environmental variables, sack/jsonweBToken/cookie parster for secure verification, and Safe HTTP header for Helmet:

npm install express mongoose dotenv bcryptjs jsonwebtoken cookie-parser

Server/App.js (Entry Point)

Next, we will set the first or central entry point for the backdrop. This is the Central Express Dot JS application file, which forms the middleware, sets the Mango DB connection, and sets the API route for verification and task management.

const express = require('express');

const mongoose = require('mongoose');

const dotenv = require('dotenv');

const cookieParser = require('cookie-parser');

const helmet = require('helmet');

const authRoutes = require('./routes/authRoutes');

const taskRoutes = require('./routes/taskRoutes');

const { notFound, errorHandler } = require('./middleware/errorMiddleware');

dotenv.config();


const app = express();

app.use(helmet());

app.use(express.json());

app.use(cookieParser());


mongoose.connect(process.env.MONGO_URI)

  .then(() => console.log('MongoDB connected!'))

  .catch(err => console.error('MongoDB connection error:', err));


app.use('/api/auth', authRoutes);

app.use('/api/tasks', taskRoutes);


app.get('/', (req, res) => {

  res.send('MERN Task Manager API is running!');

});


app.use(notFound);

app.use(errorHandler);


const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {

  console.log(`Server running on port ${PORT}`);

});

Server/. ENV

We will add to hard coding secrets, we will add .env File where we can safely store environmental variables, such as our database URI and JWT secrets. This file stores the variables of sensitive environment such as your Mango DB connection string, server port, and JWT secrets, secure them and separates them from their code base.

MONGO_URI=your_mongodb_connection_string_here

PORT=5000

JWT_SECRET=supersecretjwtkey

Server/model/user. Jay

Now, describe your user model using Mongo DB. This scheme includes usernames, email, and password fields, a way to compare pre -sau hooks and passwords for password hanging.

const mongoose = require('mongoose');

const bcrypt = require('bcryptjs');


const UserSchema = new mongoose.Schema({

  username: {

    type: String,

    required: true,

    unique: true,

  },

  email: {

    type: String,

    required: true,

    unique: true,

  },

  password: {

    type: String,

    required: true,

  },

});

UserSchema.pre('save', async function (next) {

  if (!this.isModified('password')) {

    next();

  }

  const salt = await bcrypt.genSalt(10);

  this.password = await bcrypt.hash(this.password, salt);

});


UserSchema.methods.matchPassword = async function (enteredPassword) {

  return await bcrypt.compare(enteredPassword, this.password);

};


module.exports = mongoose.model('User', UserSchema);

Server/model/task. Jay

Next, we will create a task model. This scheme explains the task model, which connects each task to the user and includes fields for title, detail, completion status, and creation time stamp.

const mongoose = require('mongoose');


const TaskSchema = new mongoose.Schema({

  user: {

    type: mongoose.Schema.Types.ObjectId,

    ref: 'User',

    required: true,

  },

  title: {

    type: String,

    required: true,

    trim: true,

  },

  description: {

    type: String,

    trim: true,

  },

  completed: {

    type: Boolean,

    default: false,

  },

  createdAt: {

    type: Date,

    default: Date.now,

  },

});


module.exports = mongoose.model('Task', TaskSchema);

Server/Controllers/Authcontroller.js

Let’s prepare the validation controller. This controller handles the user verification flow, including registration, login, logout, and obtaining user profiles, JWTs and secure HTTP only using cookies.

const User = require('../models/User');

const jwt = require('jsonwebtoken');

const generateToken = (id) => {

  return jwt.sign({ id }, process.env.JWT_SECRET, {

    expiresIn: '1h',

  });

};

exports.registerUser = async (req, res) => {

  const { username, email, password } = req.body;

  try {

    const userExists = await User.findOne({ email });

    if (userExists) return res.status(400).json({ message: 'User already exists' });

    const user = await User.create({ username, email, password });

    if (user) {

      const token = generateToken(user._id);

      res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 3600000 });

      res.status(201).json({ id: user.id, username: user.username, email: user.email });

    } else {

      res.status(400).json({ message: 'Invalid user data' });

    }

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};


exports.loginUser = async (req, res) => {

  const { email, password } = req.body;

  try {

    const user = await User.findOne({ email });

    if (user && (await user.matchPassword(password))) {

      const token = generateToken(user._id);

      res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 3600000 });

      res.json({ id: user.id, username: user.username, email: user.email });

    } else {

      res.status(401).json({ message: 'Invalid email or password' });

    }

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};


exports.logoutUser = (req, res) => {

  res.cookie('token', '', { httpOnly: true, expires: new Date(0) });

  res.status(200).json({ message: 'Logged out successfully' });

};


exports.getUserProfile = async (req, res) => {

  try {

    const user = await User.findById(req.user._id).select('-password');

    if (user) {

      res.json(user);

    } else {

      res.status(404).json({ message: 'User not found' });

    }

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};

Server/Controllers/Task Controller. Jay

The time has come to impose the task controller. It provides logic to recover, create, update and delete controller tasks, ensuring that users can only interact with their tasks.

const Task = require('../models/Task');


exports.getTasks = async (req, res) => {

  try {

    const tasks = await Task.find({ user: req.user._id });

    res.status(200).json(tasks);

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};


exports.createTask = async (req, res) => {

  const { title, description } = req.body;

  if (!title) return res.status(400).json({ message: 'Please add a title' });

  try {

    const task = await Task.create({ title, description, user: req.user._id });

    res.status(201).json(task);

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};


exports.updateTask = async (req, res) => {

  try {

    const task = await Task.findById(req.params.id);

    if (!task) return res.status(404).json({ message: 'Task not found' });

    if (task.user.toString() !== req.user._id.toString()) return res.status(401).json({ message: 'Not authorized' });


    const updatedTask = await Task.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });

    res.status(200).json(updatedTask);

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};


exports.deleteTask = async (req, res) => {

  try {

    const task = await Task.findById(req.params.id);

    if (!task) return res.status(404).json({ message: 'Task not found' });

    if (task.user.toString() !== req.user._id.toString()) return res.status(401).json({ message: 'Not authorized' });


    await Task.deleteOne({ _id: req.params.id });

    res.status(200).json({ message: 'Task removed' });

  } catch (error) {

    res.status(500).json({ message: error.message });

  }

};

Server/Middleware/Authmiddleware.js

To protect private routesFor, for, for,. We will create a middleware that will confirm the JWT from the application cookies, ensuring that only the certified user can access the specific closing points.

const jwt = require('jsonwebtoken');

const User = require('../models/User');

exports.protect = async (req, res, next) => {

  let token;

  if (req.cookies.token) {

    try {

      token = req.cookies.token;

      const decoded = jwt.verify(token, process.env.JWT_SECRET);

      req.user = await User.findById(decoded.id).select('-password');

      next();

    } catch (error) {

      res.status(401).json({ message: 'Not authorized, token failed' });

    }

  } else {

    res.status(401).json({ message: 'Not authorized, no token' });

  }

};

Server/Middleware/Urball Ware. JS

In our background to handle the errors, we will add global error -dealing middleware that can handle 404 unbeaten errors and provide a central error for a permanent API error response.

exports.notFound = (req, res, next) => {

  const error = new Error(`Not Found - ${req.originalUrl}`);

  res.status(404);

  next(error);

};


exports.errorHandler = (err, req, res, next) => {

  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;

  res.status(statusCode);

  res.json({

    message: err.message,

    stack: process.env.NODE_ENV === 'production' ? null : err.stack,

  });

};

Server/Roots/Athroutes.js

Next, let’s explain our verification routes. These closing points enable the user’s verification and map HTTP methods for their respective controller functions.

const express = require('express');

const { registerUser, loginUser, logoutUser, getUserProfile } = require('../controllers/authController');

const { protect } = require('../middleware/authMiddleware');


const router = express.Router();


router.post('/register', registerUser);

router.post('/login', loginUser);

router.get('/logout', logoutUser);

router.get('/profile', protect, getUserProfile);


module.exports = router;

Server/Routes/Task Routs. JS

Now we will include the way to task operations. This file describes the API routes for task management, and applies the Protect Middleware to secure all task -related tasks.

const express = require('express');

const { getTasks, createTask, updateTask, deleteTask } = require('../controllers/taskController');

const { protect } = require('../middleware/authMiddleware');

const router = express.Router();

router.route('/').get(protect, getTasks).post(protect, createTask);

router.route('/:id').put(protect, updateTask).delete(protect, deleteTask);

module.exports = router;

Front and NGO

Dependent

Now, you will need to launch a new reaction project and install your essential libraries: Axios for HTTP applications, reacts to the router for navigation, and reacts to the toast for information display.

npm install axios react-router-dom react-toastify

Client/SRC/Index. JS

Let’s start the front and start the entry point. Here we are presenting the central app component and wrapped it with the Authprovider to provide global certification context.

import React from 'react';

import ReactDOM from 'react-dom/client';

import './index.css';

import App from './App';

import { AuthProvider } from './context/AuthContext';


const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(

  <React.StrictMode>

    <AuthProvider>

      <App />

    AuthProvider>

  React.StrictMode>

);

Client/SRC/app. Jay

Next, we will explain our main app component. This determines the client side routing for application, and describes public and private routes, and includes the navigation bar and toast notification system.

import React from 'react';

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

import { ToastContainer } from 'react-toastify';

import 'react-toastify/dist/ReactToastify.css';


import Navbar from './components/Navbar';

import Register from './pages/Register';

import Login from './pages/Login';

import Dashboard from './pages/Dashboard';

import PrivateRoute from './components/PrivateRoute';


function App() {

  return (

    <Router>

      <Navbar />

      <ToastContainer />

      <div className="container">

        <Routes>

          <Route path="/register" element={<Register />} />

          <Route path="/login" element={<Login />} />

          <Route path="/dashboard" element={<PrivateRoute />}>

            <Route index element={<Dashboard />} />

          Route>

          <Route path="/" element={<h1>Welcome to Task Manager!h1>} />

        Routes>

      div>

    Router>

  );

}

export default App;

Client/SRC/Context/Authcontext.js

We will make a confirmation context that manages global verification state. It provides functions for the user’s login, registration, and logout, and automatically loads the user data at the component mount.

import React, { createContext, useState, useEffect } from 'react';

import axios from 'axios';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {

  const (user, setUser) = useState(null);

  const (loading, setLoading) = useState(true);


  useEffect(() => {

    const loadUser = async () => {

      try {

        const res = await axios.get('/api/auth/profile');

        setUser(res.data);

      } catch (err) {

        setUser(null);

      } finally {

        setLoading(false);

      }

    };

    loadUser();

  }, ());


  const login = async (email, password) => {

    try {

      const res = await axios.post('/api/auth/login', { email, password });

      setUser(res.data);

      return true;

    } catch (err) {

      console.error(err.response.data.message);

      return false;

    }

  };


  const register = async (username, email, password) => {

    try {

      const res = await axios.post('/api/auth/register', { username, email, password });

      setUser(res.data);

      return true;

    } catch (err) {

      console.error(err.response.data.message);

      return false;

    }

  };


  const logout = async () => {

    try {

      await axios.get('/api/auth/logout');

      setUser(null);

    } catch (err) {

      console.error(err);

    }

  };


  return (

    <AuthContext.Provider value={{ user, loading, login, register, logout }}>

      {children}

    AuthContext.Provider>

  );

};


export default AuthContext;

Client/SRC/Components/Navbar.js

Here is a component of a dynamic navigation bar that dynamic links based on the user’s verification status, which shows either the login/register options or the reception message and the logout button.

import React, { useContext } from 'react';

import { Link } from 'react-router-dom';

import AuthContext from '../context/AuthContext';


const Navbar = () => {

  const { user, logout } = useContext(AuthContext);


  return (

    <nav>

      <h1>Task Managerh1>

      <div>

        {user ? (

          <>

            <span>Welcome, {user.username}span>

            <button onClick={logout}>Logoutbutton>

            <Link to="/dashboard">DashboardLink>

          >

        ) : (

          <>

            <Link to="/login">LoginLink>

            <Link to="/register">RegisterLink>

          >

        )}

      

You may also like

Leave a Comment

At Skillainest, we believe the future belongs to those who embrace AI, upgrade their skills, and stay ahead of the curve.

Get latest news

Subscribe my Newsletter for new blog posts, tips & new photos. Let's stay updated!

@2025 Skillainest.Designed and Developed by Pro