Full Stack Application with Go, Gin, React, and MongoDB

Nick Latham
Geek Culture
Published in
15 min readAug 30, 2021

--

Hi, how’s it going! In this tutorial we will go over how to build a full stack application using Go, ReactJS and MongoDB that will run on your local machine. Whether you’re new to Go, ReactJS, or MongoDB, or just new to web developement in general, this tutorial is a great place to start in order to get exposure to these components and see how they all fit togethor. For our project, we will be creating an application that will manage restaurant orders.

Go

We will be using Go as our backend web server and we will be building an API with basic CRUD (Create, Read, Update, Delete) functionality that our frontend application can interface with.

We will be using the Gin framework, more information about that framework can be found here.

If you haven’t already downloaded Go, go ahead (pun intended) and download it at the Go website here. The official Go website also provides tutorials if you want to learn more about Go. W3Schools also has a great chapter on Go.

React

For our frontend application, we will be using React, which is a great JavaScript library for building user interfaces.

To use React you’ll need to have Node downloaded as well as NPM.

If you want to learn more about React, check out the segment on React that W3Schools offers here.

MongoDB

Lastly, we will be using MongoDB as our database. MongoDB is a document-oriented database program, which means that instead using SQL, it uses JSON to store information. It’s also has a free tier (which we’ll be using), so its a pretty great database choice for any project.

MongoDB doesn’t require any downloads, but I recommend downloading Postman, which is a handy tool for testing our API.

Setting up the database

Ok, now that you’re up to speed on our stack and have hopefully downloaded all the tools necessary, let’s set up our database!

First head over to MongoDB and sign in/sign up. Once you’re in, your home screen should look something like this:

Go ahead and create a new database by clicking on “Build a Database” and selecting the shared option.

Keep the default options and select “Create Cluster.”

Once your cluster is created, click on the “Connect” button.

Select your IP address and create a username and password. Then click on “Choose Connection Method”

Select the “Connect your application” option.

Save the connection string and click “Close”

The connection string is the URL endpoint where our backend API will communicate with the database.

Replace <password> with your password and myFirstDatabase with your cluster name.

mongodb+srv://admin:mypassword@cluster0.56rq1f.mongodb.net/Cluster0?retryWrites=true&w=majority

Now we’re done with the database portion and we can head over to the Backend portion of our application.

Backend API

Now that we have our database set up and ready to go, let’s set up our API. First we are going to create our folder which will hold all our server files. Open up your terminal, navigate to your project folder, create a new folder called “server”, and enter it.

Next we are going to create our mod file. This will hold all the configurations for our server.

Type: go mod init server and press enter.

This creates a package called server in your folder as well as creates a go.mod file. We need to add the packages for MongoDB and Gin and we do this by opening up go.mod and adding in the following lines of code:

require (
github.com/gin-gonic/gin v1.7.3
github.com/go-playground/validator/v10 v10.4.1
github.com/joho/godotenv v1.3.0
go.mongodb.org/mongo-driver v1.7.1
)

Your go.mod file should look like this:

Next create a file called main.go within your server directory and add the following code to it:

In main.go, we are first getting our imports. As you can see we are importing the gin framework as well as a package called routes which we will create later. Within the main function we first set up our ports and then following that we set up our router. We also have within this function the different endpoints that our API will have to do CRUD operations.

Before we move on, lets create our .env folder. This will hold our secure information. Create a file called .env within your server directory and add the following lines where MONGODB_URL= has your mongodb endpoint from earlier:

PORT=5000
MONGODB_URL=mongodb+srv://admin:Password@cluster0.655rf.mo
ngodb.net/Cluster0?retryWrites=true&w=majority

If you’re pushing your changes to GitHub, make sure to add the .env file to your .gitignore file.

Next we will create our data model for restaurant orders. Within the server directory, create a new directory called models and within it create a file called order.go and add the following code to it:

Within order.go, in the first line we have “package models”. This creates the package so that we can reference to it in other areas of the code. Next we are importing bson/primitive so that we can create an ObjectID unique id for each order. After that we have the schema for our order. As you can see, each order will have a unique id, a dish, a price, a server, and a table.

