Guía API CRUD con Go y PostgreSQL
Tags: tutorial, go, postgres, beginners
Published: 2021-02-08 20:35:14

## ¿Porque escoger Go para crear una API? 🤔
[Go](https://golang.org/doc/) es un lenguaje fantástico creado por Google, **fuertemente tipado, fácil de leer y con multi-hilo y asincronismo por defecto**.
En este post vamos a crear una API **CRUD de lista de Videojuegos** (Porque el típico TO DO es muy aburrido 😘) con [Go Fiber](https://gofiber.io/) y [GORM](http://gorm.io/) con una base de datos PostgreSQL, el proyecto se llamara **gamelist**. 

### Requisitos Previos 🛑
Como punto de partido tendremos un proyecto de Go usando **Go Modules**, tener una instancia de GORM con PostgreSQL y un servidor básico con Fiber. Si no sabes como, en posts anteriores cubro estos temas, combínalos y sigue el tutorial.
{% post https://dev.to/marlosrodriguez/tutorial-conectar-postgressql-en-go-con-gorm-3ggj %}
{% post https://dev.to/marlosrodriguez/guia-de-crear-una-api-con-go-fiber-de-0-1b2p %}

### La Estructura del Proyecto
Si has seguido los tutoriales previos, tendras dentro de tu proyecto un archivo `main.go` con un servidor de Fiber y una carpeta con el archivo `/storage/connectDB.go` con la conexión a la base de datos. Creamos 3 carpetas mas:
* **handlers**: Aquí crearemos las funciones HTTP.
* **models**: Nuestros modelos para la bases de datos.
* **utils**: Y aquí nuestras funciones y herramientas para nuestro proyecto.

## ¿Que son las variables de Entorno? Y porque tienes que usarlas 🤗
Las Variables de Entorno (O ENV por "Environment variables") son variables de valor dinámico que afectan el funcionamiento de la App. En proyectos de programación es información importante que afecta su funcionamiento y que **debería estar oculto** al publicarlo (Por ejemplo al subirlo al GitHub), en estos archivos deben estar cosas como los datos de conexión a la base datos.

Para continuar con el proyecto vamos a usar ENV.

## Usar ENV en nuestro proyecto 🤫
Para usar ENV agregamos en la ruta base de nuestro proyecto un archivo `.env`, la sintaxis de los ENV es `key=value`. Vamos a tomar los datos de nuestra Base de datos, y ponerla en nuestro ENV:
```
DB_HOST=localhost
DB_USER=postgres
DB_PORT=5432
DB_PASSWORD=password
DB_NAME=db
```
Los nombres de las variables suelen estar en mayúsculas y separados por "_". 

Para acceder a nuestras variables utilizares el paquete [os](https://golang.org/pkg/os/) y su función [Getenv](https://golang.org/pkg/os/#Getenv) `os.Getenv(key)` esta función buscara la variable que le pasemos y **si existe retornara el valor**.

**¡Alto! Importante** Esto por si solo no nos servira a nosotros ¿Por que? Porque esto busca en las ENV por defecto de Go, solo con agregar el archivo `.env` el programa no sabra que existe y no cargara nuestras variables.

Para que nuestra aplicación carga las variables y podamos usarlas, usaremos el paquete [GoDotEnv](https://github.com/joho/godotenv) que **cargara nuestro archivo `.env` permitiendo usarlas**. lo agregamos con el comando `go get`:
```bash
go get github.com/joho/godotenv
```
Para usarlo agregamos en el archivo donde usemos las ENV, en los `import` el paquete [GoDotEnv](https://github.com/joho/godotenv):
````go
import (
	//Autoload the env
	_ "github.com/joho/godotenv/autoload"
)
```
Al importar el `autoload` como su nombre indica solo al importarlo cargara nuestro archivo `.env`. Otra forma de hacerlo, pero que puede darle problemas en el deploy, es:
````go
import (
    "github.com/joho/godotenv"
    "log"
    "os"
)

func main() {
  err := godotenv.Load()
  if err != nil {
    log.Fatal("Error loading .env file")
  }

  val:= os.Getenv("key")
}
```

### Implementar ENV y mejores practicas 🤓
Ahora con el paquete [GoDotEnv](https://github.com/joho/godotenv), **podemos usar ENV en todo el proyecto** pero hay ciertas cosas en tener en cuenta.
* Orden de uso: Esto es algo importante, como dije podemos usar GoDotEnv en todo el proyecto, pero el orden es importante, deberías usar este paquete cargar el `.env` en una etapa temprana del programa, porque puede que intente cargar una variable importante y no lo encuentra.
* Rendimiento: Acceder al ENV es un poco lento, ten esto en cuenta y no abuses de esto.

Para evitar esto, me gusta crear en mis proyecto la utilidad `accessENV.go` el cual se encargara de acceder a todas nuestras variables y las guardara en una cache para que cuando se necesite otra vez podrá accederse rápidamente.

Creamos el archivo `accessENV.go` dentro de la carpeta `utils`, importamos los paquetes `os` `sync` y `GoDotEnv` crearemos una función con el mismo nombre `accessENV` y fuera de la función crearemos nuestra "cache" que sera un `map` de Go con un `RWMutex` del paquete `sync`, este sera el resultado de la función:
```go
package utils

import (
	"os"
	"sync"

	//Autoload the env
	_ "github.com/joho/godotenv/autoload"
)

var (
	environment      = map[string]string{}
	environmentMutex = sync.RWMutex{}
)

//AccessENV Return the ENV if exits
func AccessENV(key string) string {
        //Obtiene el valor del map, si existe regresa el valor
	environmentMutex.RLock()
        val := environment[key]
	environmentMutex.RUnlock()

	if environment[key] != "" {
		return val
	}

        //Si el valor no existe, lo obtiene de ENV
	val := os.Getenv(key)

	if val == "" || len(val) <= 0 {
		return ""
	}

        //Si existe en ENV, lo asigna al map
	environmentMutex.Lock()
	environment[key] = val
	environmentMutex.Unlock()

	return val
}
```
#### ¿Que es el `RWMutex` y porque no uso solo el `map`?
> RWMutex is a reader/writer mutual exclusion lock

Esto quiere decir que bloquea la lectura y escritura de los `map`, el cual **es necesario si se trabaja con gorutines**, porque puede dar un `fatal error` debido que si no se bloquea, dos gorutines pueden intentar leer o escribir al mismo tiempo, corrompiendo los datos y deteniendo la aplicación.

La forma es que como funciona es que:
* `Lock` `Unlock` para bloquear y desbloquear la **escritura**
* `RLock` `RUnlock` para bloquear y desbloquear la **lectura**
El uso de maps es seguro dentro de las funciones y no dará ningún error.

Ahora en podemos usar esto y acceder a nuestras variables de manera segura y rápida. Con nuestra función vamos a cambiar la función de `ConnectDB()` para acceder a los datos de la base de datos desde el archivo `.env`, cambiamos el codigo de esta manera:
```go
package storage 

import ( 
  "fmt" 
  "log" 
  "strconv"
  "github.com/jinzhu/gorm"  
  //Postgres Driver imported 
  _ "github.com/lib/pq"
  "gamelist/utils"
)
func ConnectDB() *gorm.DB {
   var (
		host     = utils.AccessENV("DB_HOST")
		user     = utils.AccessENV("DB_USER")
		port     = utils.AccessENV("DB_PORT")
		password = utils.AccessENV("DB_PASSWORD")
		name     = utils.AccessENV("DB_NAME")
	)
	if host == "" {
		log.Fatalln("Error loading ENV")
		return nil
	}

	portInt, err := strconv.Atoi(port)

	if err != nil {
		log.Fatalln("Error en convertir el port : " + err.Error())
		return nil
	}
}
```

El resto del código es igual, ahora comprobamos si las variables están vacías, en cuyo caso ocurrió un **error al obtener los valores de ENV**. Luego se convierte el port de `string` a `int`, **formato que la función necesita**.

## Crear el modelo de `Juego`
Para poder trabajar con [GORM](http://gorm.io/) necesitas crear un modelo. Dentro de la carpeta `models` creamos el archivo `juego.go`, para este proyecto solo usaremos un modelo, el cual sera `Juego` que tendra `Nombre`, `Desarrollador` y `precio`, usaremos el `Model` de GORM que nos provee con **todo lo necesario para manipular la información**:
```go
package models

import (
	"time"

	"github.com/jinzhu/gorm"
) 

//Juego estructura 
type Juego struct {
        gorm.Model
        Nombre         string `gorm:"not null" json:"nombre"`
        Desarrollador  string `gorm:"not null" json:"desarrollador"`
        Precio         int    `gorm:"not null" json:"precio"`
}
```
El `gorm.Model` nos agrega esto a nuestro `Juego`:
```go
// gorm.Model definition
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}
```
Ahorrándonos de agregarlos nosotros, por ultimo agregamos un `Hook` que nos permite GORM. Este se llamara `AfterUpdate` que se ejecuta cada que actualizamos algo y la usaremos para actualizar el dato `UpdatedAt` automáticamente, este es el código:
```go
// Updating data in same transaction
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
  u.UpdatedAt = time.Now()
  return
}
```
Por ultimo para poder usarlo, tenemos que hacer la migración en la base de datos. Dentro del `ConnectDB()` antes del `return` agregamos `DB.AutoMigrate(&models.User{})` esto hara la migración en la base de datos si no existe, y si existe simplemente la ignorara evitando cualquier tipo de problemas.

## Ya cree el modelo ¿Ahora como lo uso? Creando el CRUD de la DB 👨‍💻
Esto constara de 5 funciones que interactuarán con nuestra base de datos y sera la base de nuestro CRUD. Dentro de la carpeta storage creamos `juegos.go` donde crearemos nuestro CRUD.

importaremos estos paquetes:
```go

import (
	"log"
	"time"
	"github.com/jinzhu/gorm"

	"gamelist/models"
)
```
Existe diferentes maneras de trabajar con la instancia de la base de datos, en este tutorial crearemos una estructura con la base de datos y agregaremos los métodos de nuestro CRUD.

```go
//JuegoDB struct
type JuegoDB struct {
	db  *gorm.DB
}
```

esto creara una estructura con una instancia de GORM como parámetro, podremos crear tantas como queramos, importante que nombre de la instancia de GORM este en minuscula y asi no se exporte. Para poder usarlo tenemos que crear una nueva con una instancia de GORM conectada a nuestra base de datos, para ellos crearemos una función que la cree:
```go
//NuevoJuegoDB Create a new storage user service
func NuevoJuegoDB () JuegoDB {
        nuevaDB := ConnectDB()
	nuevoServicio := JuegoDB{db: nuevaDB}

	return nuevoServicio 
}
```
Esto creara una nueva instancia y sera lo que usemos después en nuestros handlers. Como estamos en la misma carpeta que la función `ConnectDB()` podremos usarla sin necesidad de ninguna importación.

### Crear los métodos del CRUD
Ahora que podemos crear una nueva instancia `JuegoDB` necesitamos agregar los métodos a nuestra estructura, esto hara que al crear una nueva instancia en `NuevoJuegoDB()` venga con funciones que podremos usar, para agregarlo como metodo tenemos que crear una función normal pero con `(j *JuegoDB)` antes del nombre, así que vamos a hacerlo.

* **Obtener un juego**: 
```go
//ObtenerJuego del juego con el ID
func (j *JuegoDB) ObtenerJuego(id int) *models.Juego, error {
        var juego *models.Juego = new(models.Juego)     
        // SELECT * FROM juegos WHERE id = 10;
        if err := j.db.First(&juego, id).Error; err != nil {
		return nil, err
	}
        
        return juego, nil
}
```
Esta función da por hecho que el `id` existe y no esta vacío, comprobaremos el id en los handlers. Esta función crea una variable vacia, que se usara en la DB, GORM buscara automaticamente en función del modelo de la variable y le pasaremos el `id` como parametro. En caso de error regresara un `nil` y el error, si todo salio bien, regresara el `juego` y un error `nil`.

* **Obtener todos los Juegos**: 
```go
//ObtenerJuegos Obtener todos los Juegos
func (j *JuegoDB) ObtenerJuegos () []*models.Juego, error {
        var juego []*models.Juego = []*models.Juego{new(models.Juego)     
        // SELECT * FROM juegos;
        if err := j.db.Find(&juego).Error; err != nil {
		return nil, err
	}
        
        return juego, nil
}
```

* **Crear un Juego**: 
```go
//CrearJuego crea un Juego
func (j *JuegoDB) CrearJuego(nuevoJuego *models.Juego) *models.Juego, bool, error {
        if err := j.db.Create(&nuevoJuego).Error; err != nil {
		return nil, false, err
	}
        
        return *nuevoJuego, true, nil
}
```

* **Modificar un Juego**: 
```go
//ModificarJuego Modifica el Juego
func (j *JuegoDB) ModificarJuego(nuevoJuego *models.Juego) bool, error {
        if err := j.db.Model(&juego).Updates(&nuevoJuego).Error; err != nil {
		return false, err
	}
        
        return true, nil
}
```
En esta función se utiliza `Updates` que actualiza todos los campos que no estén vacíos. GORM, encadena las funciones, al agregar `Model()` le decimos donde buscar, luego la función `Updates` buscara con el `id` del objeto y actualizara en la base de datos.

* **Eliminar un Juego**:
```go
//EliminarJuego Elimina un Juego
func (j *JuegoDB) EliminarJuego(id int) bool, error {
        if err := j.db.Delete(&nuevoJuego, id).Error; err != nil {
		return false, err
	}
        
        return true, nil
}
```

Estas son todas las funciones de nuestro CRUD, **Podremos acceder a los juegos, crearlo, modificarlo y eliminarlo**.

## Ahora con las funciones de la DB, Los Handlers 😎

Como ya dije, usaremos [Go Fiber](https://gofiber.io/) para nuestros Handlers y administrar las peticiones HTTP. Usaremos la misma estructura que las funciones de la Base de datos. Crearemos una estructura y le agregaremos las funciones de nuestro handler.
```go
import (
     "strconv"
     "strings"
     "time"

      "github.com/go-redis/redis/v8"
      "github.com/gofiber/fiber/v2"
      "github.com/jinzhu/gorm"

      "gamelist/storage"
      "gamelist/models"
)

type juegosHandler struct {
     BaseDatos storage.JuegoDB
}
```

Y crearemos la función para crear un nuevo handler:
```go
//NuevoJuegosHandler Crear un nuevo handler
func NuevoJuegosHandler() *juegosHandler {
	//Regresa nuevo Handler
	return &juegosHandler{
		BaseDatos: storage.NuevoJuegoDB(),
	}
}
```
Ahora crearemos nuestros handlers usando [Go Fiber](https://gofiber.io/), la forma de un handler de Fiber es: `func Nombre(c *fiber.Ctx) error {...}`, son todas las mismas, toma como metodo el `Context` o contexto de Fiber y regresa un `error` si todo sale bien sera `nil`. Ahora vamos a crear nuestras funciones:

* **Obtener un Juego por ID**:
```go
//ObtenerJuego Obtener un Juego por ID
func (j *juegosHandler) ObtenerJuego(c *fiber.Ctx) error {
        //Obtener el ID desde los parametros, es uno de los metodos de Fiber
	ID := c.Params("id")

        //Si el ID esta vacio, no se envio asi que regresa un error
	if len(ID) < 0 {
                //Esta es la forma de regresar errores en fiber
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Review your input"})
	}
        ...
}
```
Para enviar errores en Fiber tienes que enviar un numero de `Status` o Estatus, que indique que ha pasado con la petición, Fiber viene con codigos por defecto que son los estandares en la Web. Y opcionalmente **puedes mandar información en formato JSON** con la información del servidor, en este caso el error. Ahora convertiremos el ID, que es un `string` a un `int` para ser usado en la Base de datos:
```go
//ObtenerJuego Obtener un Juego por ID
func (j *juegosHandler) ObtenerJuego(c *fiber.Ctx) error {
        ...
        //Convertir ID a int
        IntID, err := strconv.Atoi(ID)

	if err != nil {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error converting in Integer", "data": err.Error()})
	}
        //Obtener datos del Juego
	juego, err := j.BaseDatos.ObtenerJuego(ID)

	if err != nil {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error in DB", "data": err.Error()})
	}

        //Regresar los datos del juego, Go lo convertira en JSON
	return c.Status(fiber.StatusAccepted).JSON(juego)
}
```
* **Obtener todos los juegos**: Esta es mas simple que el anterior, no tenemos que hacer ninguna comprobación por parte del usuario, solo asegurarnos de obtener la lista de Juegos.
```go
//ObtenerTodosJuegos Obtener Todos los Juegos
func (j *juegosHandler) ObtenerTodosJuegos(c *fiber.Ctx) error {
        //Obtener datos del Juego
	juego, err := j.BaseDatos.ObtenerJuego(ID)

	if err != nil {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error in DB", "data": err.Error()})
	}

        //Regresar los datos del juego, Go lo convertira en JSON
	return c.Status(fiber.StatusAccepted).JSON(juego)
}
```
Al igual que con el anterior, Go lo convertira en JSON automáticamente, no importa si es un array.

* **Crear un Juego**:
```go
//CrearJuego Crea un nuevo Juego
func (j *juegosHandler) CrearJuego(c *fiber.Ctx) error {
        //Crear el nuevo juego
	var nuevoJuego *models.Juego

        //Obtener los datos del body
        if err := c.BodyParser(&nuevoJuego); err != nil {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Revisa tu Body", "data": err.Error()})
	}
 
        //Comprobar los datos del juego
        //Nombre
	if len(strings.TrimSpace(nuevoJuego.Nombre)) <= 0 || strings.TrimSpace(nuevoJuego.Nombre) == "" {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "El Nombre del juego es obligatorio")
	}

        //Desarrollador
	if len(strings.TrimSpace(nuevoJuego.Desarrollador)) <= 0 || strings.TrimSpace(nuevoJuego.Desarrollador) == "" {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "El Desarrolladordel juego es obligatorio")
	}

        //Precio
	if len(nuevoJuego.Precio) < 0 {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "El precio no puede ser negativo")
	}

        //Crear el Juego
	juego, exitoso, err := j.BaseDatos.CrearJuego(nuevoJuego)

	if err != nil || !exitoso {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error in DB", "data": err.Error()})
	}

        //Regresar los datos del juego, Go lo convertira en JSON
	return c.Status(fiber.StatusAccepted).JSON(juego)
}
```
En esta función obtendremos los datos del nuevo juego del `body` con los datos que el usuario deberia enviar, tendra que enviar un `JSON` como este:
```json
{
        "nobmre": "Dark Souls",
        "desarrollador": "From Software",
        "precio": 20
}
```

* **Modificar un Juego**: Esta sera muy similar al anterior pero verificaremos los valores y si no lo envia lo tendra vacio y asi GORM lo ignorara:

```go
//ModificarJuego Modificia un Juego
func (j *juegosHandler) ModificarJuego (c *fiber.Ctx) error {
        //Crear el nuevo juego
	var body *models.Juego

        //Obtener los datos del body
        if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Revisa tu Body", "data": err.Error()})
	}

        //Crear el nuevo juego
	var nuevoJuego *models.Juego
 
        //Comprobar los datos del juego
        //Nombre
	if len(strings.TrimSpace(body.Nombre)) <= 0 || strings.TrimSpace(body.Nombre) == "" {
		nuevoJuego.Nombre = body.Nombre
	}

        //Desarrollador
	if len(strings.TrimSpace(body.Desarrollador)) <= 0 || strings.TrimSpace(body.Desarrollador) == "" {
		nuevoJuego.Desarrollador= body.Desarrollador
	}

        //Precio
	if len(body.Precio) < 0 {
		nuevoJuego.Precio= body.Precio
	}

        //Modificar el Juego
	exitoso, err := j.BaseDatos.ModificarJuego(nuevoJuego)

	if err != nil || !exitoso {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error in DB", "data": err.Error()})
	}

        //Esto regresara el numero de estatus de que todo salio bien
	return c.SendStatus(fiber.StatusAccepted)
}
```

* **Eliminar un Juego**: 
```go
//EliminarJuego Elimina un Juego por ID
func (j *juegosHandler) EliminarJuego(c *fiber.Ctx) error {
        //Obtener el ID desde los parametros, es uno de los metodos de Fiber
	ID := c.Params("id")

        //Si el ID esta vacio, no se envio asi que regresa un error
	if len(ID) < 0 {
                //Esta es la forma de regresar errores en fiber
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Review your input"})
	}

        //Convertir ID a int
        IntID, err := strconv.Atoi(ID)

	if err != nil {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error converting in Integer", "data": err.Error()})
	}
        //Obtener datos del Juego
	exitoso, err := j.BaseDatos.EliminarJuego(ID)

	if err != nil || !exitoso {
		return c.Status(fiber.ErrBadRequest.Code).JSON(fiber.Map{"status": "error", "message": "Error in DB", "data": err.Error()})
	}

        //Regresar Estatus de aceptado, todo salio bien.
	return c.SendStatus(fiber.StatusAccepted)
}
```

Con esto terminamos con el handler de nuestro CRUD, Lo unico que falta es configurar el servidor con los metodos necesarios.

## Configurar el Servidor Fiber

En nuestro `main.go` deberiamos tener algo como esto:
```go
func main() {
    //Crear nuestra aplicación de Fiber
    app := fiber.New()
    
    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World 👋!")
    })
  
    log.Fatal(app.Listen(":3000"))
}
```
Para poder configurar con nuestros handlers, tendremos que agregar esto a nuestros `import`
```go
import (
      "log"

      "github.com/gofiber/fiber/v2"

      "gamelist/handlers"
)
```

Ahora crearemos un nuevo `Handler` y agregaremos las rutas de nuestro CRUD:

```go
func main() {
    //Crear nuestra aplicación de Fiber
    app := fiber.New()

    nuevoHandler := handlers.NuevoJuegosHandler()

    app.Get("/:id", nuevoHandler.ObtenerJuego)
    app.Get("/", nuevoHandler.ObtenerTodosJuegos)
    app.Post("/", nuevoHandler.CrearJuego)
    app.Put("/", nuevoHandler.ModificarJuego)
    app.Delete("/:id", nuevoHandler.EliminarJuego)
  
    log.Fatal(app.Listen(":3000"))
}
```

### ¡Listo! ¡Terminamos nuestro Proyecto! 🤩

Eso es todo en este tutorial, Los siguientes pasos serian agregar `middlewares` a nuestro proyecto y seguridad con `JWT`.

Gracias por leer, cualquier cosa no dudes en preguntar.