In today’s digital world, user authentication is a crucial aspect of web development. Whether you’re building a simple blog or a complex web application, implementing a secure login system is essential to protect user data and ensure a seamless user experience. In this tutorial, we’ll walk through the process of building a login application using three powerful technologies: React for the frontend, Node.js for the backend, Typescript for JavaScript with static typing, improving code safety and maintainability, and PostgreSQL for the database from beginner to advanced level.
Before we dive into the implementation, it is always important to understand the underlined concepts before a deeper dive. Let’s briefly discuss each technology’s role in our application:
Before we can start coding our application, we need to set up our development environment. Follow these steps:
Install Node.js and npm (Node Package Manager) on your system.
- On Mac Os as of this posting
# installs NVM (Node Version Manager) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # download and install Node.js nvm install 21 # verifies the right Node.js version is in the environment node -v # should print `v21.7.3` # verifies the right NPM version is in the environment npm -v # should print `10.5.0`
Or on windows
# download and install Node.js choco install nodejs --version="21.7.3" # verifies the right Node.js version is in the environment node -v # should print `v21.7.3` # verifies the right NPM version is in the environment npm -v # should print `10.5.0`
Start a New React Project - React
The library for web and native user interfacesreact.dev
For this project, we’ll be using plain ReactJS.
I. Open a Terminal or Command Prompt:
Ensure you have Node.js installed, then navigate to your desired project directory — assuming you have a project directory called Projects.
cd projects; mkdir loginapp; cd loginapp; mkdir frontend backend;
II. Create Your React App:
Navigate to the frontend folder. Run the following command in your terminal:
cd frontend; npx create-react-app --template typescript .
You’re now ready to start working on your React project in typescript.
III. Add Eslint to the project by running the following command:
npm i -g eslint
npx eslint --init
Follow the prompt to set up linting for your project:
You can also run this command directly using 'npm init @eslint/config'. ✔ How would you like to use ESLint? · problems ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · react ✔ Does your project use TypeScript? · No / Yes ✔ Where does your code run? · browser ✔ What format do you want your config file to be in? · JSON Local ESLint installation not found. The config that you've selected requires the following dependencies:
@typescript-eslint/eslint-plugin@latest eslint-plugin-react@latest @typescript-eslint/parser@latest eslint@latest ✔ Would you like to install them now? · No / Yes ? Which package manager do you want to use? … npm ❯ yarn pnpm4. Create UI components for the login/register form:
Now you should be able to add your rules to your project in the .eslintrc.json file
{ "env": { "browser": true, "es2021": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": ["@typescript-eslint", "react"], "rules": { "no-console": "warn", "indent": ["error", 2], "quotes": ["error", "double"], "semi": ["error", "always"] } }
Note:
Ensure you’ve selected all the options that match your project’s needs and requirements, as demonstrated above. You should be able to set your rule to match your project needs as demonstrated above by editing the .eslintrc.json file. You should do the same for the backend.
I. Open Terminal or Command Prompt: Launch your terminal or command prompt to initialise your backend server
cd projects; cd loginapp; cd backend; npm init -y
II. Let's create a requirement to list all our dependencies:
Listing dependencies ensures effective management, reproducibility, and security of the project. It aids in tracking external libraries, version control, and mitigating security risks by documenting all required dependencies and their versions.
touch requirements.txt
III. Add the following libraries to your requirements.txt:
for simplicity, we will use the latest instead of listing all versions for the libraries
express@latest @types/express@latest nodemon@latest chalk@3.0 dotenv@latest @types/dotenv@latest jsonwebtoken@latest @types/jsonwebtoken@latest cors@latest @types/cors@latest morgan@latest @types/morgan@latest pg @types/pg bcryptjs @types/bcryptjs
Note:
remember you don’t need to list all the libraries on the go but whenever you will need a new library you just need to update your requirement.txt file and re-run installation as it comes in the next parts:
IV: Adding typescript to the nodejs backend server and initializing typescript with the following command.
npm i typescript ts-node @types/node --save-dev; npx tsc --init
Now you should have a tsconfig.json as above. Replace its content with the following:
{ "compilerOptions": { "module": "commonjs", "noImplicitReturns": true, "noUnusedLocals": true, "outDir": "lib", "sourceMap": true, "strict": true, "target": "es2017", "esModuleInterop": true }, "compileOnSave": true, "include": ["src/**/*"], "exclude": ["node_modules", "lib"] }
V. Install the dependencies:
ensure that you are in the terminal for the backend and run the following command:
npm i $(cat requirements.txt )
Here’s a snapshot of the installed dependencies captured from the package.json file.
VI. Open the package.json file and update the script as follows:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon src/index.ts" },
VII. Add Eslint to the project by running the following command:
npm i -g eslint
npx eslint --init
Follow the prompt to set up linting for your project:
You can also run this command directly using 'npm init @eslint/config@latest'. Need to install the following packages: @eslint/create-config@1.0.3 Ok to proceed? (y) y ✔ How would you like to use ESLint? · problems ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · none ✔ Does your project use TypeScript? · typescript ✔ Where does your code run? · node The config that you've selected requires the following dependencies: eslint, globals, @eslint/js, typescript-eslint ✔ Would you like to install them now? · No / Yes ? Which package manager do you want to use? … npm ❯ yarn pnpm bun
Now you should be able to add your rules to your project in the .eslintrc.json or eslint.config.mjs file
{ "env": { "es2021": true, "node": true }, "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": ["@typescript-eslint"], "rules": { "no-console": "warn", "quotes": ["error", "double"], "semi": ["error", "always"] } }
Note:
If you encounter an error with your tsconfig.json file because you initialized esnlit and got file eslint.config.mjs, just remove that file and create .eslintrc.json and add the content above.
Before diving into coding, let’s tidy up our frontend project to ensure a clean folder structure.
I. Cleaning up index.hmtl file and app.ts
Replace the existing index.html file in the public folder with the following code.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>CodeGenitor</title> </head> <body> <div id="root"></div> </body> </html>
replace the App.ts with the following code:
import React from "react"; const App = () => { return <div>CodeGentitor</div>; }; export default App;
Congrats on tackling the first challenge! Stuck? No worries; drop a comment for a helping hand and get the passcode to unlock the next level!
I. Organize your frontend src directory: Create separate folders for ‘pages’ and ‘components’ to enhance project structure.
cd src; mkdir pages components
Note:
You can create the folders manually or by using the CLI. Make sure you’re in the frontend folder in your terminal as demonstrated above.
II. Install Bootstrap: Enhance development speed by installing a UI framework like React Bootstrap for accelerated application building.
yarn add react-bootstrap bootstrap
You can visit react bootstrap documentation for information.
Install Formik and Yup for the form validation with the command below.
yarn add formik yup
III. Import Bootstrap Styles: Bootstrap styles provide a cohesive and visually appealing design for your application’s UI, enhancing its professionalism and usability.
In your App.tsx import bootstrap and give the div a classname as follows:
import React from "react"; import "bootstrap/dist/css/bootstrap.min.css"; const App = () => { return <div className="container">CodeGentitor</div>; }; export default App;
IV. Create a register page:
Design and implement the registration page to allow users to create new accounts.
Inside the ‘pages’ folder, create a new directory named ‘register’. Within it, add a file named ‘Register.tsx’ and include the following code
import React from "react"; import { Form, Button } from "react-bootstrap"; import { Formik, Field, FormikProps, FormikHelpers } from "formik"; import * as Yup from "yup"; interface FormValues { name: string; email: string; password: string; } const Register: React.FC = () => { const validationSchema = Yup.object().shape({ name: Yup.string().required("Name is required"), email: Yup.string() .email("Invalid email address") .required("Email is required"), password: Yup.string().required("Password is required"), }); const handleSubmit = ( values: FormValues, { setSubmitting }: FormikHelpers<FormValues> ) => { // Handle form submission console.log(values); setSubmitting(false); }; return ( <div className="mx-auto max-w-md space-y-6"> <div className="space-y-2 text-center"> <h1 className="text-3xl font-bold">Create an account</h1> <p className="text-gray-500 dark:text-gray-400"> Enter your details to get started. </p> </div> <Formik initialValues={{ name: "", email: "", password: "" }} validationSchema={validationSchema} onSubmit={handleSubmit} > {({ handleSubmit }: FormikProps<FormValues>) => ( <Form className="space-y-4" onSubmit={handleSubmit}> <Form.Group className="mb-3" controlId="name"> <Form.Label>Name</Form.Label> <Field type="text" name="name" as={Form.Control} /> <Form.Control.Feedback type="invalid" /> </Form.Group> <Form.Group className="mb-3" controlId="email"> <Form.Label>Email</Form.Label> <Field type="email" name="email" as={Form.Control} /> <Form.Control.Feedback type="invalid" /> </Form.Group> <Form.Group className="mb-3" controlId="password"> <Form.Label>Password</Form.Label> <Field type="password" name="password" as={Form.Control} /> <Form.Control.Feedback type="invalid" /> </Form.Group> <Button variant="primary" type="submit" className="w-full"> Sign Up </Button> </Form> )} </Formik> </div> ); }; export default Register;
Now, if you’re wondering why it’s not displaying on your application, open the ‘App.tsx’ file and render the ‘Register’ page to display the registration form. We’ll address this integration properly later on. See below:
import React from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import Register from "./pages/register/Register"; const App = () => { return ( <div className="container"> <Register /> </div> ); }; export default App;
V. Create the Login Page:
Inside the ‘pages’ folder, create a new directory named ‘login’. Within it, add a file named ‘login.tsx’ and include the following code
import React, { FC } from "react"; import { Form, Button } from "react-bootstrap"; import { Formik, Field, FormikProps, FormikHelpers } from "formik"; import * as Yup from "yup"; interface FormValues { email: string; password: string; } const Login: FC = () => { const validationSchema = Yup.object().shape({ email: Yup.string().required("Email is required"), password: Yup.string().required("Password is required"), }); const handleSubmit = ( values: FormValues, { setSubmitting }: FormikHelpers<FormValues> ) => { // Handle form submission console.log(values); setSubmitting(false); }; return ( <div className="mx-auto max-w-md space-y-6"> <div className="space-y-2 text-center"> <h1 className="text-3xl font-bold">Login in</h1> <p className="text-gray-500 dark:text-gray-400"> Enter your details to get started. </p> </div> <Formik initialValues={{ email: "", password: "" }} validationSchema={validationSchema} onSubmit={handleSubmit} > {({ handleSubmit }: FormikProps<FormValues>) => ( <Form className="space-y-4" onSubmit={handleSubmit}> <Form.Group className="mb-3" controlId="email"> <Form.Label>Email</Form.Label> <Field type="email" name="email" as={Form.Control} /> </Form.Group> <Form.Group className="mb-3" controlId="password"> <Form.Label>Password</Form.Label> <Field type="password" name="password" as={Form.Control} /> </Form.Group> <Button variant="primary" type="submit" className="w-full"> Sign In </Button> </Form> )} </Formik> </div> ); }; export default Login;
Let’s build the backend server for the frontend application.
I. Setting Up the Express Server: If you have done the previous setup, then now we can begin by setting up the express server.
mkdir src; cd src; mkdir middlewares routes models validations
import express, { Request, Response } from "express"; import dotenv from "dotenv"; import cors from "cors"; import morgan from "morgan"; // Initialize dotenv to access environment variables dotenv.config(); const app = express(); // middlewares app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors()); // Enable CORS for all requests app.use(morgan("dev")); // Log all requests to the console // Get the PORT from the environment variables // Add PORT=3000 to the .env file const PORT = process.env.PORT; // Basic route app.get("/", (req: Request, res: Response) => { try { return res.status(200).json({ message: " Welcome to CodeGenitor API", }); } catch (error) { return res.status(500).json({ message: "Internal Server Error", }); } }); // Unknown route handler app.use((req: Request, res: Response) => { return res.status(404).json({ message: "Route not found", }); }); // unknonw route handler app.use((req: Request, res: Response) => { return res.status(404).json({ message: "Route not found", }); }); // Start the server app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
Note:
create a .env file inside the backend folder to hold all your environment variables. It is best practice to securely store environment variables, safeguarding sensitive keys and enhancing maintainability. Remember to to do a quick research on any library you do not know at npm
II. Develop Routes and Controllers for User Authentication.
Congratulations on reaching this point! You’ve surpassed a common barrier where many individuals tend to halt their progress.
import { Request, Response } from "express"; // register a new user export const register = async (req: Request, res: Response) => { try { return res.status(200).json({ message: "User registered successfully", }); } catch (error) { return res.status(500).json({ message: "Internal Server Error", }); } }; // login a user export const login = async (req: Request, res: Response) => { try { return res.status(200).json({ message: "User logged in successfully", }); } catch (error) { return res.status(500).json({ message: "Internal Server Error", }); } };
import { Router } from "express"; import { register, login } from "../controller/user.controller"; const userRouter = Router(); // Register a new user userRouter.post("/register", register); // Login a user userRouter.post("/login", login);
import express, { Request, Response } from "express"; import dotenv from "dotenv"; import cors from "cors"; import morgan from "morgan"; import userRouter from "./routes/user.route"; // Initialize dotenv to access environment variables dotenv.config(); const app = express(); // middlewares app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors()); // Enable CORS for all requests app.use(morgan("dev")); // Log all requests to the console // Get the PORT from the environment variables // Add PORT=3000 to the .env file const PORT = process.env.PORT; // Basic route app.get("/", (req: Request, res: Response) => { try { return res.status(200).json({ message: " Welcome to CodeGenitor API", }); } catch (error) { return res.status(500).json({ message: "Internal Server Error", }); } }); // User routes app.use("/api/user", userRouter); // Unknown route handler app.use((req: Request, res: Response) => { return res.status(404).json({ message: "Route not found", }); }); // unknonw route handler app.use((req: Request, res: Response) => { return res.status(404).json({ message: "Route not found", }); }); // Start the server app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
Note:
You can test the rest of the HTTP methods using postman besides the Get where you can do it from the browser. If you do not know about postman, I have added a link to find more about here. comment to let me know if I should go into details with postman.
III. Connecting database to the application:
CREATE USER users with password 'hardpassalwaysok'; CREATE DATABASE users_auth; GRANT ALL PRIVILEGES ON DATABASE users_auth TO users;
Note:
you can call the psql from your terminal, it doesn’t matter where you open it. And you can list all the databases to verify the db is created by running `\l`
1. Install pg Package: In the Node.js project directory, install the pg package, which is the PostgreSQL client for Node.js:
npm install pg @types/pg
Note: You can just add pg to the requirement file and run the update by running it again.
mkdir config; cd config; touch connectDB.ts
Note:
Assuming you are already in the src folder from the terminal in your backend.
2. Creating the Database connection in connectDB.ts
import { Pool } from "pg"; // configure the database connection const connectDB = async () => { const pool = new Pool({ user: "users", host: "localhost", database: "users_auth", password: "hardpassalwaysok", }); // test the connection pool.query("SELECT NOW()", (err, res) => { if (err) { console.log(err); } else { console.log("Database connected successfully"); } }); }; export default connectDB;
3. Update the index.ts file to see if the database connects successfully.
import express, { Request, Response } from "express"; import dotenv from "dotenv"; import cors from "cors"; import morgan from "morgan"; import userRouter from "./routes/user.route"; import connectDB from "./config/connectDB"; // Initialize dotenv to access environment variables dotenv.config(); const app = express(); // middlewares app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors()); // Enable CORS for all requests app.use(morgan("dev")); // Log all requests to the console // Get the PORT from the environment variables // Add PORT=3000 to the .env file const PORT = process.env.PORT; // Basic route app.get("/", (req: Request, res: Response) => { try { return res.status(200).json({ message: " Welcome to CodeGenitor API", }); } catch (error) { return res.status(500).json({ message: "Internal Server Error", }); } }); // User routes app.use("/api/user", userRouter); // Unknown route handler app.use((req: Request, res: Response) => { return res.status(404).json({ message: "Route not found", }); }); // unknonw route handler app.use((req: Request, res: Response) => { return res.status(404).json({ message: "Route not found", }); }); // Start the server app.listen(PORT, async () => { console.log(`Server is running on http://localhost:${PORT}`); await connectDB(); // connect to the database });
Note:
If you’ve followed the steps correctly, your backend should now be seamlessly connected to your database. Encountering issues? Don’t hesitate to reach out by dropping a comment or sending a message, and I’ll swiftly assist you to get back on track.
4. Adding a schema to create database tables
· Navigate to the src directory in your backend project.
· Create a new folder named schema.
· Inside the schema folder, create a file named user.schema.sql.
· Open the user.schema.sql file and add the following schema definition:
CREATE SCHEMA users; CREATE TABLE users.users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL );
· Update your `connectDB.ts` file as follows:
import { Pool } from "pg"; import chalk from "chalk"; import fs from "fs"; import path from "path"; const pool = new Pool({ user: "users", host: "localhost", database: "users_auth", password: "hardpassalwaysok", }); // Read the contents of the user.schema.sql file const schemaFilePath = path.resolve(__dirname, "../schema/user.schema.sql"); const schemaSQL = fs.readFileSync(schemaFilePath, "utf-8"); // Function to create tables defined in the schema file const createTables = async () => { try { // Check if the users table already exists const result = await pool.query(` SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'users' AND table_name = 'users' ) `); const tableExists = result.rows[0].exists; if (!tableExists) { // Execute the SQL commands to create tables await pool.query(schemaSQL); console.log(chalk.greenBright("Tables created successfully")); } else { console.log(chalk.yellow("Users table already exists")); } } catch (error) { console.error(chalk.red("Error creating tables:"), error); } }; // configure the database connection const connectDB = async () => { try { // Connect to the database await pool.connect(); console.log(chalk.greenBright("Database connected successfully")); // Create tables await createTables(); } catch (error) { console.error(chalk.red("Error connecting to database:"), error); } }; export default connectDB;
IV. Implement user registration and login logic
import { Request, Response } from "express"; import { pool } from "../config/connectDB"; import bcrypt from "bcryptjs"; // register a new user export const register = async (req: Request, res: Response) => { try { // get the user data from the request body const { username, email, password } = req.body; // check if user data is provided if (!username || !email || !password) { return res.status(400).json({ message: "Please provide all user details", }); } // check if user already exists const user = await pool.query("SELECT * FROM users WHERE email = $1", [ email, ]); if (user.rows.length > 0) { return res.status(400).json({ message: "User with this email already exists", }); } // hash the password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); // execute SQL query to insert user into the database const query = ` INSERT INTO users (username, email, password) VALUES ($1, $2, $3) `; await pool.query(query, [username, email, hashedPassword]); return res.status(200).json({ message: "User registered successfully", }); } catch (error) { console.log(error); return res.status(500).json({ message: "Internal Server Error", }); } }; // login a user export const login = async (req: Request, res: Response) => { try { // get the user data from the request body const { email, password } = req.body; // check if user data is provided if (!email || !password) { return res.status(400).json({ message: "Please provide all user details", }); } // check if user exists const user = await pool.query("SELECT * FROM users WHERE email = $1", [ email, ]); if (user.rows.length === 0) { return res.status(400).json({ message: "User does not exist", }); } // compare the password const validPassword = await bcrypt.compare(password, user.rows[0].password); if (!validPassword) { return res.status(400).json({ message: "Invalid password", }); } // If the email and password are valid, return user data return res.status(200).json({ message: "User logged in successfully", user: { id: user.rows[0].id, username: user.rows[0].username, email: user.rows[0].email, }, }); } catch (error) { return res.status(500).json({ message: "Internal Server Error", }); } };
· Create a folder called service and inside it also index.service.ts a file inside the src directory of your frontend project, you can use the following command in your terminal:
mkdir service;cd service; touch index.service.ts
yarn add axios @types/axios
Note: axios package has also been installed from the CLI.
· Updated the index.service.ts file with the following:
import axios from "axios"; const apiUrl = "http://localhost:4000/api/"; interface User { username: string; email: string; password: string; } // register the user service export const registerUser = async (user: User) => { try { const response = await axios.post(`${apiUrl}user/register`, user); return response.data; } catch (error) { return error; } }; // login the user service export const loginUser = async (user: User) => { try { const response = await axios.post(`${apiUrl}user/login`, user); return response.data; } catch (error) { return error; } };
· Next, we’ll enable user registration from our frontend application. Navigate to the Register.tsx file and implement the following updates:
import React, { useState } from "react"; import { Form, Button } from "react-bootstrap"; import { Formik, Field, FormikProps, FormikHelpers } from "formik"; import * as Yup from "yup"; import { registerUser } from "../../service/index.service"; interface FormValues { username: string; email: string; password: string; } const Register: React.FC = () => { const [success, setSuccess] = useState(""); const validationSchema = Yup.object().shape({ username: Yup.string().required("Name is required"), email: Yup.string() .email("Invalid email address") .required("Email is required"), password: Yup.string().required("Password is required"), }); const handleSubmit = async ( values: FormValues, { setSubmitting }: FormikHelpers<FormValues> ) => { // Handle form submission const submit = await registerUser(values); console.log(submit.message); setSuccess(submit.message); setSubmitting(false); }; return ( <div className="mx-auto max-w-md space-y-6"> <div className="space-y-2 text-center"> <h1 className="text-3xl font-bold">Create an account</h1> <p className="text-gray-500 dark:text-gray-400"> Enter your details to get started. </p> </div> <Formik initialValues={{ username: "", email: "", password: "" }} validationSchema={validationSchema} onSubmit={handleSubmit} > {({ handleSubmit }: FormikProps<FormValues>) => ( <Form className="space-y-4" onSubmit={handleSubmit}> {success && ( <div className="text-green-500 " style={{ textAlign: "center", fontSize: "1.2rem", fontWeight: "bold", backgroundColor: "#f0f0f0", padding: "0.5rem", borderRadius: "0.5rem", }} > {success} </div> )} <Form.Group className="mb-3" controlId="name"> <Form.Label>Username</Form.Label> <Field type="text" name="username" as={Form.Control} /> <Form.Control.Feedback type="invalid" /> </Form.Group> <Form.Group className="mb-3" controlId="email"> <Form.Label>Email</Form.Label> <Field type="email" name="email" as={Form.Control} /> <Form.Control.Feedback type="invalid" /> </Form.Group> <Form.Group className="mb-3" controlId="password"> <Form.Label>Password</Form.Label> <Field type="password" name="password" as={Form.Control} /> <Form.Control.Feedback type="invalid" /> </Form.Group> <Button variant="primary" type="submit" className="w-full"> Sign Up </Button> </Form> )} </Formik> </div> ); }; export default Register;
If you have been able to follow all the steps , step by step you should have a results like this below:
Fork the source Code on Github
Lets reflect key points covered:
Next steps and additional resources:
Hopefully, I have been able to help you achieve your aim, share with a friend who could benefit and give me a like and subscribe to help me help more. As the saying goes “what good is knowledge if not shared?”
CodeGenitor
Posted on January 4, 2025