Package manager: npm
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 |
git clone https://github.com/NikolaosKantartzopoulos/mern-template.git
- MongoDB Community Server OR
- Atlas Cloud Server
cd backend && npm i && cd ../frontend && npm i && cd ../
cd frontend && npm run dev
cd backend && npm run start
Follow along as I explain how to set this up manually.
mkdir mern-template-sample && cd mern-template-sample && mkdir backend frontend && touch README.md && git init
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
cd backend && touch app.js && npm init -y
// package.json
// ...
"scripts": {
"start": "nodemon app.js",
// ...
},
OPTIONAL: Create controllers, routes and utils folders
mkdir controllers routes utils
npm i --save express mongodb
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
npm i --save-dev body-parser nodemon prettier
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
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 ../
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"
},
npm i --save react react-dom
cd src && mkdir components pages styles && touch app.tsx index.html 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
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
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 ../../
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/*"] }
- "jsx": "react-jsx"
- 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
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
npm i --save-dev prettier && echo '{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}
' > .prettierrc
npm i --save-dev eslint eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser
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
npm i --save-dev webpack webpack-cli webpack-dev-server && touch webpack.config.js
// 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,
},
};
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
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"),
}),
],
};
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"],
},
};
npm i --save-dev css-loader style-loader sass sass-loader postcss postcss-loader postcss-preset-env
echo 'module.exports = {
plugins: [["postcss-preset-env"]],
};' > postcss.config.js
"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"],
},
],
},
};
cd src/styles && echo 'h1 {
color: red;
}
.checkConnectionBox {
display: grid;
gap: 3rem;
align-items: center;
grid-template-columns: repeat(3, 1fr);
}
' > main.scss && cd ../../
// webpack.config.js
// ...
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(jpg|jpeg|png|gif|mp3|svg)$/,
use: ["file-loader"],
},
],
},
};
npm i --save-dev dotenv-webpack
// webpack.config.js
// ...
const Dotenv = require("dotenv-webpack");
module.exports = {
// ...
plugins: [
// ...
new Dotenv(),
],
};
cd src && touch .env && cd ../
4.1 Install JEST
npm i --save-dev jest jest-environment-jsdom @types/jest babel-jest
echo 'module.exports = {
clearMocks: true,
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
testEnvironment: "jsdom",
};' > jest.config.js &&
echo 'import "@testing-library/jest-dom";
' > jest.setup.js
4.2 Install React Testing Library
npm i --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event
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 ../