loading...
Cover image for Forum App with Golang/Gin and React/Hooks

Forum App with Golang/Gin and React/Hooks

stevensunflash profile image Steven Victor Updated on ・20 min read

Have you been looking forward to a production application built with Golang and React? This is one.

This application has an API Backend and a Frontend that consumes the API.
The application has two repositories:

This is live version of the App. You can interact with it.

Technologies

Backend Technologies:

  • Golang
  • Gin Framework
  • GORM
  • PostgreSQL/MySQL

Frontend Technologies:

  • React
  • React Hooks
  • Redux

Devops Technologies

  • Linux
  • Nginx
  • Docker

While the above may seem overwhelming, you will see how they all work in sync.

You might also like to check out my other articles about go, docker, kubernetes here

SECTION 1: Buiding the Backend

This is backend session wired up with Golang

Here, I will give a step by step approach to what was done.

Step 1: Basic Setup

a. The base directory
Create the forum directory on any path of your choice in your computer and switch to that directory:

        ```mkdir forum && cd forum```

b. Go Modules
Initialize go module. This takes care of our dependency management. In the root directory run:

go mod init github.com/victorsteven/forum

As seen, I used github url, my username, and the app root directory name. You can use any convention you want.

c. Basic Installations

We will be using third party packages in this application. If you have never installed them before, you can run the following commands:

go get github.com/badoux/checkmail
go get github.com/jinzhu/gorm
go get golang.org/x/crypto/bcrypt
go get github.com/dgrijalva/jwt-go
go get github.com/jinzhu/gorm/dialects/postgres
go get github.com/joho/godotenv
go get gopkg.in/go-playground/assert.v1
go get github.com/gin-contrib/cors 
go get github.com/gin-gonic/contrib
go get github.com/gin-gonic/gin
go get github.com/aws/aws-sdk-go 
go get github.com/sendgrid/sendgrid-go
go get github.com/stretchr/testify
go get github.com/twinj/uuid
github.com/matcornic/hermes/v2

d. .env file
Create and set up a .env file in the root directory.

touch .env

The .env file contains the database configuration details and other details that you want to key secret. You can use the .env.example file(from the repo) as a guide.

This is a sample .env file:

e. api and tests directories
Create an api and tests directories in the root directory.

mkdir api && mkdir tests

Thus far, our folder structure looks like this:

forum
├── api
├── tests
├── .env
└── go.mod

Step 2: Wiring up the Models

We will be needing about five models in this forum app:
a. User
b. Post
c. Like
d. Comment
e. ResetPassword

a. User Model
Inside the API directory, create the models directory:

cd api && mkdir models

Inside the models directory, create the User.go file:

cd models && touch User.go

A user can:
i. Signup
ii. Login
iii. Update his details
iv. Shutdown his account

b. Post Model
A post can be:
i. Created
ii. Updated
iii. Deleted
In the models directory, create a Post.go file:

touch Post.go

c. Like Model
Posts can be liked or unliked.
A like can be:
i. Created
ii. Deleted
Create the Like.go file:

touch Like.go

d. Comment Model
A post can have comments.
Comment can be:
i. Created
ii. Updated
iii. Deleted
Create the Comment.go file

touch Comment.go

e. ResetPassword Model
A user might forget his/her password. When this happens, they can request to change to a new one. A notification will be sent to their email address with instructions to create a new password.
In the models directory, create the ResetPassword.go file:

touch ResetPassword.go

Step 3: Security

a. Password Security
Observe in the User.go file, that before a password is saved in our database, it must first be hashed. We called a function to help us do that. Let's wire it up.
In the api directory(the path: /forum-backend/api/), create the security directory:

mkdir security

Inside the security directory, create the password.go file:

cd security && touch password.go

b. Token Creation for ResetPassword
This is the scenario: when a user requests to change his password, a token is sent to that user's email. A function is written to hash the token. This function will be used when we wire up the ResetPassword controller file.
Inside the security directory, create the tokenhash.go file:

touch tokenhash.go

Step 4: Seeder

I think is a good idea to have data to experiment with. We will be seeding the users and posts table when we eventually wire the database.
In the api directory (in the path: /forum/api/), create a seed directory:

mkdir seed