With our schema created, lets create our routes. Back in our server directory, create another directory called routes. Remember how we imported server/routes in main.go? This is it. Go ahead and create a file called connections.go which will handle our connection to the database and enter the following code:

In connections.go, as with all our other files, we have the imports that we will be using within this file. Next we have the function DBInstance, which takes the mongodb url stored in the .env file and creates a connection. The other function, OpenCollection, takes in a collection name and opens up a collection.

The next file that we are going to create within our routes folder is orders.go. This file will hold all the functions for our orders model that handle api requests for the various endpoints that we set up in main.go.

In orders.go, first we are going to add the imports and create a couple of variables:

package routesimport (
"context"
"fmt"
"net/http"
"time"

"server/models"
"github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/bson"
"github.com/gin-gonic/gin"
)
var validate = validator.New()
var orderCollection *mongo.Collection = OpenCollection(Client, "orders")

The variable validate allows us to ensure that the order data that we receive when creating or updating an order is correct, and orderCollection opens a new collection for us to use called “orders”.

Next we are going to create a function to handle creating new orders called AddOrder:

//add an order
func AddOrder(c *gin.Context) {
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
var order models.Order

if err := c.BindJSON(&order); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

validationErr := validate.Struct(order)
if validationErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.Error()})
return
}
order.ID = primitive.NewObjectID()

result, insertErr := orderCollection.InsertOne(ctx, order)
if insertErr != nil {
msg := fmt.Sprintf("order item was not created")
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

defer cancel()
c.JSON(http.StatusOK, result)
}

Within AddOrder (Side Note: functions that that have the first letter capitalized are considered public and otherwise they are considered private), We are creating the context with a timeout. Next we are creating our orders variable and populating it with the data provided by the user. Next we validate that data and then we create our unique id for the order. Finally we insert the new order object into the database and return the result.

The next function that we are going to create is GetOrders, which will return all the orders within the collection:

//get all orders
func GetOrders(c *gin.Context){
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
var orders []bson.M
cursor, err := orderCollection.Find(ctx, bson.M{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err = cursor.All(ctx, &orders); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

defer cancel()

fmt.Println(orders)
c.JSON(http.StatusOK, orders)
}

This function is pretty simple, we create an array of orders, and populate those orders from data in the database using orderCollection.Find. Like the AddOrder function and all the rest of the functions we are going to be creating, we are creating a context with a timeout and doing error checks at each step.

The next function is GetOrdersByWaiter and this function builds off of GetOrders. It takes in a waiter’s name and returns all the orders that the waiter has.

//get all orders by the waiter's name
func GetOrdersByWaiter(c *gin.Context){

waiter := c.Params.ByName("waiter")
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
var orders []bson.M
cursor, err := orderCollection.Find(ctx, bson.M{"server": waiter})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err = cursor.All(ctx, &orders); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cancel() fmt.Println(orders) c.JSON(http.StatusOK, orders)
}

As you can see, it is pretty similar to the previous function with a couple of exceptions. First we are taking in a parameter called waiter and secondly, we are using that parameter as a filter within orderCollection.Find.

Our last GET function is GetOrderById which will take in an order id and return a single order.

//get an order by its id
func GetOrderById(c *gin.Context){
orderID := c.Params.ByName("id")
docID, _ := primitive.ObjectIDFromHex(orderID)
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) var order bson.M if err := orderCollection.FindOne(ctx, bson.M{"_id": docID}).Decode(&order); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cancel() fmt.Println(order) c.JSON(http.StatusOK, order)
}

The first thing that we do in GetOrderById is to get the id parameter and convert it to an ObjectID object. Then we use orderCollection.FindOne to find the order matching the order id.

The next couple of functions will be our PUT functions, which will handle updating orders. The first function that we will implement is UpdateWaiter, which takes in an id as a parameter as well as simple json object for the waiter, and replaces the order found with the order id to have the new waiter’s name.

//update a waiter's name for an order
func UpdateWaiter(c *gin.Context){
orderID := c.Params.ByName("id")
docID, _ := primitive.ObjectIDFromHex(orderID)
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) type Waiter struct {
Server *string `json:"server"`
}
var waiter Waiter if err := c.BindJSON(&waiter); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := orderCollection.UpdateOne(ctx, bson.M{"_id": docID},
bson.D{
{"$set", bson.D{{"server", waiter.Server}}},
},
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cancel() c.JSON(http.StatusOK, result.ModifiedCount)
}

In UpdateWaiter, we first get the id parameter and convert it to a ObjectID object. Then we create a new struct for our waiter and fill that with the data that the user provided. After that we use orderCollection.UpdateOne to update the order with the waiter’s name.

The next function we are going to create is called UpdateOrder. This will be similar to the previous function except that this will take in an order object and replace an existing order with this new order.

//update the order
func UpdateOrder(c *gin.Context){
orderID := c.Params.ByName("id")
docID, _ := primitive.ObjectIDFromHex(orderID)
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) var order models.Order
if err := c.BindJSON(&order); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
validationErr := validate.Struct(order)
if validationErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.Error()})
return
}
result, err := orderCollection.ReplaceOne(
ctx,
bson.M{"_id": docID},
bson.M{
"dish": order.Dish,
"price": order.Price,
"server": order.Server,
"table": order.Table,
},
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

defer cancel()
c.JSON(http.StatusOK, result.ModifiedCount)
}

