My Full Stack Application can be found here.
- PERN Stack Course Documentation
- Starting the Server
- Create our Postgres Database and Table
- Connect database and server
- Build Routes with PostgreSQL Queries
- Restful API Overview
- Set Up the Client Side (React)
- Building The Input Todo Component
- Build The List Todo Component
- Build the Delete Button
- Build the Edit Todo Component
- PERN Stack Review
- How to Deploy a PERN application on Heorku
Create server folder
mkdir server
Install the following packages in the server folder
npm i express cors pg
- pg is used for running queries in postgres
- cors a.k.a. Cross-Origin Resource Sharing (CORS). It's a middleware
Make an index.js library and initialize the express server
const express = require("express");
const app = express();
app.listen(5000,()=> console.log("server has started on port 5000"));
Instead of using node index, can instead run nodemon index. This way everytime a change is made, it is reflected in the server. Installing nodemon globally is best practice for this.
Once we know that our server works, we initialize middleware by:
const cors = require("cors");
app.use(cors());
app.use(express.json()); // allows us to access request body
Instructions located in database.sql
- really easy when using the pg library
Basically, have to make a connection file called db.js, and pass in the relevant parameters.
const Pool = require("pg").Pool;
const pool = new Pool({
user: "postgres",
host: "localhost",
port: 5432,
database: "perntodo",
});
module.exports = pool;
Need to create, read, update, and delete
// create a todo
app.post("/todos", async (req, res) => {
// async allows for await
try {
// console.log("This event is triggered");
const { description } = req.body;
const newTodo = await pool.query(
"INSERT INTO todo (description) VALUES($1) RETURNING *",
[description] // pg library allows us to add dynamic data, $1 allows us to dynamically add values
// RETURNING * allows you to get back the data again, that was added or inserted or updated
);
res.json(newTodo.rows[0]);
} catch (err) {
console.log(err.message);
}
});
// get all todos
app.get("/todos", async (req, res) => {
try {
const allTodos = await pool.query("SELECT * FROM todo");
res.json(allTodos.rows);
} catch (error) {
console.log(error.message);
}
});
// get a todo
app.get("/todos/:id", async (req, res) => {
try {
const { id } = req.params;
const todo = await pool.query("SELECT * FROM todo WHERE todo_id = $1", [
id,
]);
res.json(todo.rows[0]);
} catch (error) {
console.log(error.message);
}
});
// update a todo
app.put("/todos/:id", async (req, res) => {
try {
const { id } = req.params;
const { description } = req.body;
console.log(description);
const updateTodo = await pool.query(
"UPDATE todo SET description = $1 WHERE todo_id= $2",
[description, id]
);
res.json("Todo was updated!");
} catch (err) {
console.log(err.message);
}
});
// delete a todo
app.delete("/todos/:id", async (req, res) => {
try {
const { id } = req.params;
const deleteTodo = await pool.query("DELETE FROM todo WHERE todo_id=$1", [
id,
]);
res.json("Todo was deleted");
} catch (err) {
console.log(err);
}
});
app.listen(5000, () => {
console.log("server has started on port 5000");
});
To write info, send a POST request
To read info, send a GET request
To update info, send a PUT request
To delete info, send a DELETE request
In the project folder:
npx create-react-app client
- This will create a react folder called client. This is where we will do all client sided programming.
npm start
- Go ahead and start the server on React side
The different components that will be part of our app will be as follows

After executing the initialization script, need to delete a few items in the src folder, and your final src folder should only contain the following:
- App.css
- App.js
- index.css
- index.js
import React from "react" in the App.js file
Create a components folder and add in the components that you were going to add. In our case, this is:
- EditTodo.js
- InputTodo.js
- ListTodos.js (notice Todos is plural here, naming is important)
Also Bootstrap 4 will be used in this tutorial.
Add Bootstrap CSS AND JS into the public > index.html file
import React, { useState } from "react";
const InputTodo = () => {
const [description, setDescription] = useState("");
const onSubmitForm = async (e) => {
e.preventDefault();
try {
const body = { description };
const response = await fetch("http://localhost:5000/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}); // default is fetch makes GET requests, have to add an object with all the options
console.log(response);
window.location = "/";
} catch (err) {
console.log(err.message);
}
};
return (
<>
<h1 className="text-center mt-5">Pern Todo List</h1>
<form className="d-flex mt-5" onSubmit={onSubmitForm}>
<input
type="text"
className="form-control"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button className="btn btn-success">Add</button>
</form>
</>
);
};
export default InputTodo;
import React, { useEffect, useState } from "react";
const ListTodos = () => {
const [todos, setTodos] = useState([]);
const getTodos = async () => {
try {
const response = await fetch("http://localhost:5000/todos");
const jsonData = await response.json();
setTodos(jsonData);
} catch (err) {
console.error(err.message);
}
};
useEffect(() => {
getTodos();
}, []);
return (
<>
<table className="table mt-5 text-center">
<thead>
<tr>
<th>Description</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{todos.map((todo) => (
<tr>
<td>{todo.description}</td>
<td>Edit</td>
<td>Delete</td>
</tr>
))}
</tbody>
</table>
</>
);
};
export default ListTodos;
Add a delete button to the table, and set its onclick property to function deleteTodo(todo.todo_id). Here we are passing the id as a parameter to the function.
Next define the function as follows
// delete todo function
const deleteTodo = async (id) => {
try {
const deleteTodo = await fetch(`http://localhost:5000/todos/${id}`, {
method: "DELETE",
});
setTodos(todos.filter((todo) => todo.todo_id !== id));
} catch (err) {
console.log(err.message);
}
};
-
Used a modal component copied from w3 schools to edit information
-
Defined a function to update information, which involved sending a
PUTrequest to the server, and then refreshing the pageimport React, { useState } from "react"; const EditTodo = ({ todo }) => { const [description, setDescription] = useState(todo.description); // edit description function const updateDescription = async (e) => { e.preventDefault(); try { const body = { description }; const response = await fetch( `http://localhost:5000/todos/${todo.todo_id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), } ); window.location = "/"; console.log(response); } catch (err) { console.log(err.message); } }; return ( <> <button type="button" className="btn btn-warning" data-toggle="modal" data-target={`#id${todo.todo_id}`} > Edit </button> <div className="modal" id={`id${todo.todo_id}`} onClick={() => setDescription(todo.description)} > <div Name="modal-dialog"> <div className="modal-content"> <div className="modal-header"> <h4 className="modal-title">Edit Todo</h4> <button type="button" className="close" data-dismiss="modal" onClick={() => setDescription(todo.description)} > × </button> </div> <div className="modal-body"> <input type="text" className="form-control" value={description} onChange={(e) => setDescription(e.target.value)} /> </div> <div className="modal-footer"> <button type="button" className="btn btn-warning" data-dismiss="modal" onClick={(e) => updateDescription(e)} > Edit </button> <button type="button" className="btn btn-danger" data-dismiss="modal" onClick={() => setDescription(todo.description)} > Close </button> </div> </div> </div> </div> </> ); }; export default EditTodo;
- We first set up the database
- We then routed requests, and tested these request with POSTMAN
- Then we went ahead and created a react app folder using
npx create-react-app client - We made a bunch of fetch requests to the server to Create, Read, Update, and Delete our Todo list items.
Based off Youtube video
- Download and install the Heroku CLI
- Deploy your app to github preferably
.git directory should be located in your root directory
- heroku stipulates that package.json file must be in the root directory
- As such place all server code into the root directory
In the index.js file, have to use process.env.PORT
const PORT = process.env.PORT || 5000;
-
change all instances of 5000 to PORT
-
also going to use
process.env.NODE_ENVto determine if we are in production or not-
if we are in production environment, then run the client/build/index.html. The way we would do this is by making the build folder accessible to
index.jsin server. This is achieved through the below codeif (process.env.NODE_ENV === "production") { // server static content // npm run build app.use(express.static(path.join(\_\_dirname, "client/build"))); }
-
Have to modify db.js
Install a library that will allow you to hide all of the secret info so it doesn't get deployed with heroku
npm i dotenv
Create a .env file and put all configurations from db.js to .env
db.js will look like follows:
const Pool = require("pg").Pool;
require("dotenv").config();
const devConfig = {
user: process.env.PG_USER,
host: process.env.PG_HOST,
port: process.env.PG_PORT,
database: process.env.PG_DATABASE,
};
const proConfig = {
connectionString: process.env.DATABASE_URL, // heroku addons
};
const pool = new Pool(
process.env.NODE_ENV === "production" ? proConfig : devConfig
);
module.exports = pool;
- basically if the process.env.NODE_ENV is production, then use production config settings, otherwise use the developer config settings
.env file will look like follows:
PG_USER = postgres
PG_HOST = localhost
PG_PORT = 5432
PG_DATABASE = perntodo
In the package.json file, need to modify a few things
So we are going to first install all the dependencies and then run the heroku-postbuild, and then start
We need to make the following changes in the scripts of the package.json file
"start": "node index.js",
"heroku-postbuild": "cd client && npm install && npm run build"
will help to shorten URL
go to client package.json file, and add
"proxy": "http://localhost:5000"
Once this step is completed, remove the http://localhost:5000, because you no longer need it.
node will automatically add the proxy if it is on development machine
In root package.json, define the node and npm versions. Add it in the the package.json:
"engines": {
"node": "14.15.3",
"npm": "6.14.10"
}
In server index.js, add this catchall method
// catch all method has to be in the bottom
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "client/build/index.html"));
});
After installing the heroku CLI
heroku login
To create an app
heroku create pern-fullstack-todo-tutorial
Then need to add postgres addon to heroku
heroku addons:create heroku-postgresql:hobby-dev -a pern-fullstack-todo-tutorial
Can access postgresql on heroku via
heroku pg:psql -a pern-fullstack-todo-tutorial
The pipe basically uses the database.sql as input into the heroku postgresql
cat database.sql | heroku pg:psql -a pern-fullstack-todo-tutorial
To connect the git repository to heorku
heroku git:remote -a pern-fullstack-todo-tutorial
To then push to heroku, we say
git push heroku master
To open our app from command line, we say
heroku open