Inside the seed directory, create the seeder file seeder.go

touch seeder.go

Step 5: Using JWT for Authentication

This app will require authentication for several things such as creating a post, liking a post, updating a profile, commenting on a post, and so on. We need to put in place an authentication system.
Inside the api directory, create the auth directory:

mkdir auth

Inside the auth directory, create the token.go file:

cd auth && touch token.go

Step 6: Protect App with Middlewares

We created authentication in step 5. Middlewares are like the Police. They will ensure that the auth rules are not broken.
The CORS middleware will allow us to interact with the React Client that we will be wiring up in section 2.

In the api directory, create the middlewares directory

mkdir middlewares

Then create the middlewares.go file inside the middlewares directory.

cd middlewares && touch middlewares.go

Step 7: Utilities

a. Error Formatting
We will like to handle errors nicely when they occur.
The ORM(Object-Relational Mapping) that is used in the app is GORM. There are some error messages that are not displayed nicely, especially those that occurred when the database is hit.
For instance, when a user inputs someone else email that is already in our database, in an attempt to sign up, we need to prevent such action and politely tell the user that he can't use that email.

In the api directory, create a the utils directory

mkdir utils

Inside the utils directory, create a formaterror directory:

cd utils && mkdir formaterror

Then create the formaterror.go file:

cd formaterror && touch formaterror.go

b. File Formatting
A user will need to update his profile(including adding an image) when he does, we will need to make sure that we image added has a unique name.

In the utils directory(path: /forum-backend/api/utils), create the fileformat directory.

mkdir fileformat

Then create the fileformat.go file inside the fileformat directory:

cd fileformat && touch fileformat.go

Step 8: Emails

Remember when we were wiring up the models, we had the ResetPassword model. Well, when a user wishes to change his password, an email is sent to him with instructions to do so. Let set up that email file.
The emails are handled using Sendgrid service.

In the api directory, create a mailer directory

mkdir mailer

Inside the mailer directory create the forgot_password_mail.go file.

cd mailer && touch forgot_password_mail.go

Step 9: Wiring Up Controllers and Routes

I perceive you might be have been thinking how all these things connect right? Well, perish the thought, because we are finally there.
This step was purposely skipped until now because it calls most of the functions and methods we defined above.

In the api directory(path: /forum-backend/api/), create the controllers directory.

mkdir controllers

You might need to pay close attention to this directory.

a. The base file
This file will have our database connection information, call our routes, and start our server:
Inside the controllers directory, create the base.go file:

cd controllers && touch base.go

b. Users Controller
Inside the controllers directory, create the users_controller.go file

touch users_controller.go

From the above file, you can observe that we sent a photo upload to either DigitalOceanSpaces or AWS S3 Bucket
If you wish to practice along, You will need to create an Amazon S3 bucket or DigitalOcean Spaces object to store the images.
Also, update your .env file:

DO_SPACES_KEY=your_do_key
DO_SPACES_SECRET=your_do_secret
DO_SPACES_TOKEN=your_do_token
DO_SPACES_ENDPOINT=your_do_endpoint
DO_SPACES_REGION=your_do_region
DO_SPACES_URL=your_do_url

# OR USING S3:

AWS_KEY=your_aws_key
AWS_SECRET=your_aws_secret
AWS_TOKEN=

c. Posts Controller
Inside the controllers directory, create the posts_controller.go file:

touch posts_controller.go

c. Login Controller
Request that update a user, create a post, delete a post, and so on, need authentication.

Inside the controllers directory, create the login_controller.go file:

touch login_controller.go

c. Likes Controller
An authenticated user can like a post or unliked already liked post.
Inside the controllers directory, create likes_controller.go file

touch likes_controller.go

d. Comments Controller
The authenticated user can create/update/delete a comment for a particular post.

touch comments_controller.go

e. ResetPassword Controller
A user can request to reset their password peradventure the password is forgotten:

touch resetpassword_controller.go

f. Routes
All controller methods are used here.
Still, in the controllers directory, create the routes.go file:

touch routes.go

Step 10: Create the Server File

In the server.go file, we open a connection to the database, provide a port the app listens to from the .env file.
Inside the api directory(in the path: forum-backend/api/) create the server.go file

touch server.go

