# Introduction to Go - Create your first REST API - Part 2


>Last time I showed you how to [build a simple book store REST API](https://blog.vojko.dev/introduction-to-go-create-your-first-rest-api). This time I want to take it a step further and show you how to build an (almost) production-ready API with real database storage instead of an in-memory mock that we used. Also, I will show you how to dockerize our API.

> **NOTE**: In a hurry? Get the final code [here](https://gitlab.com/vojko.pribudic/bookstore-api/-/tree/part-two).

## Prerequisites

Obviously, the code from part one is required and you can find it [here](https://gitlab.com/vojko.pribudic/bookstore-api/).

Last time we already defined some of the prerequisites, so we will add some on top of those:

* [gorm.io/gorm](https://gorm.io/)

* [gorm.io/driver/postgres](https://gorm.io/)

* [Docker/docker-compose](https://www.docker.com/)


[**Go tour**](https://tour.golang.org/welcome/1) is still highly recommended if you did not look at it before.

## 1. Dockerization

**Docker** is an excellent tool in the web development world. Not only does it make our life easier in terms of **shipping the app** we are building, but it also makes your **local development** so much easier. Being cross-platform friendly, you really should make every app that you build **dockerized**. The whole idea behind Docker is that we will create a standalone image that will run our application on an OS that will power up our app and its dependencies.

Create a file named `Dockerfile` in your app root directory and put the following inside of it:

```
FROM golang:1.17-alpine

RUN apk update && apk upgrade && \
    apk add --no-cache bash git openssh

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download
COPY . .

RUN go install github.com/cespare/reflex@latest

EXPOSE 5000
CMD exec reflex -r '(\.go$|go\.mod)' go run main.go --start-service
```

So, we first state that we will inject our app to an existing **Alpine Linux image** that has Go preinstalled. We copy over the mod file and **install all the modules needed** in order for our app to run and then we copy over all of our application files. We also install [reflex](https://github.com/cespare/reflex) module that will come in handy for the local development since it will detect changes in your code and reload the application to apply changes. Lastly, we start the monitoring of all `*.go` files and spin up `main.go`. We expose our app on port 5000.

So far so good... But what about the database? Time to create docker-compose file :)

### 1.1 docker-compose

We use **docker-compose** as a service orchestrator of a docker image or multiple images. What the hell did I just say, eh? Well, our API will obviously need some sort of **database** in order to save our books information. We will hook it up just by making use of docker-compose.

Create a file named `docker-compose.yml` in your root directory again and put the following inside of it:

```
version: "3.5"
services:

  db:
    image: postgres:alpine
    restart: unless-stopped
    env_file:
      - ./.env
    ports:
      - "5432:5432"
    volumes:
      - ./db:/var/lib/postgresql/data

  bookstore-api:
    build: .
    volumes:
      - .:/app
    tty: true
    env_file:
      - ./.env
    ports:
      - "5000:5000"
    restart: unless-stopped
    depends_on:
      - database
    stop_signal: SIGINT

```

A bit confusing on the first look, but it is not that complicated really. We can see the **two services** in there, one is called `bookstore-api` and the other one is called `db`. For the `db` service, we can see that we have the `image` parameter set to `postgres:alpine` which means that **we won't install anything on our host machine**, Docker will instead pull the prebuilt PostgreSQL image and run it inside of a container that we will later connect to.

As for the `bookstore-api` service, we can see the parameter `build: .` which means that in order to build the service, Docker will look inside of the current directory (.) for a file called `Dockerfile` and do as instructed in there.

Both of the services are exposed at certain ports and both were given environment variables via the `env_file` parameter. So where is our `.env` file? Good question, **let's create one** inside of our root directory:

```
HOST=0.0.0.0
PORT=5000
DB_USER=pg
DB_PASSWORD=pass
DB_HOST=db
DB_PORT=5432
DB_NAME=crud
```

This is the bare minimum that we will need for what we are building today. As you can see, the `.env` file consists of the server and database-related variables that we need in order for our app to work. Let's move on and start to implement the code.

## 2. Tweaking the code

Since we will be making use of the database in order to store our data, we will need some kind of **ORM** to help us not to write raw SQL queries but instead to make use of our models (structures). Let's go ahead and install [GORM](https://gorm.io/) and its PostgreSQL driver:

```
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
```

Now before we continue further - remember the `.env` file we created earlier? Well, we need to "feed" that data inside of our app somehow. Inside of your `src` directory, create a new file called `config.go` and put the following inside of it:

```
package src

type Configuration struct {
	Host       string
	Port       string
	DBUser     string
	DBPassword string
	DBHost     string
	DBPort     string
	DBName     string
	DBString   string
}
```

We will come back later to this part of the code so leave it like this for now.

### 2.1 Main app modification

Let's create a structure that will hold all of the "parts" that our app will use. Open up the `app.go` form `src` directory and add the following:

```
type App struct {
	Config Configuration
	Router *mux.Router
	DB     *gorm.DB
}
```

This is the **main** structure that our app will use. We have a config, router, and db ORM inside of this handy app wrapper. Now that we have the `App` wrapper ready, we can modify our `main.go` form the root directory:

```
func main() {
	app := src.App{}
	app.Configure()
	app.Run()
}
```

As you can see above - we created a new instance from the `App` structure wrapper and we called two methods from it, `Configure()` and `Run()`. The whole idea is that everything will be handled just by making use of those two methods.

Now that we have our `App` wrapper, we can add more code to our `config.go` inside of the `src` directory:

```
func getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}

func (c *Configuration) loadEnv() {
	c.Host = getEnv("HOST", "0.0.0.0")
	c.Port = getEnv("PORT", "5000")
	c.DBUser = getEnv("DB_USER", "pg")
	c.DBPassword = getEnv("DB_PASSWORD", "pass")
	c.DBHost = getEnv("DB_HOST", "db")
	c.DBPort = getEnv("DB_PORT", "5432")
	c.DBName = getEnv("DB_NAME", "crud")
	c.DBString = fmt.Sprintf(
		"postgres://%s:%s@%s:%s/%s",
		c.DBUser,
		c.DBPassword,
		c.DBHost,
		c.DBPort,
		c.DBName,
	)
}
```

The `getEnv()` function will try to **load the environment variable** by a given key, and if it exists, it will return its value. If it does not exist, it will return the fallback value instead.

The `loadEnv()` function will take an instance from the `Configuration` structure and fill it with the data from the `.env` file.

Head back to your `app. go` inside of the `src` directory and first remove the `Start()` method since we won't make any use of it, and then add the following:

```
func (app *App) Configure() {
	app.configureEnv()
	app.configureDB()
	app.configureRoutes()
	app.configureMiddleware()
}

func (app *App) Run() {
	log.Fatal(
		http.ListenAndServe(
			fmt.Sprintf("%s:%s", app.Config.Host, app.Config.Port),
			handlers.LoggingHandler(os.Stdout, app.Router),
		),
	)
}
```

As you can see, the `Run()` function is responsible for running the `http` server on desired host/port combination (the one that will come from our `.env` file) and to log the events that happen in there.

The `Configure()` method is responsible for quite a few things. Let's add more code and see everything that it will configure:

```
func (app *App) configureEnv() {
	app.Config.loadEnv()
}
```

This one will pass the `Config` (Configuration structure) from the `App` wrapper to the `loadEnv()` function inside of the `config.go`. This way our `app.Config` w**ill get loaded with the environment variables**.

```
func (app *App) configureDB() {
	var err error
	app.DB, err = gorm.Open(postgres.Open(app.Config.DBString), &gorm.Config{})

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

	_ = app.DB.AutoMigrate(&Book{})
}
```

The second param inside of our `App` wrapper is the **GORM DB instance**. The whole point of this function is to establish a connection by using `DBString` from the `app.Config`. Since we already loaded the `app.Config` with the environment data, `DBString` is already set at this point.

**The important** line of code here is `_ = app.DB.AutoMigrate(&Book{})`. This line of code will create a table inside of our DB called `books` and it will create all the fields from our `Book` structure.

In order to make this work, we need to make the `Book` structure a GORM model. This can be easily done - open up the `src/data.go` and do the **following replacement** inside of the `Book` structure:

```
Id     int    `json:"id"     gorm:"primaryKey"`
```

The `gorm` annotation will make it a GORM model. Also, while we are editing the `data.go` **we should remove the `booksDB` variable** since this time we will use the real database.

Let's add more code to our `app.go`.


```
func (app *App) configureMiddleware() {
	app.Router.Use(commonMiddleware)
}
```

We already explained the middleware usage in the last part so this time it is just moved inside of a separate function (in case we decide to register more middleware later on) for the sake of code cleanliness.

```
func (app *App) configureRoutes() {
	app.Router = mux.NewRouter()
	app.Router.HandleFunc("/book", app.getAllBooks).Methods(http.MethodGet)
	app.Router.HandleFunc("/book", app.addBook).Methods(http.MethodPost)
	app.Router.HandleFunc("/book/{book_id:[0-9]+}", app.getBook).Methods(http.MethodGet)
	app.Router.HandleFunc("/book/{book_id:[0-9]+}", app.updateBook).Methods(http.MethodPut)
	app.Router.HandleFunc("/book/{book_id:[0-9]+}", app.deleteBook).Methods(http.MethodDelete)
}
```

This is also from the last part. We configure our routes on the `app.Router`. Other than just being moved to the separate function, **we also did a tiny modification in terms of handlers**. All of our handlers now have the `app.` prefix. Since we will GORM to store the data, our handlers need to access `app.DB` somehow and that is why we will pass `app` as a handler of the methods.

We have all of the functions needed in order for the `Configure()` function to work. Time to modify our handlers.

### 2.2 Handlers modification

Inside of our handlers, we will need to make use of DB from the `App` structure wrapper.

However, before we do that, we will need to modify our `src/helpers.go`. In part one we created a method to check for duplicates, but since we removed the `booksDB` from the `src/data.go` we will need to modify our `helpers.go`. Inside of the `helpers.go` remove both `removeBook()` and `checkDuplicateBookId()` functions. We will also add this function:

```
func (app *App) checkBookExists(field string, value string) bool {
	var count int64
	err := app.DB.Model(&Book{}).Select("id").Where(fmt.Sprintf("%s = ?", field), value).Count(&count).Error
	if err != nil {
		return false
	}
	if count > 0 {
		return true
	}
	return false
}
```

This function will try to get the `id` from the `Book` model (`books` table in the DB) by trying to find the row in our DB that has a `field` with a given `value`. So for example we will check the field `title` for the value `Harry Potter` in order to determine if it exists. Function will return `true` or `false`.

#### 2.5.1 Get a single book

```
func (app *App) getBook(w http.ResponseWriter, r *http.Request) {
	var book Book
	vars := mux.Vars(r)
	bookId, _ := strconv.Atoi(vars["book_id"])
	result := app.DB.First(&book, bookId)
	if result.Error == nil {
		_ = json.NewEncoder(w).Encode(book)
		return
	}
	JSONResponse(w, http.StatusNotFound, "")
}
```

The code is very similar to what we had in part one. The main difference is that we are now pulling the book from the database. We need to create the empty `Book` structure variable and search the DB for the first match from the `books` table that has the `id` set to the value of `bookId`, If we find it, we fill the `book` model variable with the data from the DB, else we will return `404: Not Found` status back.

#### 2.5.2 Get all books

```
func (app *App) getAllBooks(w http.ResponseWriter, r *http.Request) {
	var books []Book
	_ = app.DB.Order("id asc").Find(&books)
	_ = json.NewEncoder(w).Encode(books)
}
```

We first create a slice of `Books` since we are expecting multiple books from the DB. We search the DB for all of the data from the `books` table and populate the `books` slice with the data. 

#### 2.5.3 Add a new book

```
func (app *App) addBook(w http.ResponseWriter, r *http.Request) {
	var book Book
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&book)
	if err != nil {
		JSONResponse(w, http.StatusBadRequest, "")
		return
	}
	if app.checkBookExists("title", book.Title) {
		JSONResponse(w, http.StatusConflict, "")
		return
	}
	result := app.DB.Create(&book)
	if result.Error == nil {
		w.WriteHeader(201)
		_ = json.NewEncoder(w).Encode(book)
	}
}
```

First, we decode the payload body against the `book` model as we did in part one. If everything is ok, we need to check for duplicates. For this example, a duplicate is treated as a book of the same title. If there are no duplicates found, we create a new book inside of our DB with the data from the `book` model variable.

#### 2.5.4 Update existing book

```
func (app *App) updateBook(w http.ResponseWriter, r *http.Request) {
	var book Book
	var newBook Book
	vars := mux.Vars(r)
	bookId, _ := strconv.Atoi(vars["book_id"])
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&newBook)
	if err != nil {
		JSONResponse(w, http.StatusBadRequest, "")
		return
	}
	if app.checkBookExists("title", newBook.Title) {
		JSONResponse(w, http.StatusConflict, "")
		return
	}
	result := app.DB.First(&book, bookId)
	if result.Error == nil {
		newBook.Id = bookId
		app.DB.Save(&newBook)
		_ = json.NewEncoder(w).Encode(newBook)
		return
	}
	JSONResponse(w, http.StatusNotFound, "")
}
```

For this one, we have to create two `Book` model instances. One will have the new data from the payload and the other one will contain the data that we try to get from the DB. since this is an update method,  we first must check that the book we are trying to update already exists. If it does, we need to check for the new data that we are updating in order to determine if we will create a duplicate by modifying the data. If everything is ok, we will save the new data over the old data (old book id).

#### 2.5.5 Delete existing book

```
func (app *App) deleteBook(w http.ResponseWriter, r *http.Request) {
	var book Book
	vars := mux.Vars(r)
	bookId, _ := strconv.Atoi(vars["book_id"])
	result := app.DB.First(&book, bookId)
	if result.Error == nil {
		app.DB.Delete(&book)
		return
	}
	JSONResponse(w, http.StatusNotFound, "")
}
```

We need to find the book by a given `bookId` and if it exists, we will simply delete that row from the DB.

## 3. Building and running the app

Let's see what we created :)

In order to run the app, first, we need to build it. Run the following command in your terminal:

```
docker-compose build
```

Once the image is built successfully, we will spin it up:

```
docker-compose-up
```

VOILA! Your API is up and running inside of the Docker image we just build. Feel free to test it with Insomnia / Postman.

The final code can be found [here](https://gitlab.com/vojko.pribudic/bookstore-api/-/tree/part-two).

Like always, thanks for reading :)
