CodeNewbie Community 🌱

Cover image for How to build a web app backend in Go (with user authentication and database)
Ujjwal Goyal
Ujjwal Goyal

Posted on • Originally published at dev.to

How to build a web app backend in Go (with user authentication and database)

APIs, databases, user authentication. A lot goes into making a to-do list web application than I initially thought. Having done all that, I'm excited to bring it all to you! Through this blog you will learn how to build the backend of a web application, namely, how to:

  • connect a PostgreSQL database to Go
  • write APIs in Go
  • authenticate users with JSON web tokens (JWTs) via Auth0
  • deploy everything you've written to Heroku

Note: This post does not go into installing or signing up for any of the tools or services used. We will, however, see how to use them.

The complete repository can be found here.

Getting started with the database

We'll start with our database, to which we'll need to connect our APIs. Databases are ideal for storing and looking up large sets of data. If you're curious on why using databases is better than saving data to a file, I found this thread to be a brief and excellent starting point.

If you'd like a detailed starting guide to PostgreSQL with Go (or SQL in general), I'd recommend Jon Calhoun's excellent free articles.

Open your terminal, login to psql with your account, create a database and connect to it.

<!-- connect to psql -->
❯❯❯ psql -U ujjwal
Password for user ujjwal: <password>
psql (13.2 (Ubuntu 13.2-1.pgdg20.04+1))
Type "help" for help.

<!-- create database -->
ujjwal=# CREATE DATABASE blog;
CREATE DATABASE

<!-- connect to database -->
ujjwal=# \c blog;
You are now connected to database "blog" as user "ujjwal".
Enter fullscreen mode Exit fullscreen mode

We will be using 2 tables, one for the tasks and one for the users.

Our tasks will have 4 properties:

  1. A unique id to distinguish them
  2. The task itself, that is, the text content
  3. A boolean value that reflects if the task is done or not
  4. A user id to distinguish the tasks of each user

We will assign 2 properties to users:

  1. A unique distinguishing id
  2. The user email (or some other identifying feature)

The user id in the 'tasks' and 'users' table will be the same. A new user shall be assigned an id, and when they add a task, their id shall be used to distinguish it.

Note: If you don't plan to deploy your application and only use it locally for yourself, you can skip the 4th property and the users table. In this case, you won't need authentication either.

Create tables with the suitable column names and properties.

CREATE TABLE tasks (id SERIAL PRIMARY KEY, task TEXT NOT 
NULL, status BOOLEAN NOT NULL, user_uuid UUID NOT NULL);

CREATE TABLE users (email VARCHAR (255) UNIQUE NOT NULL, 
user_id UUID PRIMARY KEY);
Enter fullscreen mode Exit fullscreen mode

Hold up.

What's a UUID?

A universally unique identifier (UUID) is a 128-bit label used for information in computer systems. UUIDs are unique when generated according to the standard methods. A UUID can be used to identify something with near certainty that the identifier does not duplicate one that has already been, or will be, created to identify something else. Information labeled with UUIDs by independent parties can therefore be later combined into a single database or transmitted on the same channel, with a negligible probability of duplication. (Source: Wikipedia)

(You can use UUID as the primary key for tasks, or integers for users if you want.)

At this point, if you try to insert an email in the users table, you'll get an error. Namely, the table can't autogenerate UUIDs. This is easily remedied.

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
ALTER TABLE users ALTER COLUMN user_id SET DEFAULT 
uuid_generate_v4();
Enter fullscreen mode Exit fullscreen mode

Finally, we need to connect the two tables, so on adding an item through our API, the user_id in users is automatically inserted as user_uuid in tasks. This is done via a foreign key.

ALTER TABLE tasks ADD CONSTRAINT constraint_fk FOREIGN KEY 
(user_uuid) REFERENCES users(user_id) ON DELETE CASCADE ON 
UPDATE CASCADE;
Enter fullscreen mode Exit fullscreen mode

The CASCADE options ensure that changes in the user_id are reflected in both tables. For example, if a record is deleted from the users table, that UUID will also be deleted, and as a result all records in the tasks table with that particular UUID will also be deleted. If the UUID is modified in the users table, the tasks table will be automatically updated.

Our database is now complete! Now let's start with our backend code and connect it to the database.

Connecting Go to database

The below file structure (backend is my working directory) shows how our directory will look when we're done with everything.

backend
β”œβ”€β”€ .env
β”œβ”€β”€ .git
β”œβ”€β”€ .gitignore
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ main
β”œβ”€β”€ main.go
β”œβ”€β”€ packages
β”‚Β Β  β”œβ”€β”€ auth.go
β”‚Β Β  └── handlers.go
└── Procfile
Enter fullscreen mode Exit fullscreen mode

We'll connect to our database in a package rather than our main.go file. Here, I'm starting with handlers.go. We'll write the code for local development first (code for deployment described later).

package backend

import (
    // Includes all packages to be used in this file
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"

    "github.com/gorilla/mux" // An HTTP router
    "github.com/joho/godotenv" // For getting the env variables
    _ "github.com/lib/pq" // Postgres driver for database/sql, _ indicates it won't be referenced directly in code
)

// Constants for database, can be set in .env file
const (
    host = "localhost"
    port = 5432 // default port for PostgreSQL
)

// The struct for a task, excluding the user_uuid which is added separately.
// Tasks in JSON will use the JSON tags like "id" instead of "TaskNum".
type Item struct {
    TaskNum int    `json:"id"`
    Task    string `json:"task"`
    Status  bool   `json:"status"`
}

// Connect to PostgreSQL database and also retrieve user_id from users table
func OpenConnection() (*sql.DB, string) {
    // Getting constants from .env
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    user, ok := os.LookupEnv("USER")
    if !ok {
        log.Fatal("Error loading env variables")
    }
    password, ok := os.LookupEnv("PASSWORD")
    if !ok {
        log.Fatal("Error loading env variables")
    }
    dbname, ok := os.LookupEnv("DB_NAME")
    if !ok {
        log.Fatal("Error loading env variables")
    }


    // connecting to database
    // 1. creating the connection string
    psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)

    // 2. validates the arguments provided, doesn't create connection to database
    db, err := sql.Open("postgres", psqlInfo)
    if err != nil {
        panic(err)
    }

    // 3. actually opens connection to database
    err = db.Ping()
    if err != nil {
        panic(err)
    }

    // add email to users table if not present
    email := GetEmail()
    addEmail := `INSERT INTO users (email) VALUES ($1) ON CONFLICT (email) DO NOTHING;`
    _, err = db.Exec(addEmail, email)
    if err != nil {
        panic(err)
    }

    // get user_id
    var userId string
    getUser := `SELECT user_id FROM users WHERE email = $1;`
    err = db.QueryRow(getUser, email).Scan(&userId)
    if err != nil {
        panic(err)
    }

    return db, userId
}

func GetEmail() string {
    // To be explained, related to authentication
}
Enter fullscreen mode Exit fullscreen mode

We will look at the GetEmail() function later, as it is related to authentication. For now, know that it gives us the user email. We also retrieve user_id in this step, which we'll require for our APIs.

The "github.com/joho/godotenv" package is for loading our .env file in which we'll store our credentials that we cannot hardcode into the package or upload for public view. Add constants to .env file in the format

USER=username
PASSWORD=password
Enter fullscreen mode Exit fullscreen mode

and so on.

addEmail and getUser are SQL statements without hardcoded values, which prevents SQL injection.

db.Exec(addEmail, email) executes the query (inserting email into table if not already present) without returning any rows.
db.QueryRow(getUser, email) queries and returns the user_id of the given email, followed by the Scan which copies the user_id into the variable 'userId' defined by us.

IMPORTANT: Make sure you add a .gitignore file to your directory, and to it add your .env file. This will avoid sharing your credentials with others if you upload your project somewhere public.

Writing APIs

Application Programming Interfaces (APIs) allow applications to communicate with each other. They define the types of requests that can be made, how to make them, data formats to be used, etc. (Source: Wikipedia)

In our application, the React frontend (the client) will make a request to the APIs that we write in Go, which will then send a response with the relevant information (the server).

Let's start with getting the complete list of tasks a user has (we're continuing in the handlers.go file).

// Get complete list of tasks
var GetList = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Set header to json content, otherwise data appear as plain text
    w.Header().Set("Content-Type", "application/json")

    // Connect to database and get user_id
    db, userId := OpenConnection()

    // Return all tasks (rows) as id, task, status where the user_uuid of the task is the same as user_id we have obtained in the previous step
    rows, err := db.Query("SELECT id, task, status FROM tasks JOIN users ON tasks.user_uuid = users.user_id WHERE user_id = $1;", userId)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }
    defer rows.Close()
    defer db.Close()

    // Initializing slice like this and not "var items []Item" because aforementioned method returns null when empty thus leading to errors, 
    // while used method returns empty slice
    items := make([]Item, 0)
    // Add each task to array of Items
    for rows.Next() {
        var item Item
        err := rows.Scan(&item.TaskNum, &item.Task, &item.Status)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            panic(err)
        }
        items = append(items, item)
    }

    // Output with indentation
    // convert items into byte stream
    itemBytes, _ := json.MarshalIndent(items, "", "\t")
    // write to w
    _, err = w.Write(itemBytes)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        panic(err)
    }

    w.WriteHeader(http.StatusOK) 

    // Alternatively, output without indentation
    // NewEncoder: WHERE should the encoder write to
    // Encode: encode WHAT
    // _ = json.NewEncoder(w).Encode(items)
})
Enter fullscreen mode Exit fullscreen mode

Some additional information about defer statements and the "StatusOK" header:

Defer statement is used to execute a function call just before the surrounding function where the defer statement is present returns. (Source: Golangbot)
200 OK is the standard response for successful HTTP requests. (Source: Wikipedia)

When this API is called successfully, it will return all the tasks the user has entered so far.

Similarly, we write our other APIs.

// Add new task
var AddTask = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Set header to json content, otherwise data appear as plain text
    w.Header().Set("Content-Type", "application/json")

    // decode the requested data to 'newTask'
    var newTask Item

    // NewDecoder: Decode FROM WHERE
    // Decode: WHERE TO STORE the decoded data
    err := json.NewDecoder(r.Body).Decode(&newTask)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    db, userId := OpenConnection()
    defer db.Close()

    sqlStatement := `INSERT INTO tasks (task, status, user_uuid) VALUES ($1, $2, $3) RETURNING id, task, status;`

    // retrieve the task after creation from the database and store its details in 'updatedTask'
    var updatedTask Item
    err = db.QueryRow(sqlStatement, newTask.Task, newTask.Status, userId).Scan(&updatedTask.TaskNum, &updatedTask.Task, &updatedTask.Status)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    w.WriteHeader(http.StatusOK)

    // gives the new task as the output
    _ = json.NewEncoder(w).Encode(updatedTask)
})

// delete task
var DeleteTask = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Set header to json content, otherwise data appear as plain text
    w.Header().Set("Content-Type", "application/json")

    // getting the task id from the request URL
    vars := mux.Vars(r) // vars includes all variables in the request URL route. 
    // For example, in "/list/delete/{id}", "id" is a variable (of type string)

    number, err := strconv.Atoi(vars["id"]) // convert the string id to integer and assign it to variable "number"
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    db, userId := OpenConnection()
    sqlStatement := `DELETE FROM tasks WHERE id = $1 AND user_uuid = $2;`

    // Note that unlike before, we assign a variable instead of _ to the first returned value by db.Exec, 
    // as we need it to confirm that the row was deleted
    res, err := db.Exec(sqlStatement, number, userId)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    // verifying if row was deleted
    _, err = res.RowsAffected()
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    // to get the remaining tasks, same as the GET function
    rows, err := db.Query("SELECT id, task, status FROM tasks JOIN users ON tasks.user_uuid = users.user_id WHERE user_id = $1;", userId)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }
    defer rows.Close()
    defer db.Close()

    // var items []Item
    items := make([]Item, 0)
    for rows.Next() {
        var item Item
        err := rows.Scan(&item.TaskNum, &item.Task, &item.Status)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            panic(err)
        }
        items = append(items, item)
    }

    // output with indentation
    // convert items into byte stream
    itemBytes, _ := json.MarshalIndent(items, "", "\t")

    // write to w
    _, err = w.Write(itemBytes)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        panic(err)
    }

    w.WriteHeader(http.StatusOK)
})

// edit task
var EditTask = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Set header to json content, otherwise data appear as plain text
    w.Header().Set("Content-Type", "application/json")

    // get the task id from the request url
    vars := mux.Vars(r)
    number, err := strconv.Atoi(vars["id"])
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    sqlStatement := `UPDATE tasks SET task = $2 WHERE id = $1 AND user_uuid = $3 RETURNING id, task, status;`

    // decode the requested data to 'newTask'
    var newTask Item

    // NewDecoder: Decode FROM WHERE
    // Decode: WHERE TO STORE the decoded data
    err = json.NewDecoder(r.Body).Decode(&newTask)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    db, userId := OpenConnection()
    defer db.Close()

    // retrieve the task after creation from the database and store its details in 'updatedTask'
    var updatedTask Item
    err = db.QueryRow(sqlStatement, number, newTask.Task, userId).Scan(&updatedTask.TaskNum, &updatedTask.Task, &updatedTask.Status)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    w.WriteHeader(http.StatusOK)

    // gives the new task as the output
    _ = json.NewEncoder(w).Encode(updatedTask)
})

// change task status
var DoneTask = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Set header to json content, otherwise data appear as plain text
    w.Header().Set("Content-Type", "application/json")

    // get the task id from the request url
    vars := mux.Vars(r)
    number, err := strconv.Atoi(vars["id"])
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    // store current status of the task from database
    var currStatus bool

    // store updated task
    var updatedTask Item

    sqlStatement1 := `SELECT status FROM tasks WHERE id = $1 AND user_uuid = $2;`
    sqlStatement2 := `UPDATE tasks SET status = $2 WHERE id = $1 AND user_uuid = $3 RETURNING id, task, status;`

    db, userId := OpenConnection()
    defer db.Close()

    // getting current status of the task
    err = db.QueryRow(sqlStatement1, number, userId).Scan(&currStatus)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }

    // changing the status of the task
    err = db.QueryRow(sqlStatement2, number, !currStatus, userId).Scan(&updatedTask.TaskNum, &updatedTask.Task, &updatedTask.Status)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        panic(err)
    }
    w.WriteHeader(http.StatusOK)

    // gives the new task as the output
    _ = json.NewEncoder(w).Encode(updatedTask)
})
Enter fullscreen mode Exit fullscreen mode

We've written all our APIs! Now, we'll work on some authentication before bringing it all together in the main.go file.

Authentication

Note: This section was heavily derived from this post on authentication in Go, but does have some changes. If you require more help with setting up your Auth0 dashboard than specified here, it's a great resource.

  1. In your Auth0 dashboard, create a new application. The complete details will be added only after writing the frontend.
  2. Create a new API (in Applications section). The identifier is where you deploy your backend to. For running locally, it'd be something like http://localhost:8000/. Deploying to Heroku would give you something like https://YOUR-APP-NAME.herokuapp.com/. Identifier cannot be modified once set.
  3. Auth0 by default provides an email sign up, but you can add your own options in the Authentication tab. For example, if you add a GitHub sign in, you'll have to configure the GitHub developer account as well, adding relevant details to Auth0 and GitHub. The application using this connection is the one you created in step 1.
  4. Add the user email (for database) to the JSON web token by adding a custom "Add email to access token" rule (in the Auth pipeline section). This rule is provided by Auth0. All you need to do is give your own "namespace" to it, which is basically a unique identifier to recognize the email. It could be as simple as "thisismyemail". You can decode and play around with JWTs here. Add the complete namespace to .env file as "NAMESPACE_DOMAIN".

For now, that's all we have to do in our dashboard.
Now, let's make a new file auth.go (in the same directory as handlers.go).

package backend

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"

    "github.com/auth0/go-jwt-middleware"
    "github.com/form3tech-oss/jwt-go"
)

type Jwks struct {
    Keys []JSONWebKeys `json:"keys"`
}

// holds fields related to the JSON Web Key Set for the API. These keys contain the public keys, which will be used to verify JWTs
type JSONWebKeys struct {
    Kty string   `json:"kty"`
    Kid string   `json:"kid"`
    Use string   `json:"use"`
    N   string   `json:"n"`
    E   string   `json:"e"`
    X5c []string `json:"x5c"`
}

// declaring variable outside function
var GetToken map[string]interface{}

