Skip to content

Instantly share code, notes, and snippets.

@0mppula
Last active June 12, 2022 07:31
Show Gist options
  • Select an option

  • Save 0mppula/ee01d8434e8877f7d7d78f81f348e3a3 to your computer and use it in GitHub Desktop.

Select an option

Save 0mppula/ee01d8434e8877f7d7d78f81f348e3a3 to your computer and use it in GitHub Desktop.

Getting Started with the Backend

Initialize Project

  • Create a directory for your project
  • Initialize your project with npm init
  • Initialize git with git init
  • Create a .gitignore file in your projects root directory
  • Add .env, node_modules and all other possible spec files to .gitignore

Install Dependencies

  • Install express with npm i express
  • Install dotenv with npm i dotenv
  • Install mongoose with npm i mongoose
  • Install colors with npm i colors (optional utility for console highlighting)
  • Install nodemon as a dev dependency with npm i -D nodemon

Package.json

  • In package.json check that all installed dependecies are listed
  • Create start script for your project "start": "node backend/index.js"
  • Create server script for your project "nodemon backend/index.js"

Creating Entry Point

  • In your projects root directory create a new backend folder
  • In backend create the entry point to your server eg. index.js or server.js

Create Basic Server

.env file

  • In projects root directory create .env file for the project
  • Set PORT and NODE_ENV variables in the .env file
    • NODE_ENV = development
    • PORT = 5000

Initialize Server

const dotenv = require('dotenv').config();
const port = process.env.PORT || 5000;

const app = express()