Within UpdateOrder, we take in the id as a parameter and the order data which we then validate. After that we use orderCollection.ReplaceOne to replace the order with the new order at that id.

Our last function that we will create is the DeleteOrder function. This will take in an id as a parameter and delete the order matching that id.

//delete an order given the id
func DeleteOrder(c * gin.Context){
orderID := c.Params.ByName("id")
docID, _ := primitive.ObjectIDFromHex(orderID)
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) result, err := orderCollection.DeleteOne(ctx, bson.M{"_id": docID})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cancel() c.JSON(http.StatusOK, result.DeletedCount)
}

As you can see, we are using orderCollection.DeleteOne to delete the order matching the id.

The full file put togethor is shown below:

We have created all our files and our file structure should look like this:

Now that we have all our files, we can build and run our server. First navigate to your server directory and enter the following commands:

go mod tidy
go build main.go
go run main.go

“go mod tidy” adds the imports, “go build main.go” builds the project, and “go run main.go” runs the project. Your output should look similar to this:

To test these url endpoints we can use postman.

Lets create a few orders:

Creating an order of french fries for waiter: john
Creating an order of chicken nuggets for waiter: Dave
Creating another order for dave

Now let’s get the orders we created. First lets get all the orders:

All the orders

Next let’s get the orders for waiter Dave:

All of Dave’s orders

Now lets only get one order:

Lets test out our update endpoints. Lets update the chicken nuggets order that we created and then assign a different waiter to it. For demonstration purposes we are using two different actions, although we could just use one update order function.

Updating the chicken nuggets order
updating the order to have a new waiter: luke
Test our changes using the get order endpoint

Finally let’s test our delete endpoint.

Deleting the order for which the waiter is Luke
Testing to make sure that the entry was deleted.

Great! Now that we have our web server set up, we can implement our frontend application.

Front End

For the front end we will be using React which is a great framework for UI. To start, lets navigate to the main project folder and create a new react project by typing “npx create-react-app frontend” which will create a new app called frontend.

Once the app is created, navigate to the frontend directory and enter the following commands

npm install axios
npm install react-bootstrap@next bootstrap@5.1.0

We will be using axios to handle our requests to the server, and react-bootstrap is for our UI styling.

Within our /src directory create a new folder called “components” and in that folder create two new files, orders.components.js and single-order.component.js

within single-order.components.js, add the following code:

import React, {useState, useEffect} from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import {Button, Card, Row, Col} from 'react-bootstrap'
const Order = ({orderData, setChangeWaiter, deleteSingleOrder, setChangeOrder}) => {
return (
)
}
export default Order

