Developing a 3-tier PERN Stack Application

Developing a 3-tier PERN Stack Application

·

11 min read

Story Line,

Sufia, a passionate tech enthusiast, is on a mission to dive deep into the fascinating world of 3-tier web applications. Eager to unravel the jargon and complexity of this architecture, she knows that there’s no better way to learn than by doing. With that in mind, she rolled up her sleeves and decided to create an exciting new app using the PERN stack. This hands-on project will not only boost her skills but also help her gain a solid understanding of how 3-tier architecture works. It's going to be an adventure in learning, for Sufia and us so come on let’s get started!

Introduction

In this blog, we will cover how full-stack applications are created in real time. We will dive deep into understanding how the frontend, backend, and database establish connections and communicate effectively. To illustrate this, we will create a simple dynamic application using the PERN stack, specifically a to-do app.

PERN stack in short:

  • Postgresql: a relational database for storing and retrieving the data.

  • Expressjs: web framework for building REST API’s with Nodejs.

  • React js: javascript library for building dynamic Single page user interface.

  • Node js: javascript runtime used to build the server-side application.

Overview of the Todo App:

In this app, the to-do list is fetched from the database and sent as a response to the React frontend by the Node.js backend. When a user adds a new to-do task or updates an existing one, the React app sends the data to the Node.js API. The API then stores or updates the to-do item in the appropriate PostgreSQL database table.

Prerequisite;

  • A basic understanding of git and version controlling. Click here to install git.

  • Familiarity in working with VS code IDE. Click here to install vscode.

  • Basic understanding of JavaScript Programming.

Project Outcomes;

  • An understanding of how web applications are developed in industry.

  • Complete Practical and Theoretical knowledge of Three-tier Architecture.

1. Project setup

1.1 Initialize Git Repository

  • Navigate to your project directory or if you haven’t created one create now by executing the below command
mkdir <project-name>

cd <project-name>

info: In vscode executing the “code .” command will open the current path folder in a new vs code window.

  • Initialize an empty GIT Repository for version control so that you can create checkpoints in a project and revert from it whenever you want to or the project demands to.
git init

output;

  • Create a git ignore file to ignore the node_modules, and other repo-specific files/directories while pushing the code from the local to a remote server

1.2 Setup development Environment

NOTE: here in this project we will be working in Windows OS since many are already familiar with it.

  • Install Nodejs and npm. Click here to install.

  • note: you can install Postgresql either locally or on a remote server. But to make this project beginner friendly we will be installing the Postgres in the local machine. Click here to install Postgresql.

  • With that done now we can ensure packages are installed correctly by executing the following command.

node -v

npm -v

psql -v

Note: versions may differ depending on the timeline you’re using for this blog.

2. Data-Layer (PostgreSQL)

2.1 Creating the User in DB

  • Creating a role in the Postgresql for authentication and authorization to access the DB.
CREATE USER <user-name> WITH PASSWORD ‘<password>’ ;

2.2 Creating the DB

  • Creating the DB in the Postgres Database. With the name of the todo app
CREATE DATABASE <db-name>

2.3 Creating the Table

  • Ensure everything works fine by Signing In to the created DB
Psql -h <host-name/address> -U <created-role> -d <db-name>

PSQL will prompt for asking for the password. Enter the corresponding password once both are correct you will be logged in.

  • Create a table inside the created DB.

      CREATE TABLE table_name (
          column_name1 data_type [constraint],
          column_name2 data_type [constraint],
          column_name3 data_type [constraint],
          ...
          CONSTRAINT constraint_name constraint_definition
      );
    

  • Listing the table in the created db

      \dt
    

2.4 Checking the DB

  • Checking whether the created DB works fine by Inserting the data in the DB and Querying the DB.

      INSERT INTO <table-name> (<field1>, <field2>) 
      VALUES
       ( <field1-value>, <field2-value> );
    

output,

Congratulations 🎉🥳🎉!! So far now DB is working perfectly fine. and responding to all queries.

3. Backend / Server-Side Development ( Express + Nodejs + Postgresql )

3.1 creating the backend folder

  • Create the backend folder on the root of your project and navigate to it.
mkdir backend

cd backend
  • Initialize the npm project
npm init -y

output,

Note: now you may notice that the package.json file is created inside the backend folder.

3.2 Install Backend Dependencies

  • Install the necessary backend dependencies.
npm install express pg cors dotenv

output,

3.3 Setup Express Server

  • Create a server.js file in your IDE. then Copy and paste the code below.

