DEV Community

Cover image for ๐Ÿš€ Go-ing Full-Stack: Building Dynamic Web Apps with Go ๐Ÿน, PostgreSQL ๐Ÿ˜, Docker ๐Ÿณ, and HTTP Servers ๐ŸŒ
Allan Githaiga
Allan Githaiga

Posted on

๐Ÿš€ Go-ing Full-Stack: Building Dynamic Web Apps with Go ๐Ÿน, PostgreSQL ๐Ÿ˜, Docker ๐Ÿณ, and HTTP Servers ๐ŸŒ

You know, sometimes Go is all you need to... well, go far.

In this tutorial, weโ€™re going full-stack using Go as our backend, PostgreSQL as our database, and a simple HTML + Docker. Why? Because weโ€™re brave, weโ€™re learning, and letโ€™s face it โ€“ we love a good challenge.

Prerequisites

Make sure you have:

  1. Go installed (version 1.15 or higher)
  2. PostgreSQL running on your machine or Docker
  3. Docker installed
  4. A sense of humor (or at least an appreciation for developer jokes)

Step 1: Setting Up the Project and Connecting to PostgreSQL

Letโ€™s start by setting up a new Go project.

mkdir go-fullstack-app
cd go-fullstack-app
go mod init go-fullstack-app

Enter fullscreen mode Exit fullscreen mode

Now, we need to install the PostgreSQL driver for Go:

go get github.com/lib/pq

Enter fullscreen mode Exit fullscreen mode

Step 2:Setting Up PostgreSQL with Docker

Instead of installing PostgreSQL directly on your machine, weโ€™re going to use Docker to run PostgreSQL in a container. This makes it easier to manage and keep things isolated.

1. Pull the PostgreSQL Docker image:
In the terminal, run the following command to pull the official PostgreSQL image from Docker Hub:

 docker pull postgres

Enter fullscreen mode Exit fullscreen mode

2. Run the PostgreSQL container:
Now, weโ€™ll run the container with a custom name and credentials. You can adjust the POSTGRES_PASSWORD, POSTGRES_USER, and POSTGRES_DB values as needed.

docker run --name my_postgres_container -e POSTGRES_PASSWORD=mysecretpassword -e POSTGRES_USER=myuser -e POSTGRES_DB=mydatabase -p 5432:5432 -d postgres

Enter fullscreen mode Exit fullscreen mode

3. Access the PostgreSQL Database:
After running the above command, you can access the PostgreSQL container via the following command:

docker exec -it my_postgres_container psql -U myuser -d mydatabase

Enter fullscreen mode Exit fullscreen mode

This opens up the PostgreSQL shell, where you can start interacting with the database.

Step 3: Creating a Table in PostgreSQL

Now that we have PostgreSQL running inside a Docker container, weโ€™ll create a table to store user data. For this, we need to write SQL queries.

1. Create the users table:
In the PostgreSQL shell, run the following query to create a table:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100) UNIQUE NOT NULL
);

Enter fullscreen mode Exit fullscreen mode


bash

  • id SERIAL PRIMARY KEY: Automatically generates unique IDs for each user.
  • name VARCHAR(100): Stores the user's name
  • email VARCHAR(100) UNIQUE NOT NULL: Stores the email address, ensuring it's unique and cannot be empty.

2. Insert data into the users table: You can insert a few sample records:

INSERT INTO users (name, email) VALUES ('allan', 'allan@gmail.com');
INSERT INTO users (name, email) VALUES ('robinson', 'robinson@gmail.com');

Enter fullscreen mode Exit fullscreen mode

3. Verify the data:
To see the data youโ€™ve just inserted, run:

SELECT * FROM users;

Enter fullscreen mode Exit fullscreen mode

You should see the users list:

id | name    | email
----+---------+-------------------
 1  | allan   | allan@gmail.com
 2  | robinson| robinson@gmail.com
Enter fullscreen mode Exit fullscreen mode

Step 4: Writing Go Code to Connect to PostgreSQL

Next, weโ€™ll write the Go code to interact with the PostgreSQL database.

  • Set Up the Go Code to Connect to the Database: Here's a simple Go program that connects to PostgreSQL and fetches the list of users.

1. Import Statements:

import (
    "database/sql" //Interacts with the SQL database.
    "fmt"
    "log"
    "net/http"
    "os"
    "github.com/joho/godotenv"  //Loads environment variables from a .env file.
    _ "github.com/lib/pq"  //A PostgreSQL driver for Go, enabling communication with PostgreSQL databases.
    "encoding/json"  //Provides functionality to encode and decode JSON data.
)

