Skip to main content

Module 03 — Go Backend

Objective

Build and deploy a Go REST API on app-server that connects to the PostgreSQL database on db-server and serves customer data.

By the end of this module you will have:

ComponentDetail
Go1.22 installed on app-server
APIREST endpoints for customers CRUD on port 8080
DatabaseApp connects to customerdb on db-server
Servicecustomerapp running via systemd

Prerequisites

  • Module 02 complete — PostgreSQL running on db-server with customerdb, appuser, and tables created
  • SSH access to app-server: ssh app-server

1. Install Go on app-server

SSH into app-server:

ssh app-server

1.1 Download and install Go 1.22

wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
rm go1.22.0.linux-amd64.tar.gz

This downloads the official Go binary release and extracts it to /usr/local/go.

1.2 Add Go to your PATH

echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

1.3 Verify the installation

go version

You should see:

go version go1.22.0 linux/amd64

2. Create the project

On app-server, create a directory for the application:

mkdir -p ~/customerapp

The project needs two files: go.mod (declares the module name and dependencies) and main.go (the application code). We will transfer them from your Mac in the next step, but first let's understand what the code does.


3. Walk through main.go

The entire backend is a single Go file. This section walks through each part so you understand how it works before deploying it.

3.1 Package, imports, and structs

package main

import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"

_ "github.com/lib/pq"
)

var db *sql.DB

