How to make a GoLang Web app with authentication, an API and database interaction

GoLang is a popular programming language for building web applications. It has a simple syntax, a rich standard library, and excellent concurrency support. In this blog post, we will learn how to make a Go web app with authentication, an API and database interaction. We will use the following tools and libraries:

  • net/http: The standard package for handling HTTP requests and responses.
  • html/template: The standard package for rendering HTML templates with data.
  • crypto/bcrypt: A package for hashing and comparing passwords securely.
  • jwt-go: A package for creating and validating JSON Web Tokens (JWTs) for authentication.
  • pq: A pure Go driver for PostgreSQL, a relational database system.
  • sqlx: A package that extends the standard database/sql package with more features and convenience methods.

Setting up the project

First, we need to create a folder for our project and initialize a Go module. A Go module is a collection of Go packages that can be versioned and managed as a unit. To create a Go module, we run the following command in our project folder:

go mod init somethings.com/gowebapptut

This will create a file called go.mod that contains the module name and its dependencies. We can add or update dependencies by running go get or go mod tidy.

Next, we need to create a folder called templates where we will store our HTML templates. We will use the html/template package to parse and execute these templates with data from our handlers.

We also need to create a .env file where we will store our environment variables, such as the database connection string and the JWT secret key. We will use the os package to read these variables from our code. The .env file should look something like this:

DB_URL="host=localhost port=5432 user=postgres password=postgres dbname=gowebapptut sslmode=disable"
JWT_SECRET=secret
PORT=8080

We should never commit this file to version control, as it contains sensitive information.

The resulting file structure of our app will look like this:

myapp
├── go.mod
├── go.sum
├── main.go
├── models.go
├── handlers.go
├── templates
│   ├── index.html
│   ├── signup.html
│   ├── login.html
│   ├── home.html
└── .env
  • go.mod and go.sum are files that contain the module name and dependencies of our app.
  • main.go is the file that runs our app and sets up the database connection, the handler instance, and the HTTP server.
  • models.go is the file that defines our data models and database methods.
  • handlers.go is the file that defines our handler functions and logic.
  • templates is the folder that contains our HTML template files for rendering the web pages.
  • .env is the file that contains our environment variables, such as the database connection string and the JWT secret key.

Creating the database

We will use PostgreSQL as our database system. PostgreSQL is a powerful and open-source relational database that supports many features and data types. To install PostgreSQL, we can follow the instructions from its official website: https://www.postgresql.org/download/.

After installing PostgreSQL, we need to create a database for our app. We can use the psql command-line tool to interact with PostgreSQL. To create a database called myapp, we run the following command:

CREATE DATABASE myapp;

Then, we need to create a table called users where we will store the user information. We will use the following schema:

  • id: A serial primary key that uniquely identifies each user.
  • username: A text column that stores the user’s username.
  • password: A text column that stores the hashed password of the user.
  • created_at: A timestamp column that stores the date and time when the user was created.

To create the table, we run the following command:

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

We can verify that the table was created by running \dt to list all the tables in the database.

Creating the models

Now that we have our database ready, we need to create some Go structs that represent our data models. We will use the sqlx package to map these structs to the database rows and columns. The sqlx package provides many convenience methods and extensions over the standard database/sql package, such as named queries, struct scanning, and embedded structs.

To use the sqlx package, we need to install it by running:

go get github.com/jmoiron/sqlx

Then, we need to import it in our code:

import "github.com/jmoiron/sqlx"

We also need to install the pq driver, which is a pure Go implementation of the PostgreSQL protocol.

go get github.com/lib/pq

The pq driver registers itself with the database/sql package, so we don’t need to use it directly in our code. We just need to import it with an underscore:

import _ "github.com/lib/pq"

Next, we need to create a file called models.go where we will define our data models. We will start by creating a struct called User that represents a user in our app:

type User struct {
  ID        int       `db:"id"`
  Username  string    `db:"username"`
  Password  string    `db:"password"`
  CreatedAt time.Time `db:"created_at"`
}

The db tags specify the names of the corresponding database columns. The sqlx package will use these tags to map the struct fields to the database columns.

Next, we need to create a struct called DB that wraps a sqlx.DB instance and provides some methods to interact with the database. We will use this struct as a receiver for our model methods. The DB struct looks something like this:

type DB struct {
  *sqlx.DB
}

We embed a pointer to a sqlx.DB instance, so we can access all its methods directly from our DB struct.

Next, we need to create a function called NewDB that takes a database connection string and returns a new DB instance. The function looks something like this:

func NewDB(dbURL string) (*DB, error) {
  db, err := sqlx.Open("postgres", dbURL)
  if err != nil {
    return nil, err
  }
  return &DB{db}, nil
}

We use the sqlx.Open function to open a connection to the database using the postgres driver and the given connection string. Then, we return a new DB instance that wraps the sqlx.DB instance.

We also need to import the crypto/bcrypt package as well

go get golang.org/x/crypto/bcrypt

Next, we need to create some methods for our User model. We will start by creating a method called CreateUser that takes a username and a password and creates a new user in the database. The method looks something like this:

func (db *DB) CreateUser(username, password string) (*User, error) {
  // Hash the password using bcrypt
  hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
  if err != nil {
    return nil, err
  }

  // Create a new user in the database
  user := &User{
    Username: username,
    Password: string(hashedPassword),
  }
  query := "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id, created_at"
  err = db.QueryRowx(query, user.Username, user.Password).StructScan(user)
  if err != nil {
    return nil, err
  }
  return user, nil
}

We use the bcrypt.GenerateFromPassword function from the crypto/bcrypt package to hash the password using bcrypt, which is a secure and adaptive hashing algorithm. We use the default cost parameter, which determines how slow the hashing function is. A higher cost makes it harder for attackers to crack the password.

Then, we create a new user in the database using an INSERT query with a RETURNING clause. The RETURNING clause allows us to get back the generated id and created_at columns from the inserted row. We use the QueryRowx method from the sqlx package to execute the query and get back a single row. We use the StructScan method to scan the row into our user struct.

If there is no error, we return the user struct. Otherwise, we return an error.

Next, we need to create a method called GetUserByUsername that takes a username and returns the user with that username from the database. The method looks something like this:

func (db *DB) GetUserByUsername(username string) (*User, error) {
  // Get the user by username from the database
  user := &User{}
  query := "SELECT * FROM users WHERE username = $1"
  err := db.Get(user, query, username)
  if err != nil {
    return nil, err
  }
  return user, nil
}

We use the Get method from the sqlx package to execute a SELECT query and get back a single row. We pass our user struct as the first argument, which will be filled with the data from the row. We pass our query as the second argument, which uses a placeholder for the username parameter. We pass our username as the third argument, which will replace the placeholder in the query.

If there is no error, we return the user struct. Otherwise, we return an error.

Creating the handlers

Now that we have our models ready, we need to create some handlers that handle HTTP requests and responses.

Create a new file called handlers.go, and install the github.com/dgrijalva/jwt-go package.

go get github.com/dgrijalva/jwt-go

We also need to import the following packages that we will use in our handlers:

import (
	"database/sql"
	"html/template"
	"net/http"
	"os"
	"time"

	"github.com/dgrijalva/jwt-go"
	"golang.org/x/crypto/bcrypt"
)
  • net/http: A package for creating https servers
  • encoding/json: A package for encoding and decoding JSON
  • html/template: A package for rendering HTML templates with data
  • os: A package for interacting with the operating system, such as reading environment variables
  • github.com/dgrijalva/jwt-go: A package for creating and validating JSON Web Tokens (JWTs) for authentication

Next we will start by creating a struct called Handler that wraps a DB instance and provides some methods to handle HTTP requests and responses. We will use this struct as a receiver for our handler functions. The Handler struct looks something like this:

type Handler struct {
  db *DB
}

We store a pointer to a DB instance, so we can access the database methods from our handler methods.

Next, we need to create a function called NewHandler that takes a DB instance and returns a new Handler instance. The function looks something like this:

