Skip to content

Latest commit

 

History

History
792 lines (607 loc) · 16 KB

README.md

File metadata and controls

792 lines (607 loc) · 16 KB

MERN stack template

Package manager: npm

Backend - NodeJS + Express + MongoDB

using body-parser, nodemon, dotenv

MongoDB - Use either with Atlas Cloud or Local Community Server


Frontend - React + Typescript

Use Tool
CSS Pre-processor Sass
CSS Post-processor postcss-loader
Bundler Webpack
Transpiler Babel
Linter ESLint
Testing JEST + React Testing Library
Env. Variables dotenv-webpack

Either...

1. Clone repo

git clone https://github.com/NikolaosKantartzopoulos/mern-template.git

2. Create .env file

2.1 Use .env.example files as a reference

2.2 Be sure to update FRONTEND_SERVER_URL at backend/.env file for CORS headers


3. Setup Database

3.1 cd to ./backend/utils/databaseUI.js

3.2 Comment in/out preferable options

  • MongoDB Community Server OR
  • Atlas Cloud Server

4. Install packages

cd backend && npm i && cd ../frontend && npm i && cd ../

5. Use stack

5.1 cd in frontend/

cd frontend && npm run dev

5.2 cd in backend/

cd backend && npm run start

or...

Follow along as I explain how to set this up manually.

1. Create project folder structure, git init

mkdir mern-template-sample && cd mern-template-sample && mkdir backend frontend && touch README.md  && git init

1.1 .gitignore

echo '# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history
.DS_Store

# Enviroment variables
.env
.env.local


#Excluding build directory
#public/
dist/
' > .gitignore

2. Backend

cd backend && touch app.js && npm init -y

2.0.1 Add the following script at package.json

// package.json
// ...
  "scripts": {
    "start": "nodemon app.js",
// ...
},

OPTIONAL: Create controllers, routes and utils folders

mkdir controllers routes utils

2.1 Install Express and MongoDB

npm i --save express mongodb

2.1.1

echo 'const express = require("express");
const { connectClient } = require("./utils/databaseUI");

const bodyParser = require("body-parser");

const app = express();

app.use(bodyParser.json());

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", process.env.FRONTEND_SERVER_URL);
  res.setHeader(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept, Authorization"
  );
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE");
  next();
});

app.get("/", (req, res) => {
  res.json({ name: "Server works!!!" });
});

connectClient()
  .then(
    app.listen(5000, () => {
      console.log("Database connected...");
      console.log(`Server listening on port 5000...`);
    })
  )
  .catch((err) => console.log(err));
' > app.js

2.2 Basic utilities

npm i --save-dev body-parser nodemon prettier

2.2.1 Prettier config file

echo 'module.exports = {
  trailingComma: 'es5',
  tabWidth: 2,
  semi: true,
  singleQuote: false,
};' >  prettier.config.js

2.3 Install dotenv

for environment variables and create .env file

npm i --save dotenv && echo 'DATABASE_HOST=localhost:27017
DATABASE_USERNAME=sampleUsername
DATABASE_PASSWORD=samplePassword
CLUSTER_NAME=cluster0
CLUSTER_ID=sampleID
FRONTEND_SERVER_URL=http://localhost:3000' > .env

2.4 Setup databaseUI utils

cd utils && echo 'require("dotenv").config();
const { MongoClient } = require("mongodb");

/*****************************************************
 *  CHOOSE LOCAL SERVER OR MONGODB ATLAS CONNECTION
 *****************************************************/
async function connectClient() {
  // Local server
  let client = new MongoClient(`mongodb://${process.env.DATABASE_HOST}`);

  // MongoDB Atlas Connection - UPDATE .env
  // let client = new MongoClient(
  //   `mongodb+srv://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.CLUSTER_NAME}.${process.env.CLUSTER_ID}.mongodb.net/?retryWrites=true&w=majority`
  // );
  return client;
}

async function connectDatabase(databaseName) {
  let client = await connectClient();
  let db = client.db(databaseName);
  return { client, db };
}

module.exports = { connectClient, connectDatabase };
' > databaseUI.js && cd ../

3. Frontend

cd ../frontend && mkdir src public && cd public && touch main.scss && cd ../ && npm init -y

3.0.1 Use the following scripts at package.json

// package.json
// ...
  "scripts": {
    "build": "webpack --mode=production --config webpack.config.js",
    "dev": "webpack-dev-server --mode development",
    "lint": "eslint ./src",
    "lint:fix": "eslint . --fix",
    "test": "jest"
  },

3.1 React

npm i --save react react-dom

3.1.1 Folder structure

cd src && mkdir components pages styles && touch app.tsx index.html index.tsx

3.1.2 index.tsx

echo 'import { createRoot } from "react-dom/client";
import App from "./app";

const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(<App />);' > index.tsx

3.1.3 app.tsx

echo 'import React from "react";
import Home from "./pages/home";
import "./styles/main.scss";

const App = () => {
  return (
    <div className="App">
      <Home />
    </div>
  );
};

export default App;' > app.tsx

3.1.4 Create Home Page

cd pages && echo 'import React, { useState } from "react";

function Home() {
  const [data, setData] = useState(null);
  const [asyncTestingActive, setAsyncTestingActive] = useState(false);

  function buttonHandler() {
    fetch("http://localhost:5000/")
      .then((res) => res.json())
      .then((data) => {
        setData(data.name);
      });
  }

  function checkAsyncTesting() {
    setAsyncTestingActive((prev) => !prev);
  }

  return (
    <div>
      <h1>Sass works if these letters are red!</h1>
      <div className="checkConnectionBox">
        Check Server Connection
        <button onClick={buttonHandler}>Check server!</button>
        <p>{data ? data : "..."}</p>
      </div>
      <div className="checkConnectionBox">
        <p>Check Async Testing</p>
        <button onClick={checkAsyncTesting}>Check Test!</button>
        <p>{!asyncTestingActive ? "..." : "Async testing works!!!"}</p>
      </div>
      <div className="checkConnectionBox">
        <p>Environment variables</p>
        <p style={{ textAlign: "center" }}> --- </p>
        <p>{process.env.testEnv}</p>
      </div>
    </div>
  );
}

export default Home;
' > home.tsx && cd ../../

3.2 Typescript

Install Typescript and include types for React library

npm i --save-dev typescript @types/react @types/react-dom

OPTIONAL: manually create a config file for ts, or use the one I provide...

npx tsc --init

Our ts.config.json file will have two parts: Compiler Options and the Include path

  • compilerOptions: (honorary mentions)
    • "jsx": "react-jsx"
      • Controls how JSX constructs are emitted in JavaScript files. This only affects output of JS files that started in .tsx files.
      • react-jsx: Emit .js files with the JSX changed to _jsx calls.
    • "baseUrl": "src",
      • Sets up paths relative to this.
    • "paths": { "@components/*": ["components/*"], "@pages/*": ["pages/*"], "@styles/*": ["styles/*"] }
  • include: "src/" directory
    • will be the only place with Typescript in our frontend project.

That being said:

echo '{
  "compilerOptions": {
    "target": "es2015",
    "lib": ["dom", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "jsx": "react-jsx",
    "baseUrl": "src",
    "paths": {
      "@components/*": ["components/*"],
      "@pages/*": ["pages/*"],
      "@styles/*": ["styles/*"]
    }
  },
  "include": ["./src/**/*"]
}' > tsconfig.json

3.3 Babel

Use Babel to compile our Typescript code to Javascript and then to "older-browser-friendly" code.

npm i --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/preset-typescript

Create and write babel.config.json file. It will "work" in reverse order (right to left), so typescript should be compiled to js/jsx and js/jsx to pre ES6 Javascript.

echo '{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}' > babel.config.json

3.4 Formatting: Prettier

npm i --save-dev prettier && echo '{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}
' > .prettierrc

3.5 Linter: ESLint

npm i --save-dev eslint eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser

3.5.1 ESLint config file

echo '{
  "env": {
    "browser": true,
    "es2022": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": ["react", "@typescript-eslint"],
  "settings": {
    "react": {
      "version": "999.999.999"
    }
  },
  "rules": {
    "react/react-in-jsx-scope": "off",
    "react-hooks/rules-of-hooks": "error",
    "max-len": ["warn", { "code": 80 }]
  }
}' > .eslintrc.json

3.6 Webpack

npm i --save-dev  webpack webpack-cli webpack-dev-server && touch webpack.config.js

