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
andgo.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!