func NewHandler(db *DB) *Handler {
  return &Handler{db}
}

We simply return a new Handler instance that wraps the given DB instance.

Next, we need to create some handler functions for our app.

We will start by creating a function called Index that handles GET requests to the / path. This function will render the index page of our app, which will show a welcome message and links to sign up or log in. The function looks something like this:

func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
  // Parse and execute the index template
  tmpl := template.Must(template.ParseFiles("templates/index.html"))
  err := tmpl.Execute(w, nil)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
}

We use the template.Must function from the html/template package to parse the index template file from the templates folder. We use the Execute method to execute the template with no data and write the output to the response writer. If there is an error, we use the http.Error function to write an error message and status code to the response.

Create a index template file that looks something like this in templates/index.html.

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <h1>Welcome to My App</h1>
  <p>This is a Go web app with authentication, an API and database interaction.</p>
  <p><a href="/signup">Sign up</a> or <a href="/login">Log in</a> to get started.</p>
</body>
</html>

It is a simple HTML file that shows a welcome message and links to sign up or log in.

Next, we need to create a function called Signup that handles GET and POST requests to the /signup path. This function will render the signup page of our app, which will show a form for the user to enter their username and password. If the user submits the form, this function will validate the input, create a new user in the database, and redirect them to the login page. The function looks something like this:

func (h *Handler) Signup(w http.ResponseWriter, r *http.Request) {
	// Check if the request method is GET or POST
	switch r.Method {
	case http.MethodGet:
		// Parse and execute the signup template
		tmpl := template.Must(template.ParseFiles("templates/signup.html"))
		err := tmpl.Execute(w, nil)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	case http.MethodPost:
		// Parse and validate the form input
		err := r.ParseForm()
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		username := r.FormValue("username")
		password := r.FormValue("password")
		if username == "" || password == "" {
			http.Error(w, "Username and password are required", http.StatusBadRequest)
			return
		}

		// Create a new user in the database
		_, err = h.db.CreateUser(username, password)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Redirect to the login page with a success message
		http.Redirect(w, r, "/login?success=You have signed up successfully", http.StatusSeeOther)
	default:
		// Return a method not allowed error
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
}

We use a switch statement to check if the request method is GET or POST. If it is GET, we parse and execute the signup template. Create a file in the templates folder called signup.html, which looks something like this:

<!DOCTYPE html>
<html>
<head>
  <title>My App - Sign up</title>
</head>
<body>
  <h1>Sign up</h1>
  <form method="POST" action="/signup">
    <p><label for="username">Username:</label>
    <input type="text" id="username" name="username"></p>
    <p><label for="password">Password:</label>
    <input type="password" id="password" name="password"></p>
    <p><input type="submit" value="Sign up"></p>
  </form>
  <p>Already have an account? <a href="/login">Log in</a></p>
</body>
</html>

It is a simple HTML file that shows a form for the user to enter their username and password.

If the request method is POST, we parse and validate the form input using the ParseForm and FormValue methods from the http.Request struct. We check if the username and password are not empty, and return a bad request error if they are.

Then, we create a new user in the database using the CreateUser method from our DB struct. We pass the username and password as arguments, and get back a user struct or an error. If there is an error, we return an internal server error.

If there is no error, we redirect the user to the login page with a success message using the Redirect function from the net/http package. We use the StatusSeeOther status code, which indicates that the resource has been temporarily moved to another URI.

If the request method is neither GET nor POST, we return a method not allowed error using the StatusMethodNotAllowed status code.

Next, we need to create a function called Login that handles GET and POST requests to the /login path. This function will render the login page of our app, which will show a form for the user to enter their username and password. If the user submits the form, this function will validate the input, check if the user exists and the password matches, create a JWT token for authentication, and redirect them to the home page. The function looks something like this:

func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
  // Check if the request method is GET or POST
  switch r.Method {
  case http.MethodGet:
    // Parse and execute the login template
    tmpl := template.Must(template.ParseFiles("templates/login.html"))
    data := map[string]interface{}{
      "Success": r.URL.Query().Get("success"),
    }
    err := tmpl.Execute(w, data)
    if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
    }
  case http.MethodPost:
    // Parse and validate the form input
    err := r.ParseForm()
    if err != nil {
      http.Error(w, err.Error(), http.StatusBadRequest)
      return
    }
    username := r.FormValue("username")
    password := r.FormValue("password")
    if username == "" || password == "" {
      http.Error(w, "Username and password are required", http.StatusBadRequest)
      return
    }

    // Get the user by username from the database
    user, err := h.db.GetUserByUsername(username)
    if err != nil {
      if err == sql.ErrNoRows {
        // Return a not found error if the user does not exist
        http.Error(w, "User not found", http.StatusNotFound)
        return
      }
      // Return an internal server error otherwise
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
    }

    // Compare the hashed password with the input password
    err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
    if err != nil {
      // Return an unauthorized error if the password does not match
      http.Error(w, "Invalid password", http.StatusUnauthorized)
      return
    }

    // Create a JWT token for authentication
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "id":       user.ID,
      "username": user.Username,
      "exp":      time.Now().Add(24 * time.Hour).Unix(),
    })
    secret := os.Getenv("JWT_SECRET")
    tokenString, err := token.SignedString([]byte(secret))
    if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
    }

    // Set a cookie with the token in the response
    cookie := &http.Cookie{
      Name:     "token",
      Value:    tokenString,
      Expires:  time.Now().Add(24 * time.Hour),
      HttpOnly: true,
      Path:     "/",
    }