note: Explanations corresponding to every line of code are typed in the respective comments

3.4 Establish DB Connection

  • Create a Config directory inside the backend directory. Inside that create a file named db_connections.js

here in this file set the environment variable in the following way.

DATABASE_URL="postgres://<db_username>:<db_password>@<Host_Address/Name>:<port_no>/<DB_Name>"
require('dotenv').config();
const { Pool } = require('pg');



// PostgreSQL Pool setup via URL
const pool = new Pool({
    connectionString: process.env.DATABASE_URL,

});


// // PostgreSQL Pool setup via passing parameter
// const client = new Client({
    //     host: process.env.PG_HOST,
    //     port: process.env.PG_PORT,
    //     user: process.env.PG_USER,
    //     password: process.env.PG_PASSWORD,
    //     database: process.env.PG_DATABASE,
    //   });

// Test DB connection
 pool.connect()
    .then(client => {
        console.log("Connected to the database");
        client.release();
    })
    .catch(err => console.error('Error connecting to the database', err));



module.exports = pool;

3.5 Creating API Routes

  • Create Routes Directory and create file todorouter.js in that file copy and paste the below given API routes.

      mkdir Routes
      cd Routes
      touch todorouter.js
    

    code

      const express = require('express');
      const { createItem, getItems, putItem, deleteItem } = require('../controllers/item_controller')
    
      const itemRouter = express.Router();
    
      itemRouter.get('/items', getItems)
      itemRouter.post('/items', createItem)
      itemRouter.put('/items/:id', putItem )
      itemRouter.delete('/items/:id', deleteItem)
    
      module.exports = { itemRouter }
    
  • For project management we have declared the req res function in the controllers directory in the file todocontroller.js

      mkdir Controllers
      cd Controllers
      touch todocontroller.js
    
  • code,

const { pool } = require('../config/db')


const getItems = async (req, res) => {
    try {
      const result = await pool.query('SELECT * FROM items_table');
      res.json(result.rows);
    } catch (error) {
      res.status(500).send('Server Error');
    }
  };

const createItem = async (req, res) => {
    const { name, description } = req.body;
    try {
      const result = await pool.query('INSERT INTO items_table (name, description) VALUES ($1, $2) RETURNING *', [name, description]);
      res.json(result.rows[0]);
    } catch (error) {
      res.status(500).send('Server Error');
    }
  };


const putItem = async (req, res) => {
    const { id } = req.params;
    const { name, description } = req.body;
    try {
      const result = await pool.query('UPDATE items_table SET name = $1, description = $2 WHERE id = $3 RETURNING *', [name, description, id]);
      if (result.rows.length > 0) {
        res.json(result.rows[0]);
      } else {
        res.status(404).send('Item not found');
      }
    } catch (error) {
      res.status(500).send('Server Error');
    }
  };

const deleteItem =  async (req, res) => {
    const { id } = req.params;
    try {
      const result = await pool.query('DELETE FROM items_table WHERE id = $1 RETURNING *', [id]);
      if (result.rows.length > 0) {
        res.json(result.rows[0]);
      } else {
        res.status(404).send('Item not found');
      }
    } catch (error) {
      res.status(500).send('Server Error');
    }
  };

  module.exports =  { getItems, createItem, putItem, deleteItem }
  • server.js main file
const express = require('express');
const cors = require('cors');
const { itemRouter } = require('./routers/item_router')

require('dotenv').config();


const app = express();
const port = process.env.PORT || 5000;

// Middleware
app.use(cors());
app.use(express.json()); // To parse JSON bodies

app.use('/api', itemRouter)


// Start the server
app.listen(port, '0.0.0.0', () => {
  console.log(`Server running on port ${port}`);
});

3.6 Testing the API

  • Now that everything is set up, we can test the API to see if it works as expected. This involves sending a request to the backend API using the browser on the local machine. Before doing that, we need to start the backend server.
node .\server.js

output,

URL: localhost:5000/api/items

If everything is done correctly you will see the response produced by the server.

🎉🥳🎉 Congratulations now we have established the communication between the backend and db.

4. Frontend Development ( Reactjs )

4.1 Create a React app

  • Create a react app by running the below command
npx create-react-app <frontend-name>

note: Explore and understand the differences between npm and npx to enhance your development process. Click here to gain valuable insights that can improve your workflow.

After running the command you will have the basic setup created by the React framework

4.2 Install Frontend Dependencies

  • Install the Dependencies required for the Frontend application
npm install axios