app.listen(port, () => console.log(`Server is running on port: ${port}`))```

Creating the First Route

Create the First Route File

  • Create a folder for the routes in the backend direcory
  • Create a file for routes eg planRoutes.js

Add Simple CRUD Functionality to Route

const router = express.Router()

router.get('/', (req, res) => {
	res.status(200).json({ plan: 'plan_1' });
});

router.post('/', (req, res) => {
	res.status(200).json({ plan: 'posted a new plan' });
});

router.put('/:id', (req, res) => {
	res.status(200).json({ plan: `Updated plan ${req.params.id}` });
});

router.delete('/:id', (req, res) => {
	res.status(200).json({ plan: `Deleted plan ${req.params.id}` });
});

module.exports = router

Update index.js

  • In the index file add the routes of the new routes file
  • app.use('/api/plans', require('./routes/planRoutes'))
  • Now you have routes of a specific category added and organized nicely

Creating the First Route Controller

Create the first Controller File

  • Create a folder for the controllers in the backend direcory
  • Create a file for controller eg planController.js in the controllers directory
  • In the controller directory all the logic of the routes are written

Add Simple CRUD Finctions to Controller file

// @desc    Get Plans
// @route   GET /api/plans
// @access  Private
const getPlans = (req, res) => {
	res.status(200).json({ plan: 'plan_1' });
};

// @desc    Post a Plan
// @route   POST /api/plans
// @access  Private
const postPlan = (req, res) => {
	res.status(200).json({ plan: 'posted a new plan' });
};

// @desc    Update a Plan
// @route   PUT /api/plans/:id
// @access  Private
const updatePlan = (req, res) => {
	res.status(200).json({ plan: `Updated plan ${req.params.id}` });
};

// @desc    Delete a Plan
// @route   DELETE /api/plans/:id
// @access  Private
const deletePlan = (req, res) => {
	res.status(200).json({ plan: `Deleted plan ${req.params.id}` });
};

Add the Constoller Functions to the Routes File

router.route('/').get(getPlans).post(postPlan);
router.route('/:id').put(updatePlan).delete(deletePlan);

Accepting Body Data

  • To accept body data add middleware to index.js
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

Error & Exception Handling

Checking for Field Values

  • Next add error and exception handling to controllers for the first route
const postPlan = (req, res) => {
	if(!req.body.name) {
		res.status(400)
		// Express error handling
		throw new Error('Please provide a name for your plan')
	}
	res.status(200).json({ plan: 'posted a new plan' });;
};

Use Expresses Built-in Error Handling

  • To change Expresses default error handler a middleware function is needed
  • Create a middleware folder inside backend
  • Create a errorMiddleware.js file inside the middleware folder
  • Create a function that overrides the default error handler
const errorHandler = (err, req, res, next) => {
	const statusCode = res.statusCode ? res.statusCode : 500;

	res.status(statusCode);
	res.json({
		message: err.message,
		stack: (process.env.NODE_ENV = 'production' ? null : err.stack),
	});
};

module.exports = {
	errorHandler,
};
  • Import and use the new error handler in index.js
const { errorHandler } = require('./middleware/errorMiddleware');
// Under the routes
app.use(errorHandler);

Make Controllers Asynchronous

Install express-async-handler

  • When using Mongoose in controller functions to interact with the database you get back a promise
  • To use the custom error handler instead of using try catch with asynchronous controller functions install express-async-handler with npm i express-async-handler

Converting Controller functions to Asynchronous Functions

  • Import express-async-handler in controller file
const asyncHandler = require('express-async-handler');
  • Now make all controller functions asynchronous and wrap them with the imported express-async-handler
const postPlan = asyncHandler(async (req, res) => {
	if (!req.body.name) {
		res.status(400);
		throw new Error('Please provide a name for your plan');
	}
	res.status(200).json({ plan: 'posted a new plan' });
});

MongoDB Database

Creating a MongoDB Database for project

Add the MongoDB Connection String to the .env File

MONGO_URI = mongodb+srv://<username>:<password>@<cluster>

Connecting Project with the MongoDB Database

  • Create a config folder inside backend for configuring the database
  • Create db.js file in the config directory
  • Create a function that connects with database
const mongoose = require('mongoose');
const connectDB = async () => {
	try {
		const conn = await mongoose.connect(process.env.MONGO_URI);

		console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
	} catch (error) {
		console.log(error);
		process.exit(1);
	}
};
module.exports = connectDB;
  • Finally import the function in index.js

Create First Model

  • Create a models folder inside backend
  • Create a planModel.js file (model name corresponding with the projects resourse in question) inside the models folder
  • Create first schema inside planModel.js
const mongoose = require('mongoose');
const planSchema = mongoose.Schema(
	{
		name: {
			type: String,
			required: [true, 'Please name your plan'],
		},
	},
	{
		timestamps: true,
	}
);
module.exports = mongoose.model('Plan', planSchema);

Adding the Model to Controllers

  • Import the new model and use it in its controller file
const Plan = require('../models/planModel');

const postPlan = asyncHandler(async (req, res) => {
	if (!req.body.name) {
		res.status(400);
		throw new Error('Please provide a name for your plan');
	}

	const plan = await Plan.create({
		name: req.body.name,
	});

	res.status(200).json(plan);
});

Create the Second User Model

  • Create a usersModel.js file inside the models folder
  • Create schema for users inside usersModel.js
const mongoose = require('mongoose');
const userSchema = mongoose.Schema(
	{
		username: {
			type: String,
			required: [true, 'Please add a name'],
		},
		email: {
			type: String,
			required: [true, 'Please add an email'],
			unique: true,
		},
		password: {
			type: String,
			required: [true, 'Please add a password'],
		},
	},
	{
		timestamps: true,
	}
);

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

Associate the First Model with the Users Model

const planSchema = mongoose.Schema(
	{
		user: {
			type: mongoose.Schema.Types.ObjectId,
			required: true,
			ref: 'User',
		},
		...
	},
);

Creating the User Routes and Controllers

User Routes

  • Create a file for the user routes userRoutes.js in routes
  • Use the new routes in index.js
app.use('/api/users', require('./routes/userRoutes'));

User Controllers

  • Create a file for the user controllers userControllers.js in controllers
  • In the user controller file create the controller functions and export them
  • Import the new controller functions in the corresponding routes file

Users Controller functions

  • When registering new users, their passwords need to be encrypted

Install the Dependencies for Password Encryption

  • Install bcryptjs with npm i bcryptjs
  • Install jsonwebtoken with npm i npm i jsonwebtoken

Import the Dependencies for the User Controller Functions

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const asyncHandler = require('express-async-handler');

const user = require('../models/userModel');

Register User Controller Function

// @desc    Register a new user
// @route   POST /api/users/
// @access  Public
const registerUser = asyncHandler(async (req, res) => {
	const { username, email, password } = req.body;

  // Check fields
	if (!username || !email || !password) {
		throw new Error('Please add all fields');
	}

	// Check if the user exists
	const userExists = await User.findOne({ email });

	if (userExists) {
		res.status(400);
		throw new Error('User already exists');
	}

	// Hash password
	const salt = await bcrypt.genSalt(10);
	const hashedPassword = await bcrypt.hash(password, salt);

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

  // check if user is created successfully
	if (user) {
		res.status(201).json({
			_id: user._id,
			username: user.username,
			email: user.email,
		});
	} else {
		res.status(400);
		throw new Error('Invalid user data');
	}
});

Login User Controller Function

  • To authenticate a user get the email and password of the user from the body data
  • Check if user with given username exists on server
  • Check if given password matches the encrypted password on the server
  • Handle send user data on success and handle error on error
const loginUser = asyncHandler(async (req, res) => {
	const { email, password } = req.body;

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

	if (user && await bcrypt.compare(password, user.password)) {
		res.status(201).json({
			_id: user._id,
			username: user.username,
			email: user.email,
		});
	} else {
		res.status(400);
		throw new Error('Invalid credentials');
	}
});

Generate JSON Web token

  • The register and login user controller functions need a JSON web token for user authorization

Secret

  • To sign a JWT a secret is needed.
  • In the .env file add a secret variable eg. JWT_SECRET = abc123

Generate JWT Function

const generateToken = (id) => {
	return jwt.sign({ id }, process.env.JWT_SECRET, {
		expiresIn: '30d',
	});
};
  • Add the token to login and register controller functions retruns token: generateToken(user._id),

Protecting Routes With Authentication Middleware

  • Create a new authMiddleware.js file in the middleware folder
  • Import jwt, asyncHandler and User in authMiddleware.js

Creating the Protect Function

  • Get the token from the request header
  • Verify the token with jwt
  • Get the user from the token
const protect = asyncHandler(async (req, res, next) => {
	let token;

	// Check if the http header of the request contains an authorization object that starts with
	// "Bearer" the token is send in the authorization header like this "Bearer" "token"
	if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
		try {
			// Get the token from the http authorization header
			token = req.headers.authorization.split(' ')[1];

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

			// Re-assingn req.user with the id from the token excluding the password
      		// Now req.user is available in other controller functions that user the same token
			req.user = await User.findById(decoded.id).select('-password');

			// Call the next piece of middleware
			next();
		} catch (error) {
			console.log(error);
			res.status(401);
			throw new Error('Not authorized');
		}
	}

	if (!token) {
		res.status(401);
		throw new Error('Not authorized not token');
	}
});

Adding the Protect Function to Private Routes

User Routes

  • Import the route protector function in userRoutes
  • Protect the private routes by adding the protector function as the first argument
const { protect } = require('../middleware/authMiddleware');

router.get('/me', protect, getUserData);

Plan Routes

  • Import the route protector function in planRoutes
  • Protect the private routes by addding the protector function as the first argument
const { protect } = require('../middleware/authMiddleware');

router.route('/').get(protect, getPlans).post(protect, postPlan);
router.route('/:id').put(protect, updatePlan).delete(protect, deletePlan);

Get Data From Protected Routes

User Routes

  • Get only the logged in users data
const getUserData = asyncHandler(async (req, res) => {
	const { _id, username, email } = await User.findById(req.user.id);

	res.status(200).json({
		_id,
		username,
		email,
	});
});

Plan Routes

  • Get only the logged in users Plans
const getPlans = asyncHandler(async (req, res) => {
	const plans = await Plan.find({ user: req.user.id });

	res.status(200).json(plans);
});
  • Add user field to new posted plans to ensure there is a relationship between plan and user
const postPlan = asyncHandler(async (req, res) => {
	if (!req.body.name) {
		res.status(400);
		throw new Error('Please provide a name for your plan');
	}

	const plan = await Plan.create({
		user: req.user.id,
		name: req.body.name,
	});

	res.status(200).json(plan);
});
  • Update & delete only own plans
  • Import the User model
  • Check if the given user exists
  • Check if the user id matches the resource id
// Get user from database
const user = await User.findById(req.user.id);

// Check if user exists
if (!user) {
	res.status(401);
	throw new Error('User not found');
}

// Make sure logged in user matches the plans user
if (plan.user.toString() !== user.id) {
	res.status(401);
	throw new Error('User not authorized');
}

Getting Started with the Frontend

Initialize Project

  • Initialize the react frontend with the redux template with npx create-react-app frontend --template redux
  • Add a script to run the frontend in the package.json file in the root directory of the project "client": "npm start --prefix frontend"
  • Delete all the unwanted files from the redux template in frontend
  • Clear up the default App.js file as well as the store.js file in the app directory

Install Dependencies

  • Install react-router-dom with npm i react-router-dom
  • Install react-icons with npm i react-icons

Creating the First Pages

  • In frontend create a pages folder to store the pages of the app
  • Create some .jsx pages in the pages directory
  • Import the pages in App.js and setup basic routing for the them

Creating the Nav Component

  • In frontend create a components folder to store the components of the app
  • Create a nav component in the components directory
  • In the nav component, create a navbar and add some links to the routes of the app with the Link tag
  • Import the nav component in App.js

Creating the Register and Login Forms

  • Create a simple form with fields for username, email, password and confirm password
  • The register form can be largely re-used when creating the login page so copy and paste it there
  • Modify the login forms code so its ready to take in the users login information

Concurrently setup

  • To run the client and the server concurrently with the same script, the concurrently package is needed
  • Install concurrently with npm i concurrently
  • Create the script for running the client and the server concurrently in the package.json found in the root directory
  • "dev": "concurrently \"npm run server\" \"npm run client\" "

Redux

Authentication State setup

  • To handle the user authentication state a slice file is needed
  • Create a features directory in frontend
  • Create a auth directory in features
  • Create a authSlice.js file in the auth directory

First Slice File

  • Import createSlice and createAsyncThunk from redux-toolkit in authSlice.js
  • Get the user token from localStorage
  • Create the initial state object for the authentication state
const user = JSON.parse(localStorage.getItem('user'));

const initialState = {
	user: user ? user : null,
	isError: false,
	isSuccess: false,
	isLoading: false,
	message: '',
};
  • Create the slice object
export const authSlice = createSlice({
	name: 'auth',
	initialState,
	reducers: {
		reset: (state) => {
			state.isError = false;
			state.isSuccess = false;
			state.isLoading = false;
			state.message = '';
		},
	},
	extraReducers: () => {},
});
  • Export the slice with its actions
export const { reset } = authSlice.actions;
export default authSlice.reducer;
  • Import and add the slice in store.js file found in src/app
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';

export const store = configureStore({
	reducer: {
		auth: authReducer,
	},
});
  • Check the developer console with the redux extension installed for the newly created state

User Registeration with Redux State

Setup

  • Now that the initial state for user authentication is setup. It's time to create a user registeration AsyncThunk function in authSlice.js
  • Create a authService.js file in the auth directory for the http requests needed in the asyncThunk functions of authSlice
  • Install axios for making http calls with npm i axios
  • Install react-toastify for alerts with npm i react-toastify (optional)

Creating the Registeration Asynchronous Thunk Function

  • Create the user registration function in authSlice
export const register = createAsyncThunk('auth/register', async (user, thunkAPI) => {
	try {
		return await authService.register(user);
	} catch (error) {
		const message =
			(error.response && error.response.data && error.response.data.message) ||
			error.message ||
			error.toString();
		return thunkAPI.rejectWithValue(message);
	}
});
  • The register function in authSlice now calls the register function in authService which should make the apropriate http request
  • Create the register function in authService
  • Export the function and import it in authSlice
import axios from 'axios';
const API_URL = '/api/users/';

// Register user
const register = async (userData) => {
	const response = await axios.post(API_URL, userData);
	if (response.data) {
		localStorage.setItem('user', JSON.stringify(response.data));
	}
	return response.data;
};

const authService = {
	register,
};

export default authService;
  • To ensure axios http calls are made to the correct url add a proxy in the package.json file in the frontend "proxy": "http://localhost:5000/",

Adding Extra Reducers for Register

  • To account for the pending, fulfilled and rejected states the redux slice object needs to have extra reducers
  • Add extraReducers function to the authSlice object
  • The extraReducers takes in builder as a parameter, add the different cases to builder
  • The added cases handle the auth state during the added cases
export const authSlice = createSlice({
	name: 'auth',
	initialState,
	reducers: {
		...
	},
	extraReducers: (builder) => {
		builder
			.addCase(register.pending, (state) => {
				state.isLoading = true;
			})
			.addCase(register.fulfilled, (state, action) => {
				state.isLoading = false;
				state.isSuccess = true;
				state.user = action.payload;
			})
			.addCase(register.rejected, (state, action) => {
				state.isLoading = false;
				state.isError = true;
				state.message = action.payload;
				state.user = null;
			});
	},
});

Integrate the Auth Slice with the Register Form

Setup

  • Import the reset and register functions from the authSlice
  • Import useSelector useDispatch in the register form
  • The useSelector is used to acces the redux state
  • The useDispatch is used to call redux functions
  • Import useNavigate for redirecting users on successful register
  • Import ToastContainer in App.js for toastified alerts (optional)
  • Initialize dispatch and navigate
  • Select the states needed from authSlice
const dispatch = useDispatch();
const navigate = useNavigate();
const { user, isLoading, isError, isSuccess, message } = useSelector((state) => state.auth);

Building the Submit Logic

  • Handle form validation in the onSubmit function
  • Dispatch the register function with the users data in the onSubmit function of the register page
dispatch(register(userData));

React to Redux State with useEffect

  • Create a useEffect and add the redux state dependencies used in the register page eg. isError user
  • Check for errors and add a prompt (toast) for the user with the message from redux state
  • Check for successful registeration of user and handle consequent navigations
  • Lasty dispatch the reset function at the end of useEffect to reset any potentially irrelevant redux state

Logout the User

Creating the Logout Function

  • To logout the user just destroy the users token from localStorage
  • Create an asynchronous thunk logout function in authSlice that calls a logout service in authService
export const logout = createAsyncThunk('auth/logout', async () => {
	await authService.logout();
});
  • Create and export a logout function in authService
const logout = async () => {
	localStorage.removeItem('user');
};

Adding reducers to the Logout Function

  • Reducers help manage the state and keep it up to date
  • Add a fulfilled reducer case for the logout function and set the user to null

Dispatching the Logout Function

  • The logout functionality is used in the navbar
  • Import the logout and reset functions from authSlice
  • Import useSelector, useDispatch and useNavigate
  • Initialize navigate and dispatch
  • Add user state conditional rendering to the navbar
  • If user is logged in add an element that can call the logout function from authSlice
  • Handle the subsequent navigation of the user and reset the redux state in the logout function called from the logout element

Login the User

Creating the Login Function

  • Create an asynchronous thunk login function in authSlice that calls a login service in authService
export const login = createAsyncThunk('auth/login', async (userData, thunkAPI) => {
	try {
		return await authService.login(userData);
	} catch (error) {
		const message =
			(error.response && error.response.data && error.response.data.message) ||
			error.message ||
			error.toString();
		return thunkAPI.rejectWithValue(message);
	}
});
  • Create and export a login function in authService
const login = async (userData) => {
	const response = await axios.post(`${API_URL}login`, userData);
	if (response.data) {
		localStorage.setItem('user', JSON.stringify(response.data));
	}
	return response.data;
};

Adding reducers to the Login Function

  • Reducers help manage the state and keep it up to date
  • Add a pending, fulfilled and rejected extra reducer cases for the login function

Dispatching the Login Function

  • The login function is used in the login page
  • Import the reset and login functions from authSlice
  • Import useSelector, useDispatch and useNavigate
  • Initialize dispatch and navigate
  • Select the states needed from authSlice

Building the Submit Logic

  • Handle form validation in the onSubmit function
  • Dispatch the login function with the users data in the onSubmit function of the login page

React to Redux State with useEffect

  • Create a useEffect and add the redux state dependencies used in the login page eg. isError user
  • Check for errors and add a prompt (toast) for the user with the message from redux state
  • Check for successful login of user and handle consequent navigations
  • Lasty dispatch the reset function at the end of useEffect to reset any potentially irrelevant redux state

Restrict Logged Out Users From the Homepage

  • In this scenario users are not allowed on this page (homepage), and they should be redirected to the login page
  • Import useSelector, useEffect and useNavigate
  • Initialize navigate
  • Select the states needed from authSlice
  • Create a useEffect that checks if the user is logged in and redirect logged out users in it

Deployment

Pointing the Server to the Static Files of the App

// Serve client
if (process.env.NODE_ENV === 'production') {
	app.use(express.static(path.join(__dirname, '../frontend/build')));

	app.get('*', (req, res) => {
		res.sendFile(path.resolve(__dirname, '../', 'frontend', 'build', 'index.html'));
	});
} else {
	app.get('/', (req, res) => {
		res.send('The application in not in production');
	});
}

Deploying to Heroku

  • Install the Heroku CLI
  • Run heroku Login
  • Create a Heroku app with heroku create <app_name>
  • Add the projects environmental variables in Heroku eg. NODE_ENV
  • Write a Heroku post build script in the package.json file of the root directory: "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend"
  • Push to Heroku with git push heroku master
  • Open app with heroku open
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment