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".
We will be using 2 tables, one for the tasks and one for the users.
Our tasks will have 4 properties:
- A unique id to distinguish them
- The task itself, that is, the text content
- A boolean value that reflects if the task is done or not
- A user id to distinguish the tasks of each user
We will assign 2 properties to users:
- A unique distinguishing id
- 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);
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();
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;
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
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
}
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
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)
})
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)
})
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.
- In your Auth0 dashboard, create a new application. The complete details will be added only after writing the frontend.
- 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.
- 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.
- 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
}
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
}
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))
}
Let's break down the r.Handle("/list", jwtMiddleware.Handler(backend.GetList)).Methods("GET")
line.
-
r
is the router instance. It registers a new routejwtMiddleware.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 routebackend.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
)
-
module backend
indicates that "backend" is the module path, and also the import path for the root directory. This is why we imported ourauth.go
andhandlers.go
files as "backend/packages". (The "backend" inbackend.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
}
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
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
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 simplyheroku create
, autoassigning an app name). - Run
git push heroku main
(ormaster
, 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 lineheroku-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. Runheroku config
and check if it is already present. If not, runDATABASE_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)
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.
Thanks for sharing this Info with us i really like it.