func Middleware() (*jwtmiddleware.JWTMiddleware, map[string]interface{}) {
    // jwtMiddleware is a handler that will verify access tokens
    jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
        ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
            // get token for database
            // assigning value to earlier declared variable through pointer
            p := &GetToken
            *p = token.Claims.(jwt.MapClaims)

            // Verify 'aud' claim
            // 'aud' = audience (where you deploy the backend, either locally or Heroku)
            aud := "YOUR_API_IDENTIFIER"

            // convert audience in the JWT token to []interface{} if multiple audiences
            convAud, ok := token.Claims.(jwt.MapClaims)["aud"].([]interface{})
            if !ok {
                // convert audience in the JWT token to string if only 1 audience
                strAud, ok := token.Claims.(jwt.MapClaims)["aud"].(string)
                // return error if can't convert to string
                if !ok {
                    return token, errors.New("Invalid audience.")
                }
                // return error if audience doesn't match
                if strAud != aud {
                    return token, errors.New("Invalid audience.")
                }
            } else {
                for _, v := range convAud {
                    // verify if audience in JWT is the one you've set
                    if v == aud {
                        break
                    } else {
                        return token, errors.New("Invalid audience.")
                    }
                }
            }

            // Verify 'iss' claim
            // 'iss' = issuer
            iss := "https://YOUR_DOMAIN/"
            checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
            if !checkIss {
                return token, errors.New("Invalid issuer.")
            }

            cert, err := getPemCert(token)
            if err != nil {
                panic(err.Error())
            }

            result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
            return result, nil
        },
        SigningMethod: jwt.SigningMethodRS256,
    })
    return jwtMiddleware, GetToken
}

// function to grab JSON Web Key Set and return the certificate with the public key
func getPemCert(token *jwt.Token) (string, error) {
    cert := ""
    resp, err := http.Get("https://YOUR_DOMAIN/.well-known/jwks.json")

    if err != nil {
        return cert, err
    }
    defer resp.Body.Close()

    var jwks = Jwks{}
    err = json.NewDecoder(resp.Body).Decode(&jwks)

    if err != nil {
        return cert, err
    }

    for k := range jwks.Keys {
        if token.Header["kid"] == jwks.Keys[k].Kid {
            cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
        }
    }

    if cert == "" {
        err := errors.New("Unable to find appropriate key.")
        return cert, err
    }

    return cert, nil
}
Enter fullscreen mode Exit fullscreen mode