4.3 Create Folder Structure

  • After running the npx create-react-app command, a directory will be created with the name you provided as an argument.

  • Enter the directory. There, you notice several files and folders have been created. Among them, navigate to the src directory; this is where our code changes will be made.

  • Some important files to be noted off.

  • Public/index.html: This HTML template file is loaded when the app starts. React will inject the react components into the <div id="root"></div> tag inside this file.

  • src/index.js: This is the javascript entry point for your app. React components are rendered here.

  • src/App.js: The default App component provided by create-react-app. This is where you can begin building your app.

4.4 create React Components

  • Now under the src/App.js file delete all the content and paste our below-provided todo app component.
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const App = () => {
  const [items, setItems] = useState([]);
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');
  const [editId, setEditId] = useState(null);

  useEffect(() => {
    // Fetch all items
    axios.get(`${process.env.Backend_Base_Url}/api/items`)
      .then(response => {
        setItems(response.data);
      })
      .catch(error => console.log(error));
  }, []);

  const handleAddItem = async () => {
    try {
      const response = await axios.post(`${process.env.Backend_Base_Url}/api/items`, { name, description });
      setItems([...items, response.data]);
      setName('');
      setDescription('');
    } catch (error) {
      console.error(error);
    }
  };

  const handleUpdateItem = async () => {
    try {
      const response = await axios.put(`${process.env.Backend_Base_Url}/api/items/${editId}`, { name, description });
      setItems(items.map(item => item.id === editId ? response.data : item));
      setName('');
      setDescription('');
      setEditId(null);
    } catch (error) {
      console.error(error);
    }
  };

  const handleDeleteItem = async (id) => {
    try {
      await axios.delete(`${process.env.Backend_Base_Url}/api/items/${id}`);
      setItems(items.filter(item => item.id !== id));
    } catch (error) {
      console.error(error);
    }
  };

  const noItemsInfoMsg = <h4>Currently no item in the List, Add an Item to display</h4>

  const itemsList = items.map(item => (
    <li key={item.id}>
      <strong>{item.name}</strong>: {item.description}
      <button onClick={() => { setEditId(item.id); setName(item.name); setDescription(item.description); }}>Edit</button>
      <button onClick={() => handleDeleteItem(item.id)}>Delete</button>
    </li>
  ))

  return (
    <div>
      <h1>Items List</h1>

      <input
        type="text"
        placeholder="Name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        type="text"
        placeholder="Description"
        value={description}
        onChange={(e) => setDescription(e.target.value)}
      />
      <button onClick={editId ? handleUpdateItem : handleAddItem}>
        {editId ? 'Update Item' : 'Add Item'}
      </button>

      <ul>
        { items.length === 0 ?  noItemsInfoMsg : itemsList }
      </ul>
    </div>
  );
};

export default App;

4.5 Fetch Data and Display Tasks

  • Now run the app and verify the output by executing the command.
cd <frontend-name>

npm install

npm start

🎉🥳🎉 hip hip hurrah!! our simple Todo task application works perfectly fine.

5. Connecting the Frontend and Backend

now that all three tiers of our application are running smoothly! now we are on the pivotal point of our 3-tier development journey: connecting the front end with the back end and integrating the data layer.

This next step is crucial and will bring our application to life, enhancing functionality and user experience. so, make sure that all modular parts (frontend, backend, db ) of our app are working perfectly fine.

5.1 Establishing the connection:

  • In the front end, wherever we made API requests using Axios, change the URL to point to localhost with port number 5000. http://localhost:5000

  • Now in the vs code click the + icon in the top right corner of the terminal start.

The app runs locally in your browser. This time, data is fetched from PostgreSQL by the backend to the frontend. Any changes made in the app will affect the PostgreSQL database.

5.2 Cross-Origin Resource Sharing (CORS): Troubleshooting

When running the front and backend on different ports during development, ensure that CORS is enabled on the Express server.

click here to get an insight into what is CORS and why it is important.

const cors = require('cors');

app.use(cors());

6. Conclusion

  • With that done now you have a complete understanding of how 3-tier applications work and how they are developed in real time with practical knowledge.

  • To practice, you can feel free to attempt and implement further improvements, such as adding user authentication or enhancing the UI/UX of the code, and then push your changes to the repository through pull requests.

  • Click here to access the complete code repository on GitHub for reference.

Further Steps

  • Deploying the PERN stack application on a cloud VM.

  • Implementing serverless deployment for the PERN stack application.

  • Containerizing the application effectively.

  • Planning and constructing the CI/CD pipeline.