http.SetCookie(w, cookie)

    // Redirect to the home page with a success message
    http.Redirect(w, r, "/home?success=You have logged in successfully", http.StatusSeeOther)
  default:
    // Return a method not allowed error
    http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    return
  }
}

We use a switch statement to check if the request method is GET or POST. If it is GET, we parse and execute the login template. Create a file in the templates folder called login.html, which looks something like this:

<!DOCTYPE html>
<html>
<head>
  <title>My App - Log in</title>
</head>
<body>
  <h1>Log in</h1>
  {{if .Success}}
  <p>{{.Success}}</p>
  {{end}}
  <form method="POST" action="/login">
    <p><label for="username">Username:</label>
    <input type="text" id="username" name="username"></p>
    <p><label for="password">Password:</label>
    <input type="password" id="password" name="password"></p>
    <p><input type="submit" value="Log in"></p>
  </form>
  <p>Don't have an account? <a href="/signup">Sign up</a></p>
</body>
</html>

It is a simple HTML file that shows a form for the user to enter their username and password. It also shows a success message if there is one in the query string.

If the request method is POST, we parse and validate the form input using the ParseForm and FormValue methods from the http.Request struct. We check if the username and password are not empty, and return a bad request error if they are.

Then, we get the user by username from the database using the GetUserByUsername method from our DB struct. We pass the username as an argument, and get back a user struct or an error. If there is an error, we check if it is a sql.ErrNoRows error, which means that the user does not exist. In that case, we return a not found error using the StatusNotFound status code. Otherwise, we return an internal server error.

Then, we compare the hashed password from the database with the input password using the bcrypt.CompareHashAndPassword function from the crypto/bcrypt package. We pass the hashed password and the input password as byte slices, and get back an error if they do not match. In that case, we return an unauthorized error using the StatusUnauthorized status code.

Then, we create a JWT token for authentication using the jwt.NewWithClaims function from the github.com/dgrijalva/jwt-go package. We pass the signing method as jwt.SigningMethodHS256, which uses HMAC-SHA256 to sign the token. We also pass a map of claims as the second argument, which contains some information about the user and an expiration time. We use the os.Getenv function to get the JWT secret key from the environment variable. We use the SignedString method to sign the token with the secret key and get back a token string or an error. If there is an error, we return an internal server error.

Then, we set a cookie with the token in the response using the http.SetCookie function from the net/http package. We create a new cookie struct with some properties, such as name, value, expiration time, http-only flag, and path. The http-only flag prevents JavaScript from accessing the cookie, which adds some security against cross-site scripting (XSS) attacks.

Finally, we redirect the user to the home page with a success message using the Redirect function from the net/http package. We use the StatusSeeOther status code, which indicates that the resource has been temporarily moved to another URI.

If the request method is neither GET nor POST, we return a method not allowed error using the StatusMethodNotAllowed status code.

Next, we need to create a function called Home that handles GET requests to the /home path. This function will render the home page of our app, which will show a welcome message and the user’s username. This function will also check if the user is authenticated by verifying the JWT token in the cookie. The function looks something like this:

func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
  // Check if the request method is GET
  if r.Method != http.MethodGet {
    // Return a method not allowed error
    http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    return
  }

  // Get the cookie with the token from the request
  cookie, err := r.Cookie("token")
  if err != nil {
    if err == http.ErrNoCookie {
      // Return an unauthorized error if the cookie does not exist
      http.Error(w, "Unauthorized", http.StatusUnauthorized)
      return
    }
    // Return an internal server error otherwise
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }

  // Parse and validate the token from the cookie
  tokenString := cookie.Value
  secret := os.Getenv("JWT_SECRET")
  token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    // Check if the signing method is valid
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
      return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
    }
    // Return the secret key as the verification key
    return []byte(secret), nil
  })
  if err != nil {
    // Return an unauthorized error if the token is invalid
    http.Error(w, "Unauthorized", http.StatusUnauthorized)
    return
  }

  // Get the claims from the token
  claims, ok := token.Claims.(jwt.MapClaims)
  if !ok || !token.Valid {
    // Return an unauthorized error if the claims are invalid
    http.Error(w, "Unauthorized", http.StatusUnauthorized)
    return
  }

  // Get the username from the claims
  username, ok := claims["username"].(string)
  if !ok {
    // Return an internal server error if the username is missing or not a string
    http.Error(w, "Internal server error", http.StatusInternalServerError)
    return
  }

  // Parse and execute the home template
  tmpl := template.Must(template.ParseFiles("templates/home.html"))
  data := map[string]interface{}{
    "Success": r.URL.Query().Get("success"),
    "Username": username,
  }
  err = tmpl.Execute(w, data)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
}

We check if the request method is GET using the Method field from the http.Request struct. If it is not GET, we return a method not allowed error using the StatusMethodNotAllowed status code.

Then, we get the cookie with the token from the request using the Cookie method from the http.Request struct. We pass the name of the cookie as “token” and get back a cookie struct or an error. If there is an error, we check if it is a http.ErrNoCookie error, which means that the cookie does not exist. In that case, we return an unauthorized error using the StatusUnauthorized status code. Otherwise, we return an internal server error.

Then, we parse and validate the token from the cookie using the Parse function from the github.com/dgrijalva/jwt-go package. We pass the token string as the first argument and a function as the second argument. The function takes a token struct as an argument and returns an interface or an error. The function checks if the signing method of the token is valid by asserting it to a *jwt.SigningMethodHMAC type. If it is not valid, it returns an error with a message. Otherwise, it returns the secret key as a byte slice as the verification key.

If there is an error from parsing or validating the token, we return an unauthorized error using the StatusUnauthorized status code.

Then, we get the claims from the token by asserting it to a jwt.MapClaims type. This is a map of interface values that represents the payload of the token. We also check if this assertion is successful and if the token is valid using the Valid field from the jwt.Token struct. If not, we return an unauthorized error using the StatusUnauthorized status code.

Then, we get the username from the claims by indexing it with “username” and asserting it to a string type. We also check if this assertion is successful using an ok variable. If not, we return an internal server error using the StatusInternalServerError status code.

Then, we parse and execute the home template. Create a file in the templates folder called home.html, which looks something like this:

<!DOCTYPE html>
<html>
<head>
  <title>My App - Home</title>
</head>
<body>
  <h1>Home</h1>
  {{if .Success}}
  <p>{{.Success}}</p>
  {{end}}
  <p>Welcome, {{.Username}}!</p>
  <p><a href="/logout">Log out</a></p>