Step 11: Run the App

Let's now see some output from our labor thus far.
Create the main.go file in the root directory of the app, and call the Run method defined in server.go file above.
In the path /forum-backend/,

touch main.go

Confirm that your directory structure looks like this:

Alt Text

Running Without Docker

If you just want to run this API without docker, make sure you have this in your .env file:

DB_HOST=127.0.0.1

Also that your database is created, the username, password, and every other thing are in place.

Open the Terminal, in the root directory, run:

go run main.go

Your terminal output should look like this:
Alt Text

Running With Docker

a. Edit your .env file like this:

DB_HOST=forum-postgres

b. Create the Dockerfile for development:
In the project root (path: /forum-backend/), create the Dockerfile

touch Dockerfile

You can rename the example-Dockerfile.dev(from the repo) to Dockerfile

c. Create the docker-compose.yml file for development
In the project root (path: /forum/), create the docker-compose.yml

touch docker-compose.yml

You can also rename the example-docker-compose.dev.yml to docker-compose.yml

d. Run the app:
Open the terminal and run:

docker-compose up --build

e. You can use pgadmin to view your database.
Look up this article I wrote for a guide here

Step 13: Writing Unit and Integration Tests

The API is 99.9% tested.

Golang has a beautiful term called Table Testing.
That term might not sound familiar if you are coming from the NodeJS/PHP/Python/Ruby world.
Table testing in Go, give the Developer the privilege of testing all edge cases of a particular functionality just with one test function.
This is what I mean, Imagine a user signing up. What could possibly go wrong?

  • The user might input an invalid email
  • The user might input a password that does not meet the requirement
  • The user might input an email that belongs to someone else in our database.
    • and so on.

With the power of Table Tests, you can test all the cases with one test function, instead of writing multiple functions with more lines of code to worry about.

Tests Set up

Remember, we created a tests directory at the start of the project.
Inside the tests directory, create the setup_test.go

touch setup_test.go

Since you will be running this tests in your local, let your TestMain and Database functions look like this:

func TestMain(m *testing.M) {
    var err error
    err = godotenv.Load(os.ExpandEnv("./../.env"))
    if err != nil {
        log.Fatalf("Error getting env %v\n", err)
    }

    Database()

    os.Exit(m.Run())

}

func Database() {

    var err error

    TestDbDriver := os.Getenv("TEST_DB_DRIVER")
    if TestDbDriver == "mysql" {
        DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", os.Getenv("TEST_DB_USER"), os.Getenv("TEST_DB_PASSWORD"), os.Getenv("TEST_DB_HOST"), os.Getenv("TEST_DB_PORT"), os.Getenv("TEST_DB_NAME"))
        server.DB, err = gorm.Open(TestDbDriver, DBURL)
        if err != nil {
            fmt.Printf("Cannot connect to %s database\n", TestDbDriver)
            log.Fatal("This is the error:", err)
        } else {
            fmt.Printf("We are connected to the %s database\n", TestDbDriver)
        }
    }
    if TestDbDriver == "postgres" {
        DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", os.Getenv("TEST_DB_HOST"), os.Getenv("TEST_DB_PORT"), os.Getenv("TEST_DB_USER"), os.Getenv("TEST_DB_NAME"), os.Getenv("TEST_DB_PASSWORD"))
        server.DB, err = gorm.Open(TestDbDriver, DBURL)
        if err != nil {
            fmt.Printf("Cannot connect to %s database\n", TestDbDriver)
            log.Fatal("This is the error:", err)
        } else {
            fmt.Printf("We are connected to the %s database\n", TestDbDriver)
        }
    }
}

...

I had to modify the repository because Circle CI could not detect the .env file that has the test database details. Please take note of this. The rest of the functions in the setup_test.go remain unchanged.

The setup_test.go file has functionalities that:

  • Initializes our testing database
  • Refresh the database before each test
  • Seed the database with relevant data before each test. This file is very handy because it will be used throughout the tests. Do well to study it.

Model Tests.

a. User Model Tests
In the tests directory, create the model_users_test.go file

touch model_users_test.go

After ensuring that your test database is created, the right user and password set and all files saved, You can go ahead and run this test. Launch your terminal in the path: /forum-backend/tests and run:

go test -v 

The v flag is for verbose output.
To run individual tests in the model_users_test.go file, Say for instance I want to run the TestSaveUser, run:

go test -v --run TestSaveUser

b. Post Model Tests
In the tests directory, create the model_posts_test.go file

touch model_posts_test.go

c. Like Model Tests
In the tests directory, create the model_likes_test.go file

touch model_likes_test.go

d. Comment Model Tests
In the tests directory, create the model_comments_test.go file

touch model_comments_test.go

Controller Tests.

a. Login Controller Test
Observe in the login_controller.go file that, the Login method depends on the SignIn method.
In the tests directory, create the controller_login_test.go file.

touch controller_login_test.go

b. Users Controller Test
Each method in the users' controller call at least one method from somewhere else. The methods that each Users Controller Method called are tested in the Unit Tests session.

In the tests directory, create the controller_users_test.go file.

touch controller_users_test.go

c. Posts Controller Test
In the tests directory, create the controller_posts_test.go file.

touch controller_posts_test.go

d. Likes Controller Test
In the tests directory, create the controller_likes_test.go file.

touch controller_likes_test.go

e. Comments Controller Test
In the tests directory, create the controller_comments_test.go file.

touch controller_comments_test.go

f. ResetPassword Controller Test
In the tests directory, create the controller_reset_password_test.go file.

touch controller_reset_password_test.go

As mentioned earlier, You can run any test in the tests directory. No test function depends on another to pass. All test functions run independently.

To run the entire test suite use:

go test -v 

You can also run tests from the main directory of the app that is, outside the tests directory(path: /forum-backend/) using:

go test -v ./...

Running Tests with Docker

If you wish to run the tests with docker, do the following:

a. Dockerfile.test file
In the root directory, create a Dockerfile.test

touch Dockerfile.test

You can rename the example.Dockerfile.test(from the repo) to Dockerfile.test

b. docker-compose.test.yml file
In the root directory, create a docker-compose.test.yml

touch docker-compose.test.yml

You can rename the example.docker-compose.test.yml(from the repo) to docker-compose.test.yml

c. Run the tests suite:
Ensure that the test database details are provided in the .env file and the Test_Host_DB is set as such:

TEST_DB_HOST=forum-postgres-test 

From the project root directory, run:

docker-compose -f docker-compose.test.yml up --build

Step 14: Continuous Integration Tools

Circle CI is used as the CI tool in this API. Another option you might consider is Travis CI.

Steps to Integrate CircleCI:
a. config.yml
In the root directory(path: /forum-backend/), create the .circleci

mkdir .circleci

Create the config.yml file inside the .circleci directory

cd .circleci && touch config.yml

b. Connect the repository
Since you have following this tutorial on your local, you can now create a github/bitbucket repository and push the code.

Login to Circle CI and choose the repo to build.
Click on start building.
After the build process, you will be notified if it succeeds or fails. For failure, check the logs in the CI environment to know why.
Go to the settings, copy the badge and add it to the README.md of your repo
For a successful build, your badge should look like mine:

Alt Text

Step 15: Deployment

I deployed a dockerized version of the app to digitalocean. The job can also be done with Amazon AWS.
This deployment process is worth a full-blown article. If you be interested in the step by step process, do well to comment, I will spin up a different article for that.

Get the Repository for the backend here

Section 2: Buiding the Frontend

You might have been waiting for the session.
This is a where you will appreciate the backend work done in Section 1

We will be using React. I would have as well decided to use Vue(which is also cool).
This frontend has zero class definition. React Hooks are used 100%.
Redux is used for state management.

The repository for the frontend is this:
https://github.com/victorsteven/Forum-App-React-Frontend

Step 1: Basic Step Up

a. Installation

To follow along from scratch, create a new React Project. Note that this project should be created outside the backend. You can create it in your Desktop, Documents, or your dedicated frontend directory.

npx create-react-app forum-frontend

Follow the instructions in the terminal after the project is created.

Change to the forum-frontend directory:

cd forum-frontend

And start the app:

npm start

Visit on the browser:

  http://localhost:3000

Please note that I will be as concise as possible.

b. Install External Packages.
We installed packages like axios, moment, and so on.
To be brief, use the content in the project package.json file:

Then run:

npm update

c. API Url
The Backend is totally standalone from the Frontend
So a means of communication is needed.
Inside the src directory, create the apiRoute.js file:

cd src && touch apiRoute.js


Not, from the above file, the production URL for the forum app is used, you can as well change it to yours if you have hosted a backend somewhere.

d. Authorization
Authenticated will be needed for some requests in the app.
Take, for instance, a user needs to be authenticated to create a post.
Since axios is used for api calls(sending requests to the backend), we need to send the authenticated user's authorization token to each request they make. Instead of adding the authorization token manually, let's do it automatically.
Inside the src directory, create the authorization directory:

mkdir authorization

Create the authorization.js file inside the authorization directory

cd authorization && touch authorization.js

e. History
We may need to call redirection from our redux action.
This is what I mean: When a user creates a post, redirect him to the list of posts available.
To achieve this, we will use the createBrowserHistory function from the history package.

Inside the src directory, create the history.js file:

touch history.js

f. Assets
For each newly registered user, a default avatar is used as their display image.
Inside the src directory, create the assets directory:

mkdir assets

Add the avatar below in the assets directory. You can rename it to Default.png

Alt Text

Step 2: Wiring up our Store

As said earlier, we will be using redux for state management. And I think it is best the store is fired up before we start calling components that we will create later.
Inside the src directory, create the store directory:

cd src && mkdir store

Inside the store directory, create the modules directory:

cd store && mkdir modules

a. The Authentication Store

Inside the modules directory, create the auth directory:

cd modules && mkdir auth

Inside the auth directory, create these directories and files as shown in the image below:

Alt Text

i. auth/actions/authActions.js

ii. auth/authTypes/index.js

iii. auth/reducer/authReducer.js

b. The Posts Store

Inside the modules directory, create the posts directory:

mkdir posts

Inside the posts directory, create these directories and files as shown in the image below:

Alt Text

i. posts/actions/postsActions.js

ii. posts/postsTypes/index.js

iii. posts/reducer/postsReducer.js

c. The Likes Store

Inside the modules directory, create the likes directory:

mkdir likes

Inside the likes directory, create these directories and files as shown in the image below:

Alt Text

i. likes/actions/likesActions.js

ii. likes/likeTypes/index.js

iii. likes/reducer/likesReducer.js

d. The comments Store

Inside the modules directory, create the comments directory:

mkdir comments

Inside the comments directory, create these directories and files as shown in the image below:

Alt Text

i. comments/actions/commentsActions.js

ii. comments/commentTypes/index.js

iii. comments/reducer/commentsReducer.js

e. The Combined Reducer

We will need to combine the reducers from each of the stores defined above.
Inside the modules directory(path: /src/store/modules/), create the index.js file.

touch index.js

f. The Store File

This is the file that a kind of wraps up the store.

  • The combined reducer is called
  • We applied the thunk middleware
  • Enabled Redux DevTools

In the store directory(path: /src/store/), create the index.js file.

touch index.js

Step 3: Wiring Up The Components

Inside the src directory, create the components directory

cd src && mkdir components

Navigation Component

This component takes us wherever we want in the app.

a. Navigation
Inside the components directory, create the Navigation component

cd components && touch Navigation.js

b. Navigation.css
Inside the components directory, create the Navigation.css file

Utils Component

Inside the components directory, create the utils directory

mkdir utils

a. Message: This is the notification component.
Create a Message.js file inside the utils directory:

cd utils && touch Message.js

Auth Component

This is the component that will house our authentication.
Inside the components directory, create the auth directory

mkdir auth

a. Signup: A user can register on the app.
Create a Register.js file inside the auth directory:

cd auth && touch Register.js

b. Login: A user can log in.
Create a Login.js file inside the auth directory:

touch Login.js

c. Auth.css Add styling to auth files.
Create a Auth.css file inside the auth directory:

touch Auth.css

Users Component

The user can update his profile picture, change his email address, request to change his password, and so on.
Inside the components directory, create the users directory

mkdir users

a. Profile: A user can update his profile.
Inside the users directory, create the Profile.js component:

cd users && touch Profile.js

b. Profile.css. Add the profile css file.
Inside the users directory, create the Profile.css file:

touch Profile.css

c. ForgotPassword: A user can request to change their forgotten password.
Inside the users directory, create the ForgotPassword.js component:

touch ForgotPassword.js

d. ResetPassword: A user can reset their password.
Inside the users directory, create the ResetPassword.js component:

touch ResetPassword.js

Posts Component

An authenticated user can create/edit/delete posts they created.
Inside the components directory, create the posts directory

mkdir posts

a. Posts: A user can view all posts.
Inside the posts directory, create the Posts.js component:

cd posts && touch Posts.js

b. Post: This a single component inside the Posts component
Inside the posts directory, create the Post.js component:

touch Post.js

c. PostDetails: A user can visit a particular post.
Inside the posts directory, create the PostDetails.js component:

touch PostDetails.js

d. CreatePost: An authenticated user can create a post.
Inside the posts directory, create the CreatePost.js component:

touch CreatePost.js

e. EditPost: An authenticated user can edit their post.
Inside the posts directory, create the EditPost.js component:

touch EditPost.js

f. DeletePost: An authenticated user can delete the post they created.
Inside the posts directory, create the DeletePost.js component:

touch DeletePost.js

g. AuthPosts: An authenticated user view all the posts they created.
Inside the posts directory, create the AuthPosts.js component:

touch AuthPosts.js

h. AuthPost: This is a single component inside the AuthPosts component.
Inside the posts directory, create the AuthPost.js component:

touch AuthPost.js

i. Posts.css: This is CSS file for the above components.

Likes Component

Inside the components directory, create the likes directory

mkdir likes

a. Likes: An authenticated user can like a post or unlike already liked post.
Inside the likes directory, create the Likes.js component:

cd likes && touch Likes.js

Comments Component

An authenticated user can create/edit/delete comments they created.
Inside the components directory, create the comments directory

mkdir comments

a. Comments: A user can view all comments for a post.
Inside the comments directory, create the Comments.js component:

cd comments && touch Comments.js

b. Comment: This a single component inside the Comments component.
Inside the comments directory, create the Comment.js component:

touch Comment.js

c. CreateComment: An authenticated user can create a comment.
Inside the comments directory, create the CreateComment.js component:

touch CreateComment.js

d. EditComment: An authenticated user can edit their comment.
Inside the comments directory, create the EditComment.js component:

touch EditComment.js

e. DeleteComment: An authenticated user can delete their comment.
Inside the comments directory, create the DeleteComment.js component:

touch DeleteComment.js

Dashboard Component

This is the entry component of the application.
Inside the components directory, create the Dashboard.js component

touch Dashboard

Step 4: Wiring Up The Route

If routing is not in place, we cannot navigate to the different components we have.
In the src directory, create the Route.js file

touch Route.js

Step 4: Wiring Up The App Main Entry

All that is done above, from the store* to the routing need to connect at some point.
This is done in the index.js file in the src directory.

Edit the index.js file in the src directory

Also, edit the index.css file in the src directory. This file has just once CSS class color-red. This is used in all components that error is displayed

Fire up your terminal and run http://localhost:3000

Welcome to the App.

Step 4: Deployment

The frontend is deployed using Netlify
before you deploy, in the public directory(path: forum-frontend/public), create the _redirects file

touch _redirects

File content:

/*    /index.html   200

Steps to deploy:

  • Create a new github repo(different from the backend)
  • Push the frontend code to the repo
  • Login to your Netlify account and connect the frontend repo.
  • Give it sometime to deploy.

Note the following:

  • For the backend to work with the deployed frontend, it needs to be deployed also to a live server(digitalocean, aws, heroku, etc).
  • Make sure that url for the backend is not just the ip address. you can get a domain name and make sure https is enabled
  • You can update the apiRoute file and add your backend url

Conclusion

I tried as concise as possible to avoid a 2 hours or so read.

This is the visit the production application
https://seamflow.com
You can visit and try all that you learned in this article.

Also, get the github repositories

Don't forget to drop a star.

You can ask me personal questions on questions on twitter

You might also like to check out my other articles about go, docker, kubernetes here

Thanks.

Posted on by:

Discussion

markdown guide
 
 

This one realy help me alot to understand go web, thank you Steven