3.6.1 Entry and output

// webpack.config.js
const path = require("path");

module.exports = {
	entry: "./src/index.tsx",
	output: {
		filename: "main.js",
		path: path.resolve(__dirname, "dist"),
		clean: true,
	},
	devServer: {
		static: {
			directory: path.resolve(__dirname, "dist"),
		},
		hot: true,
		open: true,
		port: 3000,
	},
};

3.6.2 html-webpack-plugin && index.html

This is the index file used by html-webpack-plugin to create a template. Includes a div with id="root" where React works its magic!

3.6.2.1 Install html-webpack-plugin

npm i --save-dev html-webpack-plugin

3.6.2.2 Create index.html file

cd src && echo '<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sample Title from index.html</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>' > index.html && cd ../

Favicon use is commented out.
If you want to use a favicon comment the line in and add the file at the appropriate path

// webpack.config.js
// ...
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
	// ...
	plugins: [
		new HtmlWebpackPlugin({
			template: path.resolve(__dirname, "./src/index.html"),
			//favicon: path.resolve(__dirname, "./public/images/favicon.ico"),
		}),
	],
};

3.6.3 Webpack Typescript configuration

ts-loader - Typescript loader for Webpack

npm i --save-dev ts-loader
// webpack.config.js
// ...
module.exports = {
	// ...
	module: {
		rules: [
			{
				test: /\.(ts|tsx)$/,
				use: "ts-loader",
				exclude: /node_modules/,
			},
		],
	},
	resolve: {
		extensions: [".tsx", ".ts", ".js"],
	},
};

3.6.4 Webpack CSS configuration

3.6.4.1 Install packages

npm i --save-dev css-loader style-loader sass sass-loader postcss postcss-loader postcss-preset-env

3.6.4.2 Postcss config file

echo 'module.exports = {
  plugins: [["postcss-preset-env"]],
};' > postcss.config.js

3.6.4.3 Update webpack config file

"use:" is read in reverse order by webpack

// webpack.config.js
// ...
module.exports = {
	// ...
	module: {
		rules: [
			// ...
			{
				test: /\.(css|scss)$/,
				use: ["style-loader", "css-loader", "sass-loader", "postcss-loader"],
			},
		],
	},
};

3.6.4.4 Create Sass main file

cd src/styles && echo 'h1 {
  color: red;
}

.checkConnectionBox {
  display: grid;
  gap: 3rem;
  align-items: center;
  grid-template-columns: repeat(3, 1fr);
}

' > main.scss && cd ../../

3.6.5 Webpack file-loader rules

// webpack.config.js
// ...
module.exports = {
	// ...
	module: {
		rules: [
			// ...
			{
				test: /\.(jpg|jpeg|png|gif|mp3|svg)$/,
				use: ["file-loader"],
			},
		],
	},
};

3.7 Environment variables

3.7.1 Install dotenv-webpack

npm i --save-dev dotenv-webpack

3.7.2 Update webpack config

// webpack.config.js
// ...
const Dotenv = require("dotenv-webpack");

module.exports = {
	// ...
	plugins: [
		// ...
		new Dotenv(),
	],
};

3.7.3 Create .env file

cd src && touch .env && cd ../

4 Testing

4.1 Install JEST

npm i --save-dev jest jest-environment-jsdom @types/jest babel-jest

4.1.1 jest.config.js && jest.setup.js

echo 'module.exports = {
  clearMocks: true,
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
  testEnvironment: "jsdom",
};' > jest.config.js &&
echo 'import "@testing-library/jest-dom";
' > jest.setup.js
npm i --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event

4.3 Create Basic Tests

mkdir __tests__ && cd __tests__ && echo 'import React from "react";
import { screen, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Home from "../src/pages/home";

test("Home page initial render", async () => {
  const user = userEvent.setup();
  render(<Home />);

  expect(
    screen.getByText("Sass works if these letters are red!")
  ).toBeInTheDocument();

  const asyncTestingButton = screen.getByRole("button", {
    name: /Check Test!/i,
  });
  await user.click(asyncTestingButton);

  expect(screen.getByText(/Async testing works!!!/i)).toBeInTheDocument();
});' > home.test.js && cd ../

5 Any comments will be greatly appreciated!