</body>
</html>

It is a simple HTML file that shows a welcome message and the user’s username. It also shows a success message if there is one in the query string. It also shows a link to log out.

Next, we need to create a function called Logout that handles GET requests to the /logout path. This function will clear the cookie with the token in the response and redirect the user to the index page. The function looks something like this:

func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
  // Check if the request method is GET
  if r.Method != http.MethodGet {
    // Return a method not allowed error
    http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    return
  }

  // Clear the cookie with the token in the response
  cookie := &http.Cookie{
    Name:     "token",
    Value:    "",
    Expires:  time.Unix(0, 0),
    HttpOnly: true,
    Path:     "/",
  }
  http.SetCookie(w, cookie)

  // Redirect to the index page with a success message
  http.Redirect(w, r, "/?success=You have logged out successfully", http.StatusSeeOther)
}

We check if the request method is GET using the Method field from the http.Request struct. If it is not GET, we return a method not allowed error using the StatusMethodNotAllowed status code.

Then, we clear the cookie with the token in the response using the http.SetCookie function from the net/http package. We create a new cookie struct with some properties, such as name, value, expiration time, http-only flag, and path. We set the value to an empty string and the expiration time to a past date, which will effectively delete the cookie from the browser.

Then, we redirect the user to the index page with a success message using the Redirect function from the net/http package. We use the StatusSeeOther status code, which indicates that the resource has been temporarily moved to another URI.

Next, we need to create a function called API that handles GET requests to the /api path. This function will return a JSON response with some data from the database. This function will also check if the user is authenticated by verifying the JWT token in the cookie. The function looks something like this:

func (h *Handler) API(w http.ResponseWriter, r *http.Request) {
	// Check if the request method is GET
	if r.Method != http.MethodGet {
		// Return a method not allowed error
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Get the cookie with the token from the request
	cookie, err := r.Cookie("token")
	if err != nil {
		if err == http.ErrNoCookie {
			// Return an unauthorized error if the cookie does not exist
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		// Return an internal server error otherwise
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Parse and validate the token from the cookie
	tokenString := cookie.Value
	secret := os.Getenv("JWT_SECRET")
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Check if the signing method is valid
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		// Return the secret key as the verification key
		return []byte(secret), nil
	})
	if err != nil {
		// Return an unauthorized error if the token is invalid
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get the claims from the token
	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok || !token.Valid {
		// Return an unauthorized error if the claims are invalid
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get the username from the claims
	username, ok := claims["username"].(string)
	if !ok {
		// Return an internal server error if the username is missing
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Get some data from the database using the user id
	user, err := h.db.GetUserByUsername(username)
	if err != nil {
		// Return an internal server error if there is an error getting the data
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	user.Password = ""
	// Encode and write the data as JSON to the response
	w.Header().Set("Content-Type", "application/json")
	err = json.NewEncoder(w).Encode(user)
	if err != nil {
		// Return an internal server error if there is an error encoding the data
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

We check if the request method is GET using the Method field from the http.Request struct. If it is not GET, we return a method not allowed error using the StatusMethodNotAllowed status code.

Then, we get the cookie with the token from the request using the Cookie method from the http.Request struct. We pass the name of the cookie as “token” and get back a cookie struct or an error. If there is an error, we check if it is a http.ErrNoCookie error, which means that the cookie does not exist. In that case, we return an unauthorized error using the StatusUnauthorized status code. Otherwise, we return an internal server error.

Then, we parse and validate the token from the cookie using the Parse function from the github.com/dgrijalva/jwt-go package. We pass the token string as the first argument and a function as the second argument. The function takes a token struct as an argument and returns an interface or an error. The function checks if the signing method of the token is valid by asserting it to a *jwt.SigningMethodHMAC type. If it is not valid, it returns an error with a message. Otherwise, it returns the secret key as a byte slice as the verification key.

If there is an error from parsing or validating the token, we return an unauthorized error using the StatusUnauthorized status code.

Then, we get the claims from the token by asserting it to a jwt.MapClaims type. This is a map of interface values that represents the payload of the token. We also check if this assertion is successful and if the token is valid using the Valid field from the jwt.Token struct. If not, we return an unauthorized error using the StatusUnauthorized status code.

Then, we get the username from the claims by indexing it with “username” and asserting it to a string type. We also check if this assertion is successful using an ok variable. If not, we return an internal server error using the StatusInternalServerError status code.

Then, we get the user from the database using the username using the GetUserByUsername method from our DB struct. We pass the username as an string as an argument, and get back a user struct or an error. If there is an error, we return an internal server error.

Then, we encode and write the data as JSON to the response using the json.NewEncoder function from the encoding/json package. We set the content type header to “application/json” using the Set method from the http.Header struct. We use the Encode method to encode the data and write it to the response writer. If there is an error, we return an internal server error.

Running the app

Now that we have our models and handlers ready, we need to create a file called main.go where we will run our app.

Install the following package so that we can read the env file we made at the begining of the tutoeiral.

go get github.com/joho/godotenv

We will start by importing the packages that we need:

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

	"github.com/joho/godotenv"
	_ "github.com/lib/pq"
)

Then, we need to create a function called main that will be the entry point of our app. The function looks something like this:

func main() {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatalf("Some error occured. Err: %s", err)
	}
	// Get the database connection string from the environment variable
	dbURL := os.Getenv("DB_URL")
	if dbURL == "" {
		log.Fatal("DB_URL is required")
	}

	// Create a new DB instance
	db, err := NewDB(dbURL)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Create a new Handler instance
	handler := NewHandler(db)

	// Create a new ServeMux instance
	mux := http.NewServeMux()

	// Register the handler functions with the ServeMux
	mux.HandleFunc("/", handler.Index)
	mux.HandleFunc("/signup", handler.Signup)
	mux.HandleFunc("/login", handler.Login)
	mux.HandleFunc("/logout", handler.Logout)
	mux.HandleFunc("/home", handler.Home)
	mux.HandleFunc("/api", handler.API)

	// Start the HTTP server
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	log.Printf("Listening on port %s\n", port)
	log.Fatal(http.ListenAndServe(":"+port, mux))
}

We use the os.Getenv function to get the database connection string from the environment variable. We check if it is not empty, and log a fatal error if it is.

Then, we create a new DB instance using the NewDB function that we defined earlier. We pass the database connection string as an argument, and get back a DB instance or an error. We log a fatal error if there is an error. We also defer the Close method of the DB instance, which will close the database connection when the function returns.

Then, we create a new Handler instance using the NewHandler function that we defined earlier. We pass the DB instance as an argument, and get back a Handler instance.

Then, we create a new ServeMux instance using the http.NewServeMux function from the net/http package. A ServeMux is an HTTP request multiplexer that matches incoming requests to registered handlers based on the request path.

Then, we register our handler functions with the ServeMux using the HandleFunc method. We pass the request path as the first argument and the handler function as the second argument. For example, we register the Index handler function for the / path.

Then, we start the HTTP server using the http.ListenAndServe function from the net/http package. We pass the port number as the first argument and the ServeMux as the second argument. The port number is obtained from another environment variable, or defaults to “8080” if not set. We log a message indicating that the server is listening on that port, and log a fatal error if there is an error starting the server.

To run our app, run the go run command:

go run .

This will start our app and listen for HTTP requests on port 8080. We can then open our browser and visit http://localhost:8080 to see our app in action.

Conclusion

In this blog post, we learned how to make a Go web app with authentication, an API and database interaction. We used some standard packages such as net/http, html/template, and crypto/bcrypt, as well as some third-party packages such as sqlx, pq, and jwt-go. We also used PostgreSQL as our database system and JWT as our authentication mechanism.

We covered some basic concepts such as models, handlers, templates, cookies, and tokens. We also implemented some features such as signup, login, logout, home page, and API endpoint.

You can find all the code here: https://github.com/RedGhoul/gowebapptut

We hope you enjoyed this tutorial and learned something new. Happy coding!