Golang CRUD Rest API using Mux, Postgres, Docker and Docker Compose
Tags: go, docker, postgres, beginners
Published: 2023-02-26 08:56:18

Let's create a CRUD Rest API in GO, using:

- Mux (Framework to build web servers in Go)
- Postgres (relational database)
- Docker (for containerization)
- Docker Compose 

If you prefer a video version:

{% youtube aLVJY-1dKz8 %}

All the code is available in the GitHub repository (link in the video description): https://youtube.com/live/aLVJY-1dKz8

___
## 🏁 Intro

Here is a schema of the architecture of the application we are going to create:

[![crud, read, update, delete, to a go app and postgres service, connected with docker compose. Postman and tableplus to test it](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rhpvqqw7ayt1u9d8xwfz.png
)](https://youtube.com/live/aLVJY-1dKz8)

We will create 5 endpoints for basic CRUD operations:
- Create
- Read all
- Read one
- Update
- Delete

Here are the steps we are going through:

1. Create a Go application using Mux as a framework
2. Dockerize the Go application writing a Dockerfile and a docker-compose.yml file to run the application and the database.
3. Run the Postgres database in a container using Docker Compose, and test it with TablePlus.
4. Build the Go App image and run it in a container using Docker Compose, then test it with Postman.

We will go with a step-by-step guide, so you can follow along.

___
## 🏁 Create a GO application using Mux as a framework

Create a new folder:
```
mkdir go-crud-api
```

step into the folder:
```
cd go-crud-api
```

initialize a new Go module b using this command:
```
go mod init api
```

Install the dependencies:
```
go get github.com/gorilla/mux github.com/lib/pq
```

We need just 3 more files for the Go application, including containerization.

You can create these files in different ways. One of them is to create them manually, the other one is to create them with the command line:
```
touch main.go Dockerfile docker-compose.yml
```

Open the folder with your favorite IDE. I am using VSCode, so I will use the command:
```
code .
```

Your project folder should look like this:


[![folder structure - Build a CRUD Rest API in Go using Mux, Postgres, Docker and Docker Compose](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uas5lqedig3bhyxdd85t.png)](https://youtube.com/live/aLVJY-1dKz8)

___
## 🗒️ main.go file

The main.go file is the main file of the application: it contains all the endpoints and the logic of the app.

Populate the main.go file as follows:

```go
package main

import (
	"database/sql"
	"encoding/json"
	"log"
	"net/http"
	"os"

	"github.com/gorilla/mux"
	_ "github.com/lib/pq"
)

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

func main() {
	//connect to database
	db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	//create the table if it doesn't exist
	_, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)")

	if err != nil {
		log.Fatal(err)
	}

	//create router
	router := mux.NewRouter()
	router.HandleFunc("/users", getUsers(db)).Methods("GET")
	router.HandleFunc("/users/{id}", getUser(db)).Methods("GET")
	router.HandleFunc("/users", createUser(db)).Methods("POST")
	router.HandleFunc("/users/{id}", updateUser(db)).Methods("PUT")
	router.HandleFunc("/users/{id}", deleteUser(db)).Methods("DELETE")

	//start server
	log.Fatal(http.ListenAndServe(":8000", jsonContentTypeMiddleware(router)))
}

func jsonContentTypeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		next.ServeHTTP(w, r)
	})
}

// get all users
func getUsers(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.Query("SELECT * FROM users")
		if err != nil {
			log.Fatal(err)
		}
		defer rows.Close()

		users := []User{}
		for rows.Next() {
			var u User
			if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
				log.Fatal(err)
			}
			users = append(users, u)
		}
		if err := rows.Err(); err != nil {
			log.Fatal(err)
		}

		json.NewEncoder(w).Encode(users)
	}
}

// get user by id
func getUser(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		id := vars["id"]

		var u User
		err := db.QueryRow("SELECT * FROM users WHERE id = $1", id).Scan(&u.ID, &u.Name, &u.Email)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		json.NewEncoder(w).Encode(u)
	}
}

// create user
func createUser(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var u User
		json.NewDecoder(r.Body).Decode(&u)

		err := db.QueryRow("INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id", u.Name, u.Email).Scan(&u.ID)
		if err != nil {
			log.Fatal(err)
		}

		json.NewEncoder(w).Encode(u)
	}
}

// update user
func updateUser(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var u User
		json.NewDecoder(r.Body).Decode(&u)

		vars := mux.Vars(r)
		id := vars["id"]

		_, err := db.Exec("UPDATE users SET name = $1, email = $2 WHERE id = $3", u.Name, u.Email, id)
		if err != nil {
			log.Fatal(err)
		}

		json.NewEncoder(w).Encode(u)
	}
}

// delete user
func deleteUser(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		id := vars["id"]

		var u User
		err := db.QueryRow("SELECT * FROM users WHERE id = $1", id).Scan(&u.ID, &u.Name, &u.Email)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		} else {
			_, err := db.Exec("DELETE FROM users WHERE id = $1", id)
			if err != nil {
				//todo : fix error handling
				w.WriteHeader(http.StatusNotFound)
				return
			}
	
			json.NewEncoder(w).Encode("User deleted")
		}
	}
}
```

Explanations:

We are importing:
``database/sql`` as a connector to the Postgres db
``encoding/json`` to work easily with objects in json format
``log`` to log errors
``net/http`` to handle http requests
``os`` to handle environment variables

The struct defined is for an User with an Id (autoincremented by the db), a name and an email.

In the ``main function`` do some things:
- We connect to the Postgres db setting an evironment variable
- we create a table in the db if it doesn't exist
- we use Mux to handle the 5 endpoints
- we listen the server on the port 8000
- the function ``jsonContentTypeMiddleware`` is a middleware function to add a header (application/json) to al the responses. Nice to have the responses formatted properly and ready ot get used from an eventual frontend
- then there are 5 controller to Creat, Read, Update and Delete users.

___
## 🐳 Dockerize the Go application

Let's populate the ``Dockerfile`` :

```
# use official Golang image
FROM golang:1.16.3-alpine3.13

# set working directory
WORKDIR /app

# Copy the source code
COPY . . 

# Download and install the dependencies
RUN go get -d -v ./...

# Build the Go app
RUN go build -o api .

#EXPOSE the port
EXPOSE 8000

# Run the executable
CMD ["./api"]
```

Explanatiuon:
``FROM`` sets the base image to use. In this case we are using the golang:1.16.3-alpine3.13 image, a lightweight version

``WORKDIR`` sets the working directory inside the image

``COPY . . `` copies all the files in the current directory to the working directory

``RUN go get -d -v ./...`` Is a command to isntall the dependencies before building the image

``RUN go build -o api .`` build the Go app inside the Image filesystem

``EXPOSE 8000`` exposes the port 8000

``CMD ["./api"]`` sets the command to run when the container starts

___
## 🐳🐳Docker compose

The term "Docker compose" might be a bit confusing because it's referred both to a file and to a set of CLI commands. Here we will use the term to refer to the file.

Populate the ``docker-compose.yml`` file:

```
version: '3.9'

services:
  go-app:
    container_name: go-app
    image: francescoxx/go-app:1.0.0
    build: .
    environment:
      DATABASE_URL: "host=go_db user=postgres password=postgres dbname=postgres sslmode=disable"
    ports:
      - "8000:8000"
    depends_on:
      - go_db
  go_db:
    container_name: go_db
    image: postgres:12
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:  
  pgdata: {}

```

Explanation:

we just defined 2 services, ``go-app`` and ``go_db``

``go-app`` is the Go application we just Dockerized writing the Dockerfile

``go_db`` is a Postgres container, to store the data. We will use the official Postgres image

``version`` is the version of the docker-compose file. We are using the verwion 3.9

``services`` is the list of services (containers) we want to run. In this case, we have 2 services: "go-app" and "go_db"

``container_name`` is the name of the container. It's not mandatory, but it's a good practice to have a name for the container. Containers find each other by their name, so it's important to have a name for the containers we want to communicate with.

``image`` is the name of the image we want to use. I recommend replacing "dockerhub-" with YOUR Dockerhub account (it's free).

``build`` is the path to the Dockerfile. In this case, it's the current directory, so we are using ``.`` 

``ports`` is the list of ports we want to expose. In this case, we are exposing the port 8000 of the go-app container, and the port 5432 of the go_db container. The format is "host_port:container_port"

``depends_on`` is the list of services we want to start before this one. In this case, we want to start the Postgres container before the app container.

``environment`` is to define the environment variables. for the go-app, we will have a database url to configure the configuration. For the go_db container, we will have the environment variables we have to define when we wan to use the Postgres container (we can't change the keys here, because we are using the Postgres image, defined by the Postgres team).

``volumes`` in the go_db defines a named volume we will use for persistency. Containers are ephimerals by definition, so we need this additional feature to make our data persist when the container will be removed (a container is just a process).

``volumes`` at the end of the file is the list of volumes we want to create. In this case, we are creating a volume called ``pgdata``. The format is ``volume_name: {}``

___
## 👟 Run the Postgres container and test it with TablePlus

To run the Postgres container, type:

```
docker compose up -d go_db
```

The ``-d`` flag is to run the container in detached mode, so it will run in the background.

You should see something like this:

[![docker downlading image - Build a CRUD Rest API in GO using Mux, Postgres, Docker, ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0fs51fnuw6aizk63jilf.png)](https://youtube.com/live/aLVJY-1dKz8).

If the last line is ``LOG:  database system is ready to accept connections``, it means that the container is running and the Postgres server is ready to accept connections.

But to be sure, let's make another test.

To show all the containers (running and stopped ones) type:
```
docker ps -a
```
The output should be similar to this:

[![one container running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qfgpagm8d5h079gfigz4.png)](https://youtube.com/live/aLVJY-1dKz8)

Now, to test the db connection, we can use any tool we want. Personally, I use TablePlus.

Use the following configuration:

Host: ``localhost``

Port: ``5432``

User: ``postgres``

Password: ``postgres``

Database: ``postgres``

[![Tableplus interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8zt4zbr169a8xxerx92.png)](https://youtube.com/live/aLVJY-1dKz8)

Then hit "Test" (at the bottom-right). 

If you get the message "connection is OK" you are good to go.

[![TAbleplus, OK connection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/96boisx1566xyj1bqwzn.png)](https://youtube.com/live/aLVJY-1dKz8)

You can also click "Connect" and you will see an empty database. This is correct.

[![Tableplus empty but connected db](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/em8sr7gb57fj5w12cxhp.png)](https://youtube.com/live/aLVJY-1dKz8)

___
## 🔨 Build and run the GO application

Now, let's build and run the GO application.

Let's go back to the folder where the ``docker-compose.yml`` is located and type:
```
docker compose build
```

This should BUILD the go-app image, with the name defined in the "image" value. In my case it's francescoxx/go-app:1.0.0 because that's my Dockerhub username. You should replace "francescoxx" with your Dockerhub username.

You can also see all the steps docker did to build the image, layer by layer. You might recognize some of them, because we defined them in the Dockerfile.

[![docker build](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7dc3c6st9wkgzbg0boh5.png)](https://youtube.com/live/aLVJY-1dKz8).

Now, to check if the image has been built successfully, type:
```
docker images
```

We should see a similar result, with the image we just built:

[![2 docker images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cwof8jj3c61dn6wbxcjt.png)](https://youtube.com/live/aLVJY-1dKz8)

___
## ⚡ Run the go-app service

We are almost done, but one last step is to run a container based on the image we just built.

To do that, we can just type:   
```
docker compose up go-app
```

In this case we don't use the -d flag, because we want to see the logs in the terminal.

We should see something like this:

[![docker compose up command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e87030g308qbyfxzrv4o.png)](https://youtube.com/live/aLVJY-1dKz8)


___
## 🔍 Test the application

Let's test our application. First of all, let's see if the 
application is responding. To do this, make a GET request to ``localhost:8000/users``


[![GET request to localhost:8000/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dcjeicdr15of79et88mx.png)](https://youtube.com/live/aLVJY-1dKz8)


___
### 📝 Create a user

YNow let's create a user, making a POST request to ``localhost:8000/users`` with the body below as a request body:

[![POST request to localhost:8000/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iv0n5og3sy3yzm9sw8kt.png)](https://youtube.com/live/aLVJY-1dKz8)

Let's crete another one:

[![POST request to localhost:8000/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dlwiwmjkcm00zmmwgs37.png)](https://youtube.com/live/aLVJY-1dKz8)

One more:

[![POST request to localhost:8000/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t9eo1wenr8mj2jsu40xy.png)](https://youtube.com/live/aLVJY-1dKz8)

___
### 📝 Get all users
<br>
Now, let's make a GET request to ``localhost:8000/users`` to get all the users:

[![GET request to localhost:8000/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c724khctwmigbux1us4i.png)](https://youtube.com/live/aLVJY-1dKz8)

We just created 3 users.

___
### 📝 Get a specific user
<br>
If you want to get a specific user, you can make a GET request to ``localhost:8000/users/<user_id>``. 

For example, to get the user with id 2, you can make a GET request to ``localhost:8000/users/2``

[![GET request to localhost:8000/users/2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jmwtzm3hixgcqb1izea6.png)](https://youtube.com/live/aLVJY-1dKz8)

___
### 📝 Update a user
If you want to update a user, you can make a PUT request to ``localhost:8000/users/<user_id>``.

For example, to update the user with id 2, you can make a PUT request to ``localhost:8000/users/2`` with the body below as a request body:

[![PUT request to localhost:8000/users/2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2yn25zawdox81ynu393m.png)](https://youtube.com/live/aLVJY-1dKz8)

___
### 📝 Delete a user
<br>
To delete a user, you can make a DELETE request to ``localhost:8000/users/<user_id>``.

For Example, to delete the user with id 2, you can make a DELETE request to ``localhost:8000/users/2``

[![DELETE request to localhost:8000/users/2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aglqt6ruxm032r2djgqt.png)](https://youtube.com/live/aLVJY-1dKz8)

To check if the user has been deleted, you can make a GET request to ``localhost:8000/users``

[![GET request to localhost:8000/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e90264qqvwh8fz9huwfr.png)](https://youtube.com/live/aLVJY-1dKz8)

As you can see the user with id 2 is not there anymore.

___
## 🏁 Conclusion

We made it! We have built a CRUD rest API in Go, using Mux, Postgres, Docker and Docker compose.

This is just an example, but you can use this as a starting point to build your own application.

If you prefer a video version:

{% youtube aLVJY-1dKz8 %}

All the code is available in the GitHub repository (link in the video description): https://youtube.com/live/aLVJY-1dKz8

That's all. 

If you have any question, drop a comment below.

[Francesco](https://francescociulla.com)