Note: I have slightly modified the middleware from the linked post to check for audience (original method couldn't convert slices correctly), and also return some data from the JWT in a modified form, which we'll use to get the user email.

Now that we have added user email to the JWT and can get the token data as well, it's time to complete the GetEmail() function in our handlers.go file which we left incomplete earlier.

// get and parse the token data for email
func GetEmail() string {
    // load .env file
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    // The field in the JWT to which email is added
    key, ok := os.LookupEnv("NAMESPACE_DOMAIN")
    if !ok {
        log.Fatal("Error loading env variables (namespace domain)")
    }
    // extract token via Middleware function
    _, token := Middleware()
    // parse token for email and convert it to string
    email := token[key].(string)
    return email
}
Enter fullscreen mode Exit fullscreen mode

Finally, it's time to bring everything together in our main.go file!

main.go and CORS

Our main.go file looks like this:

package main

import (
    "log"
    "net/http"
    "os"

    "backend/packages" // importing files from the packages directory, to be explained
    "github.com/gorilla/mux" // An HTTP router
    _ "github.com/lib/pq" // Postgres driver for database/sql, _ indicates it won't be referenced directly in code
    "github.com/rs/cors" // For handling CORS (to be explained)
)

func main() {
    // the handler to verify access tokens
    jwtMiddleware, _ := backend.Middleware()

    r := mux.NewRouter()
    r.Handle("/list", jwtMiddleware.Handler(backend.GetList)).Methods("GET")
    r.Handle("/list/add", jwtMiddleware.Handler(backend.AddTask)).Methods("POST")
    r.Handle("/list/delete/{id}", jwtMiddleware.Handler(backend.DeleteTask)).Methods("DELETE")
    r.Handle("/list/edit/{id}", jwtMiddleware.Handler(backend.EditTask)).Methods("PUT")
    r.Handle("/list/done/{id}", jwtMiddleware.Handler(backend.DoneTask)).Methods("PUT")

    // for handling CORS
    c := cors.New(cors.Options{
        // Only add 1 value to allowed origins. Only the first one works. "*" is no exception.
        AllowedOrigins:   []string{"https://YOUR-FRONTEND-URL/"},
        AllowedMethods:   []string{"GET", "DELETE", "POST", "PUT", "OPTIONS"},
        AllowedHeaders:   []string{"Content-Type", "Origin", "Accept", "Authorization"},
        AllowCredentials: true,
    })

    // if deployed, looks for port in the environment and runs on it. Otherwise, runs locally on port 8000
    port, ok := os.LookupEnv("PORT")
    if !ok {
        port = "8000"
    }

    // apply the CORS specification on the request, and add relevant CORS headers as necessary
    handler := c.Handler(r)
    log.Println("Listening on port " + port + "...")
    // run on the designated port
    log.Fatal(http.ListenAndServe(":"+port, handler))
}
Enter fullscreen mode Exit fullscreen mode

Let's break down the r.Handle("/list", jwtMiddleware.Handler(backend.GetList)).Methods("GET") line.

  • r is the router instance. It registers a new route jwtMiddleware.Handler(backend.GetList) for the URL path /list, that is, when a request is made to this path, the route specified decides what will happen next.
  • jwtMiddleware.Handler() wraps around the actual route backend.GetList. This wrapping protects the API endpoint, preventing unauthorized access.
  • Methods("GET") specifies which HTTP method is used.

{id} in some routes is a variable in the path, decided by the task that sends the request. For example, if a task with the id 65 sends the DELETE request, the URL path would be /list/delete/65. This 65 is stored in the route variables (of type map[string]string) with the key id.

Cross-Origin Resource Sharing (CORS)
This concept is not something you'll encounter if you're only sending API requests via a tool like Postman, but when you have any origin other than the backend, you'll need to account for it.

CORS is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served. (Source: Wikipedia)

CORS relies on a mechanism by which browsers make a β€œpreflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request. (Source: MDN Web Docs)

Not all HTTP requests trigger a preflight request (see MDN documentation linked above), but in our case, they will. An HTTP request is first sent using the OPTIONS method to the resource on the other origin, to determine if the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data.

Thus, through the imported rs/cors package, we add the requisite headers, and subsequently wrap the router instance with the CORS handler. You can skip the AllowedOrigins for now.

log.Fatal(http.ListenAndServe(":"+port, handler)) runs our code on the particular port, and we can access our APIs locally! That is... after one more step.

Initializing module

A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module’s module path, which is also the import path used for the root directory, and its dependency requirements, which are the other modules needed for a successful build. Each dependency requirement is written as a module path and a specific semantic version. (Source: The Go blog)

Don't worry if you didn't understand everything above, you will soon!

In the terminal, in the root directory of your project (that is, the same level as the main.go file), run the following command:

go mod init backend

You should have a new go.mod file in your directory that looks like this:

module backend

go 1.16

require (
    github.com/auth0/go-jwt-middleware v1.0.0
    github.com/form3tech-oss/jwt-go v3.2.3+incompatible
    github.com/gorilla/mux v1.8.0
    github.com/joho/godotenv v1.3.0
    github.com/lib/pq v1.10.1
    github.com/rs/cors v1.7.0
)
Enter fullscreen mode Exit fullscreen mode
  • module backend indicates that "backend" is the module path, and also the import path for the root directory. This is why we imported our auth.go and handlers.go files as "backend/packages". (The "backend" in backend.GetList and other parts of the code is because the name of the package is also backend. You can give another name to the package or the module if it's confusing, just ensure to replace it in the relevant sections).
  • go 1.16 is the version of Go to be used.
  • require specifies all the dependency requirements of the directory.

Running the code

There are two ways we can go about this step.

  • Running go run main.go in the terminal. This build the source file into an executable binary in a temporary location, runs the compiled program, then removes the binary.
  • Running go build ./main.go followed by ./main. This compiles and builds the executable in the current directory, and doesn't require compilation each time the program is to be run. (Sources: DigitalOcean, Oldbloggers)

The first method is suitable for development when regular testing is required, while in production the latter method should be preferred.

Now you can finally test your program locally! You can use a tool like Postman to make API requests. Make sure to include the complete path (including http://localhost:8000/). To get the access token and make a request,

  • open the "test" section of your API on the Auth0 dashboard
  • copy the string value in the "access_token" field in the "response" section
  • in Postman, in the request headers section, type in "Authorization" as the key, and "Bearer YOUR_TOKEN" as the value.

So far, we've worked on our local system. Now, we will deploy our code so it is available to everyone!

Getting ready for deployment to Heroku

Firstly, the code for deployment differs slightly, so we'll make those changes. The major changes are in the OpenConnection() function in handlers.go. We won't need the "github.com/joho/godotenv" package in this file either.

// -------- This section is no longer needed ----------
// const (
//  host = "localhost"
//  port = 5432
// )
// ----------------------------------------------------

// Connect to PostgreSQL database that will be deployed on Heroku and also retrieve user_id from users table
func OpenConnection() (*sql.DB, string) {
    // Retrieve database URL (obtained after deployment) from the environment. It contains all the necessary information to connect to the database.
    dbURL, ok := os.LookupEnv("DATABASE_URL")
    if !ok {
        log.Fatal("Error loading env variables.")
    }

    // Connect to database
    db, err := sql.Open("postgres", dbURL)
    if err != nil {
        panic(err)
    }

    // Add email to users table if not present
    email := GetEmail()
    addEmail := `INSERT INTO users (email) VALUES ($1) ON CONFLICT (email) DO NOTHING;`
    _, err = db.Exec(addEmail, email)
    if err != nil {
        panic(err)
    }

    // Get user_id from users table
    var userId string
    getUser := `SELECT user_id FROM users WHERE email = $1;`
    err = db.QueryRow(getUser, email).Scan(&userId)
    if err != nil {
        panic(err)
    }

    return db, userId
}
Enter fullscreen mode Exit fullscreen mode

Thus, the only way we are altering our code is how we connect to our database, which will need to be deployed as well.

Next, create a new file at the root level of the directory named Procfile.

Heroku apps include a Procfile that specifies the commands that are executed by the app on startup. (Source: Heroku)

In it, enter the following code.

web: backend
Enter fullscreen mode Exit fullscreen mode

That's it, you've just configured the commands to be run on deployment. (If you changed the name of the module, use that instead of "backend").

Your file structure should finally look like what we discussed at the beginning.

backend
β”œβ”€β”€ .env
β”œβ”€β”€ .git
β”œβ”€β”€ .gitignore
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ main
β”œβ”€β”€ main.go
β”œβ”€β”€ packages
β”‚Β Β  β”œβ”€β”€ auth.go
β”‚Β Β  └── handlers.go
└── Procfile
Enter fullscreen mode Exit fullscreen mode

It is important to note that all your code to be deployed should be on the master/main branch of git. Also, you'll be configuring the environment manually, so the .env file won't be used, but you can leave it as it is.

Deploying to Heroku

Note: The following steps do not cover installing or signing into Heroku. Please visit this page if you require assistance. The following steps also assume your terminal is opened at the root level of your project directory.

  • Run heroku create app-name (or simply heroku create, autoassigning an app name).
  • Run git push heroku main (or master, depending on your default branch name).

This will deploy the code to the given URL, but it is not ready yet for use. We still have to deploy our database. (Official documentation here)

  • Run heroku addons:create heroku-postgresql:hobby-dev (hobby-dev is the free plan available on Heroku, but you can use another plan if you want).

Next, we're going to push our local database which we used in local development to this database. You can skip this section if you don't wish to do so.

  • Run heroku addons. This will give you a list of the addons associated with your application, including a line heroku-postgresql (DEPLOYED_DATABASE_NAME). Take note of this name.
  • Run heroku pg:push LOCAL_DATABASE_NAME DEPLOYED_DATABASE_NAME --app APP_NAME.

Finally, we need to add configuration variables for Heroku to use. This is done via the command heroku config:set KEY=VALUE.

  • Set NAMESPACE_DOMAIN to your namespace domain (set earlier in .env file as well).
  • We need the DATABASE_URL to connect to our deployed database. Run heroku config and check if it is already present. If not, run DATABASE_URL=$(heroku config:get DATABASE_URL -a APP_NAME).

That's all! Your program should now be up and running along with the database! I will soon share the steps to building a frontend for this application as well. If you have any problems or doubts, please feel free to reach out to me on Twitter or in the comments below.

Top comments (2)

Collapse
 
alexwood3738 profile image
alexwood3738

Building a web app backend in Go with user authentication and database integration involves a few structured steps. Start by setting up a Go project and choosing a framework like Gin or Echo for streamlined routing and request handling. Next, integrate a database such as PostgreSQL or MySQL, using GORM or a native SQL package to manage database interactions. Implement user authentication by creating secure signup and login endpoints, using JWT (JSON Web Tokens) or session cookies for token-based authentication to keep users logged in securely. Hash passwords with bcrypt to ensure user data safety. For community or customer service integration, tools like Saathi GTPL provide support for adding chat or help desk options, enhancing user experience and backend functionality.

Collapse
 
christhomas412 profile image
christhomas412

Thanks for sharing this Info with us i really like it.