Our order component will take in the order data, a function to delete an order, and functions to ready the order for updating the waiter or changing the order. We’ll come back to it but for now add the following code to orders.components.js:

import React, {useState, useEffect} from 'react';
import axios from "axios";
import {Button, Form, Container, Modal } from 'react-bootstrap'
import Order from './single-order.component';
const Orders = () => {
return (
)
}
export default Orders

This component will display all the orders on our webpage. Go back one directory and change App.js to the following so that it will call the Orders component:

Go back to single-order.component.js and after the return statement, add these two functions.

function changeWaiter(){
setChangeWaiter({
"change": true,
"id": orderData._id
})
}
function changeOrder(){
setChangeOrder({
"change": true,
"id": orderData._id
})
}

These will set the values of setChangeWaiter and setChangeOrder and alert the Order component to display the popups for changing the data. Within the return statement, add the following code:

<Card>
<Row>
<Col>Dish:{ orderData !== undefined && orderData.dish}</Col>
<Col>Server:{ orderData !== undefined && orderData.server </Col>
<Col>Table:{ orderData !== undefined && orderData.table}</Col>
<Col>Price: ${orderData !== undefined && orderData.price}</Col>
<Col><Button onClick={() => deleteSingleOrder(orderData._id)}>delete order</Button></Col>
<Col><Button onClick={() => changeWaiter()}>change waiter</Button></Col>
<Col><Button onClick={() => changeOrder()}>change order</Button></Col>
</Row>
</Card>

This will display the row within the webpage which will information about the order as well as buttons to delete, change, and update the waiter.

Your completed file should look like this:

Now go to your orders.component.js file and at the beginning of the component, add the following code:

const [orders, setOrders] = useState([])
const [refreshData, setRefreshData] = useState(false)
const [changeOrder, setChangeOrder] = useState({"change": false, "id": 0})
const [changeWaiter, setChangeWaiter] = useState({"change": false, "id": 0})
const [newWaiterName, setNewWaiterName] = useState("")
const [addNewOrder, setAddNewOrder] = useState(false)
const [newOrder, setNewOrder] = useState({"dish": "", "server": "", "table": 0, "price": 0})

These will be our variables to control adding, updating, showing, and deleting orders. Each variable comes with a setter that when called, refreshes the page.

After the return statement add the following functions which will use axios to submit requests to the server for our various functionalities:

//changes the waiter
function changeWaiterForOrder(){
changeWaiter.change = false
var url = "http://localhost:5000/waiter/update/" + changeWaiter.id
axios.put(url, {
"server": newWaiterName
}).then(response => {
console.log(response.status)
if(response.status == 200){
setRefreshData(true)
}
})
}
//changes the order
function changeSingleOrder(){
changeOrder.change = false;
var url = "http://localhost:5000/order/update/" + changeOrder.id
axios.put(url, newOrder)
.then(response => {
if(response.status == 200){
setRefreshData(true)
}
})
}
//creates a new order
function addSingleOrder(){
setAddNewOrder(false)
var url = "http://localhost:5000/order/create"
axios.post(url, {
"server": newOrder.server,
"dish": newOrder.dish,
"table": newOrder.table,
"price": parseFloat(newOrder.price)
}).then(response => {
if(response.status == 200){
setRefreshData(true)
}
})
}
//gets all the orders
function getAllOrders(){
var url = "http://localhost:5000/orders"
axios.get(url, {
responseType: 'json'
}).then(response => {
if(response.status == 200){
setOrders(response.data)
}
})
}
//deletes a single order
function deleteSingleOrder(id){
var url = "http://localhost:5000/order/delete/" + id
axios.delete(url, {
}).then(response => {
if(response.status == 200){
setRefreshData(true)
}
})
}

Then right above the return statement add this:

//gets run at initial loadup
useEffect(() => {
getAllOrders();
}, [])
//refreshes the page
if(refreshData){
setRefreshData(false);
getAllOrders();
}

UseEffect is called initially to get all the data and since it has [] at the end, it will only get called once. Then the if statement checks at each refresh to see if the orders need to be updated through refreshData. SetRefreshData() is called at the successful completion of each call to the server.

Now inside your return statement, add the following code:

<div>
{/* add new order button */}
<Container>
<Button onClick={() => setAddNewOrder(true)}>Add new order</Button>
</Container>
{/* list all current orders */}
<Container>
{orders != null && orders.map((order, i) => (
<Order orderData={order} deleteSingleOrder={deleteSingleOrder} setChangeWaiter={setChangeWaiter} setChangeOrder={setChangeOrder}/>
))}
</Container>
{/* popup for adding a new order */}
<Modal show={addNewOrder} onHide={() => setAddNewOrder(false)} centered>
<Modal.Header closeButton>
<Modal.Title>Add Order</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group>
<Form.Label >dish</Form.Label>
<Form.Control onChange={(event) => {newOrder.dish = event.target.value}}/>
<Form.Label>waiter</Form.Label>
<Form.Control onChange={(event) => {newOrder.server = event.target.value}}/>
<Form.Label >table</Form.Label>
<Form.Control onChange={(event) => {newOrder.table = event.target.value}}/>
<Form.Label >price</Form.Label>
<Form.Control type="number" onChange={(event) => {newOrder.price = event.target.value}}/>
</Form.Group>
<Button onClick={() => addSingleOrder()}>Add</Button>
<Button onClick={() => setAddNewOrder(false)}>Cancel</Button>
</Modal.Body>
</Modal>
{/* popup for changing a waiter */}
<Modal show={changeWaiter.change} onHide={() => setChangeWaiter({"change": false, "id": 0})} centered>
<Modal.Header closeButton>
<Modal.Title>Change Waiter</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group>
<Form.Label >new waiter</Form.Label>
<Form.Control onChange={(event) => {setNewWaiterName(event.target.value)}}/>
</Form.Group>
<Button onClick={() => changeWaiterForOrder()}>Change</Button>
<Button onClick={() => setChangeWaiter({"change": false, "id": 0})}>Cancel</Button>
</Modal.Body>
</Modal>
{/* popup for changing an order */}
<Modal show={changeOrder.change} onHide={() => setChangeOrder({"change": false, "id": 0})} centered>
<Modal.Header closeButton>
<Modal.Title>Change Order</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group>
<Form.Label >dish</Form.Label>
<Form.Control onChange={(event) => {newOrder.dish = event.target.value}}/>
<Form.Label>waiter</Form.Label>
<Form.Control onChange={(event) => {newOrder.server = event.target.value}}/>
<Form.Label >table</Form.Label>
<Form.Control onChange={(event) => {newOrder.table = event.target.value}}/>
<Form.Label >price</Form.Label>
<Form.Control type="number" onChange={(event) => {newOrder.price = parseFloat(event.target.value)}}/>
</Form.Group>
<Button onClick={() => changeSingleOrder()}>Change</Button>
<Button onClick={() => setChangeOrder({"change": false, "id": 0})}>Cancel</Button>
</Modal.Body>
</Modal>
</div>

Your completed code should look like this:

Now that we have all the code, go to the /frontend directory within your terminal and run “npm start” This will run the webpage in your browser and you should be able to run the completed project.

adding an order
viewing all open orders
Changing the waiter

Thanks for reading this article! The GitHub repo with the completed code is located here: https://github.com/nlatham1999/GoApp. Please feel free to comment with your thoughts and questions!

If you want additional resources, I provided several links below:

Go and MongoDB:
https://www.mongodb.com/blog/post/quick-start-golang-mongodb-starting-and-setup
https://www.mongodb.com/blog/post/quick-start-golang--mongodb--how-to-create-documents
https://www.mongodb.com/blog/post/quick-start-golang--mongodb--how-to-read-documents
https://www.mongodb.com/blog/post/quick-start-golang--mongodb--how-to-update-documents
https://www.mongodb.com/blog/post/quick-start-golang--mongodb--how-to-delete-documents

React:
https://react-bootstrap.github.io/components/alerts
https://www.w3schools.com/react/

--

--