Enter fullscreen mode Exit fullscreen mode

2.User Struct:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

Enter fullscreen mode Exit fullscreen mode

Defines a simple User struct, which will represent a user in the application.

3.getUsers Function:

func getUsers(w http.ResponseWriter, r *http.Request) {
    rows, err := DB.Query("SELECT id, name, email FROM users")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
            log.Println("Error scanning user", err)
            continue
        }
        users = append(users, user)
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

Enter fullscreen mode Exit fullscreen mode

- Purpose: Fetches a list of users from the PostgreSQL database and returns it as a JSON response.
- Key Operations:

  • Executes a SQL query to fetch id, name, and email from the users table.
  • Loops through the result set (rows.Next()), scanning each row into the User struct.
    • Appends each user to the users slice.
    • Responds with the list of users encoded in JSON.

4.Global DB Variable & initDB Function:

var DB *sql.DB

func initDB() {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf("Error loading .env file: %v", err)
    }

    connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
        os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME"))

    var errConn error
    DB, errConn = sql.Open("postgres", connStr)
    if errConn != nil {
        log.Fatalf("Error opening database: %v", errConn)
    }

    if err = DB.Ping(); err != nil {
        log.Fatalf("Cannot connect to the database: %v", err)
    }

    fmt.Println("Database connected successfully!")
}


Enter fullscreen mode Exit fullscreen mode
  • A global variable DB that holds the connection pool for PostgreSQL. This will be used throughout the app to query the database.

InitDB Purpose: Initializes the connection to the PostgreSQL database.

Key Operations:

  • Loads environment variables from the .env file using godotenv.
  • Constructs a PostgreSQL connection string using the loaded environment variables.
  • Opens a connection to the PostgreSQL database using sql.Open.
  • Pings the database to ensure the connection is successful.
  • Logs an error and exits if the connection fails.

5.loadhomepage Function:

func loadhomepage(w http.ResponseWriter, r *http.Request) {
    // Query the database to get users
    rows, err := DB.Query("SELECT id, name, email FROM users")
    if err != nil {
        http.Error(w, "Error fetching users: "+err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
            http.Error(w, "Error scanning user: "+err.Error(), http.StatusInternalServerError)
            return
        }
        users = append(users, user)
    }

    // HTML template to render the users
    html := "<html><head><title>Users List</title></head><body>"
    html += "<h1>Users List</h1>"
    html += "<table border='1'><tr><th>ID</th><th>Name</th><th>Email</th></tr>"

    // Loop through users and display them in a table
    for _, user := range users {
        html += fmt.Sprintf("<tr><td>%d</td><td>%s</td><td>%s</td></tr>", user.ID, user.Name, user.Email)
    }

    html += "</table></body></html>"

    w.Header().Set("Content-Type", "text/html")
    w.Write([]byte(html))
}

Enter fullscreen mode Exit fullscreen mode
  • Purpose: Renders the list of users in HTML format.
    Key Operations:

    • Executes a SQL query to fetch user data from the users table.
    • Creates an HTML table to display the users.
    • Loops through the users slice and formats each user as a table row in HTML.
    • Sends the HTML response back to the client.

6.main Function:

func main() {
    initDB()
    // Set up routes and start your server here...
    http.HandleFunc("/users", getUsers)
    http.HandleFunc("/", loadhomepage)

    //start server
    fmt.Println("starting server on port 8080...")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("Server failed to start: %v", err)
    }
}

Enter fullscreen mode Exit fullscreen mode

Purpose: The entry point of the application, responsible for starting the server and defining the routes.
Key Operations:

  • Calls initDB to initialize the database connection.
    • Sets up two routes:
      • /users: This will invoke the getUsers function to return users in JSON format.
      • /: This serves the homepage, calling loadhomepage to display users in an HTML table.
    • Starts the HTTP server on port 8080 and listens for incoming requests.

Step 5: Running and Testing the App

Run the Go server:

go run main.go

Enter fullscreen mode Exit fullscreen mode

Now, open in your browser, and voila! You should see a list of users fetched from your database. If notโ€ฆ well, debugging is half the fun (and sometimes 90% of the time).

screenshot

Wrapping Up

In this project, you:

  1. Set up a Go backend with PostgreSQL.
  2. Created API routes to manage users.
  3. Built a frontend to display user data.
    1. Setting Up PostgreSQL with Docker

And thatโ€™s a wrap! Building a full-stack app in Go is surprisingly straightforward, and you now have a foundation to grow into more complex projects. Happy coding, and remember: If it works, donโ€™t touch it. Unless itโ€™s Go โ€“ then Go for it!

Top comments (0)