type Customer struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
Address string `json:"address"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}

type LoginResponse struct {
Message string `json:"message"`
Username string `json:"username"`
}

The database/sql package provides a generic SQL interface, while github.com/lib/pq is the PostgreSQL driver (imported with _ because we only need its side-effect of registering itself). The Customer struct maps directly to columns in the customers table, and the JSON tags control how fields appear in API responses.

3.2 Database connection

connStr := "host=192.168.56.11 port=5432 user=appuser password=apppassword123 dbname=customerdb sslmode=disable"

var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("Failed to open database: %v", err)
}
defer db.Close()

err = db.Ping()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
log.Println("Connected to database")

sql.Open creates a connection pool but does not actually connect — db.Ping() verifies the connection is reachable. The connection string points to db-server's IP and uses the appuser credentials we created in Module 02.

3.3 Main function and routes

http.HandleFunc("/health", healthHandler)
http.HandleFunc("/api/login", loginHandler)
http.HandleFunc("/api/customers", customersHandler)
http.HandleFunc("/api/customers/", customerByIDHandler)
http.Handle("/", http.FileServer(http.Dir("./static")))

log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))

Each HandleFunc maps a URL pattern to a handler function. The /api/customers/ pattern (with trailing slash) matches any path that starts with it, which is how we handle requests like /api/customers/1. The file server on / serves the frontend (we will add it in Module 04).

3.4 Health check handler

func healthHandler(w http.ResponseWriter, r *http.Request) {
err := db.Ping()
// returns {"status":"healthy","database":"connected"} or unhealthy
}

A simple endpoint that pings the database and returns a JSON status. This is useful for verifying the API and database connection are working.

3.5 Login handler

func loginHandler(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
json.NewDecoder(r.Body).Decode(&req)

query := fmt.Sprintf("SELECT id, username FROM users WHERE username = '%s' AND password = '%s'",
req.Username, req.Password)
row := db.QueryRow(query)
// ...
http.SetCookie(w, &http.Cookie{Name: "session", Value: username, Path: "/"})
}

The login handler parses a JSON body, queries the users table, and sets a session cookie on success. Protected endpoints check for this cookie before allowing access.

3.6 List customers handler

func listCustomersHandler(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, name, email, phone, address, created_at, updated_at FROM customers ORDER BY id")
// iterate rows, scan into Customer structs, return JSON array
}

Queries all customers ordered by ID and returns them as a JSON array. The rows.Next() loop scans each database row into a Customer struct.

3.7 Get customer handler

func getCustomerHandler(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/customers/")
query := fmt.Sprintf("SELECT id, name, email, phone, address, created_at, updated_at FROM customers WHERE id = %s", id)
row := db.QueryRow(query)
// scan and return single customer
}

Extracts the customer ID from the URL path and queries for that specific customer. Returns a 404 if not found.

3.8 Create customer handler

func createCustomerHandler(w http.ResponseWriter, r *http.Request) {
var c Customer
json.NewDecoder(r.Body).Decode(&c)
query := fmt.Sprintf("INSERT INTO customers (name, email, phone, address) VALUES ('%s', '%s', '%s', '%s') RETURNING ...",
c.Name, c.Email, c.Phone, c.Address)
// execute and return created customer with 201 status
}

Parses a JSON body into a Customer struct and inserts a new row. The RETURNING clause gives us the complete record (including the auto-generated ID and timestamps) without a second query.

3.9 Update customer handler

func updateCustomerHandler(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/customers/")
var c Customer
json.NewDecoder(r.Body).Decode(&c)
query := fmt.Sprintf("UPDATE customers SET name = '%s', email = '%s', phone = '%s', address = '%s', updated_at = NOW() WHERE id = %s RETURNING ...",
c.Name, c.Email, c.Phone, c.Address, id)
// execute and return updated customer
}

Combines the ID from the URL with fields from the JSON body to update the record. Returns the updated customer or a 404 if the ID does not exist.

3.10 Delete customer handler

func deleteCustomerHandler(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/customers/")
query := fmt.Sprintf("DELETE FROM customers WHERE id = %s", id)
result, err := db.Exec(query)
// check RowsAffected, return success or 404
}

Deletes the customer with the given ID. Uses RowsAffected() to detect whether the customer existed — if zero rows were affected, it returns a 404.


4. Transfer code to app-server

On your Mac (not inside an SSH session), copy the source files to app-server:

scp src/backend/* app-server:~/customerapp/

This copies both go.mod and main.go to the ~/customerapp directory on app-server.


5. Install dependencies and build

SSH into app-server and build the application:

ssh app-server
cd ~/customerapp

5.1 Download dependencies

go mod tidy

This reads go.mod, downloads the lib/pq driver, and creates a go.sum file with checksums for integrity verification.

5.2 Build the binary

go build -o customerapp .

This compiles the application into a single binary called customerapp. Go produces statically-linked binaries, so there are no runtime dependencies to install.


6. Run and test

6.1 Run the application

./customerapp

You should see:

2024/01/01 12:00:00 Connected to database
2024/01/01 12:00:00 Server starting on :8080

Leave this running and open a new terminal on your Mac to test the endpoints.

6.2 Health check

curl http://192.168.56.12:8080/health

Expected response:

{"database":"connected","status":"healthy"}

6.3 Login

curl -X POST http://192.168.56.12:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'

Save the cookie for subsequent requests:

curl -c cookies.txt -X POST http://192.168.56.12:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'

6.4 Create a customer

curl -b cookies.txt -X POST http://192.168.56.12:8080/api/customers \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com","phone":"555-0100","address":"123 Main St"}'

Expected: a JSON object with the new customer including an auto-assigned id.

6.5 List all customers

curl -b cookies.txt http://192.168.56.12:8080/api/customers

Expected: a JSON array containing the customer you just created.

6.6 Get a single customer

curl -b cookies.txt http://192.168.56.12:8080/api/customers/1

6.7 Update a customer

curl -b cookies.txt -X PUT http://192.168.56.12:8080/api/customers/1 \
-H "Content-Type: application/json" \
-d '{"name":"John Updated","email":"john@example.com","phone":"555-0100","address":"456 Oak Ave"}'

6.8 Delete a customer

curl -b cookies.txt -X DELETE http://192.168.56.12:8080/api/customers/1

Expected:

{"message":"Customer deleted successfully"}

Once all endpoints work, press Ctrl+C to stop the application. We will set it up as a service next.


7. Set up systemd service

Running the application manually is fine for testing, but in production you want it to start automatically and restart on failure.

7.1 Create the service file

sudo tee /etc/systemd/system/customerapp.service > /dev/null <<EOF
[Unit]
Description=Customer App Go Backend
After=network.target

[Service]
Type=simple
User=trainee
WorkingDirectory=/home/trainee/customerapp
ExecStart=/home/trainee/customerapp/customerapp
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

This tells systemd to run the binary as the trainee user, restart it if it crashes, and start it on boot.

7.2 Enable and start the service

sudo systemctl daemon-reload
sudo systemctl enable customerapp
sudo systemctl start customerapp

7.3 Verify the service is running

sudo systemctl status customerapp

You should see active (running). You can also check the logs:

sudo journalctl -u customerapp -f

7.4 Test again

From your Mac, verify the API still works through the systemd service:

curl http://192.168.56.12:8080/health

Troubleshooting

"dial tcp 192.168.56.11:5432: connect: connection refused"

The app cannot reach PostgreSQL. Verify:

  1. PostgreSQL is running on db-server: sudo systemctl status postgresql
  2. pg_hba.conf allows connections from 192.168.56.12 (see Module 02, section 4)
  3. postgresql.conf has listen_addresses = '*' (see Module 02, section 4)
  4. Test from app-server: psql -h 192.168.56.11 -U appuser -d customerdb

"go: command not found"

Go is not in your PATH. Run:

export PATH=$PATH:/usr/local/go/bin

Then add it to ~/.bashrc permanently (see section 1.2).

"cannot find package github.com/lib/pq"

Run go mod tidy in the project directory to download dependencies.

"listen tcp :8080: bind: address already in use"

Another process is using port 8080. Find and stop it:

sudo lsof -i :8080
sudo kill <PID>

Service fails to start

Check logs for details:

sudo journalctl -u customerapp --no-pager -n 50

Common causes: wrong WorkingDirectory path, missing binary, database not reachable.


Summary

You now have a working Go REST API running on app-server that connects to PostgreSQL on db-server. The API provides full CRUD operations for customers and a simple cookie-based login system. In the next module we will build the frontend that talks to this API.