Steven Victor
Posted on November 1, 2019
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:
- https://github.com/victorsteven/Forum-App-Go-Backend (Backend)
- https://github.com/victorsteven/Forum-App-React-Frontend (Frontend)
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:
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:
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:
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
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:
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:
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:
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:
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
- https://github.com/victorsteven/Forum-App-Go-Backend (Backend)
- https://github.com/victorsteven/Forum-App-React-Frontend (Frontend)
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 November 1, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.