DEV Community

Cover image for API Rest con Go (Golang) y PostgreSQL Parte 2
Orlando Monteverde
Orlando Monteverde

Posted on • Edited on

API Rest con Go (Golang) y PostgreSQL Parte 2

Requisitos:

  • Conocimientos básicos sobre el lenguaje go.
  • Conocimiento sobre HTTP y REST recomendable.
  • Go en su versión 1.11 o superior.
  • PostgreSQL en su versión 9.6 o superior.
  • Conocimientos básicos de SQL deseables.
  • Ganas de aprender.

Que vamos a desarrollar

Crearemos un API Rest para un microblog, estilo twitter en su versión mas básica. Con un modelo de usuarios y uno de publicaciones. Con un CRUD completo en cada caso, autenticación con JSON Web Tokens, creación, uso de middlewares y asegurar contraseña por con bcrypt. ¿Suena interesante? ¡Entonces vamos!

Meme

Indice

Esta es la segunda parte, te recomiendo comenzar por el articulo anterior.

Creando los modelos

User

Primero crearemos un directorio llamado user dentro del directorio pkg y, dentro de este nuevo directorio, crearemos un archivo llamado user.go.

mkdir pkg/user && touch pkg/user/user.go
Enter fullscreen mode Exit fullscreen mode

Vamos a crear la estructura User dentro de este archivo, que va a representar a los usuarios de nuestra API.

// pkg/user/user.go
package user

import "time"

// User of the system.
type User struct {
    ID           uint      `json:"id,omitempty"`
    FirstName    string    `json:"first_name,omitempty"`
    LastName     string    `json:"last_name,omitempty"`
    Username     string    `json:"username,omitempty"`
    Email        string    `json:"email,omitempty"`
    Picture      string    `json:"picture,omitempty"`
    Password     string    `json:"password,omitempty"`
    PasswordHash string    `json:"-"`
    CreatedAt    time.Time `json:"created_at,omitempty"`
    UpdatedAt    time.Time `json:"updated_at,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

Esta estructura contendrá la información de nuestro usuario, bastante sencillo, un identificador único para gestionarlo dentro de la base de datos, información personal, como nombre, apellido y correo electrónico, el Username, que utilizaremos para el login y la contraseña, el campo PasswordHash sera utilizado para almacenar el hash de la contraseña generado por bcrypt, cosa que haremos en el siguiente paso. También tenemos los campos CreatedAt y UpdatedAt de tipo ** time.Time** que utilizaremos para mantener un registro de la fecha de cuando fue creado el usuario y cuando fue su ultima modificación.

Si te resultan extrañas las anotaciones junto a los campos en la estructura, simplemente son una menera conveniente de proporcionar información adicional que, en este caso, usa el paquete encoding/json para procesar los campos, como nombrarlos en minúscula y usando notación snake case, que omita el campo si esta vació (omitempty), o evitar exponer un campo al convertir la estructura a formato JSON (json:"-"). Para mas información, les recomiendo consultar este articulo.

Ahora, agregaremos dos métodos a la estructura User para gestionar las contraseñas. Pero, primero debemos descargar el paquete bcrypt.

go get golang.org/x/crypto/bcrypt
Enter fullscreen mode Exit fullscreen mode

En primer lugar, una función para generar el hash a partir de la contraseña del usuario, para no almacenar esta directamente dentro de la base de datos. Esto se consigue facilmente gracias a la función GenerateFromPassword del paquete bcrypt que acabamos de instalar.

func (u *User) HashPassword() error {
    passwordHash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }

    u.PasswordHash = string(passwordHash)

    return nil
}
Enter fullscreen mode Exit fullscreen mode

De manera similar, crearemos un método que nos permita comparar el hash de la contraseña, almacenada en la propiedad PasswordHash, con la contraseña que recibe el método haciendo uso de la función CompareHashAndPassword del paquete bcrypt.

func (u User) PasswordMatch(password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))

    return err == nil
}
Enter fullscreen mode Exit fullscreen mode

Con esto ya hemos terminado con la estructura User. Ahora, dentro del mismo paquete user crearemos una interface que definirá las operaciones a realizar con el usuario en la base de datos. Esto puede realizarse en el mismo archivo user.go o en uno separado mientras comparta el mismo paquete, en este caso lo crearemos en un archivo diferente, ya que lo considero mas ordenado de esa forma, pero es solo una preferencia personal.

// pkg/user/repository.go
package user

import "context"

// Repository handle the CRUD operations with Users.
type Repository interface {
    GetAll(ctx context.Context) ([]User, error)
    GetOne(ctx context.Context, id uint) (User, error)
    GetByUsername(ctx context.Context, username string) (User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, id uint, user User) error
    Delete(ctx context.Context, id uint) error
}
Enter fullscreen mode Exit fullscreen mode

Esto quizás no parezca tener demasiado sentido en este punto, pero facilita mucho operaciones como testing, ademas de que nos permite separar la lógica de los handlers de la base de datos, por que podrías usar alguna otra, incluso si es NoSQL como MongoDB sin cambios importantes. Pronto estaré compartiendo contenido sobre Go y MongoDB 😉.

Si no están familiarizados con el paquete context les recomiendo leer esta interesante publicación del blog de go. De cualquier forma, este paquete nos da acceso al tipo de dato Context que nos permite definir tiempos limite, cancelar grupos de operaciones, pasar valores a través del contexto, entre otras cosas.

Post

Ahora crearemos un directorio llamado post dentro del directorio pkg. Y, dentro de este nuevo directorio crearemos un archivo llamado post.go.

mkdir pkg/post && touch pkg/post/post.go
Enter fullscreen mode Exit fullscreen mode

Y crearemos la estructura Post dentro de este archivo, que va a representar a las publicaciones de un usuario de nuestra API.

// pkg/post/post.go
package post

import "time"

// Post created by a user.
type Post struct {
    ID        uint      `json:"id,omitempty"`
    Body      string    `json:"body,omitempty"`
    UserID    uint      `json:"user_id,omitempty"`
    CreatedAt time.Time `json:"created_at,omitempty"`
    UpdatedAt time.Time `json:"updated_at,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

Bastante similar al caso anterior. Ahora crearemos también una interface Repository para esta estructura.

// pkg/post/repository.go
package post

import "context"

// Repository handle the CRUD operations with Posts.
type Repository interface {
    GetAll(ctx context.Context) ([]Post, error)
    GetOne(ctx context.Context, id uint) (Post, error)
    GetByUser(ctx context.Context, userID uint) ([]Post, error)
    Create(ctx context.Context, post *Post) error
    Update(ctx context.Context, id uint, post Post) error
    Delete(ctx context.Context, id uint) error
}
Enter fullscreen mode Exit fullscreen mode

Con esto ya tenemos suficiente para pasar a la siguiente fase, la base de datos.

Conexión con la base de datos.

Lo primero que debemos hacer es conectarnos a Postgres, pueden usar un cliente de terminal como psql que viene con la instalación de postgres, alguna interfaz gráfica como pgAdmin, lo que les resulte mejor, para el ejemplo usaremos psql.

psql -U postgres
Enter fullscreen mode Exit fullscreen mode

En mi caso tengo el usuario de postgres sin contraseña, porque es una instalación local que uso para pruebas. Pero es posible que les solicite contraseña para ingresar si establecieron una.

Ahora debemos estar viendo un prompt parecido a este

postgres=#
Enter fullscreen mode Exit fullscreen mode

En espera de comandos. Lo primero que haremos sera crear un nuevo usuario, esto no es obligatorio, perfectamente podríamos trabajar con el usuario postgres pero es mejor idea tener un usuario que solo tenga acceso a la base de datos del proyecto por seguridad.

postgres=# CREATE ROLE gopher WITH LOGIN PASSWORD 'gopher';
Enter fullscreen mode Exit fullscreen mode

En el comando de arriba estamos creando un nuevo usuario llamado gopher, le estamos dando permiso para hacer login y le asignamos la super segura contraseña gopher. Esto solo para efectos del ejemplo, recomiendo usar una contraseña un poco mas segura 😁.

Al presionar la tecla ENTER el comando se ejecutara y deberíamos tener una respuesta indicando: CREATE ROLE.

Buen trabajado, ahora crearemos la base de datos que vamos a utilizar en nuestra API.

postgres=# CREATE DATABASE microblog OWNER gopher;
Enter fullscreen mode Exit fullscreen mode

Si todo ha salido bien, tendríamos una respuesta similar a esta: CREATE DATABASE. Si desean confirmar que su base de datos fue creada exitosamente, puede listar las bases de datos con el comando \l y deberían obtener una respuesta como la siguiente.

 microblog         | gopher   | UTF8     | en_US.utf8 | en_US.utf8 | 
 postgres          | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 template0         | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
                   |          |          |            |            | postgres=CTc/postgres
 template1         | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
                   |          |          |            |            | postgres=CTc/postgres
Enter fullscreen mode Exit fullscreen mode

De momento hemos terminado y podemos desconectarnos de Postgres. Desde psql podemos hacerlo por medio del comando \q.

De regreso a nuestro proyecto, crearemos un nuevo directorio llamado data, dentro del directorio internal, este nuevo directorio debería quedar al mismo nivel que server.

mkdir internal/data && touch internal/data/data.go
Enter fullscreen mode Exit fullscreen mode

Y dentro de este nuevo directorio crearemos un archivo llamado data.go, que inicialmente contendrá el siguiente código.

package data

import (
    "database/sql"
    "sync"
)

var (
    data *Data
    once sync.Once
)

// Data manages the connection to the database.
type Data struct {
    DB *sql.DB
}
Enter fullscreen mode Exit fullscreen mode

Hecho eso, crearemos un nuevo archivo dentro del mismo paquete al que llamaremos postgres.go, aquí definiremos la conexión con la base de datos que creamos anteriormente. Antes de avanzar, vamos a descargar un nuevo paquete, en este caso un driver para postgres llamado pq

go get github.com/lib/pq
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a agregar algo de código a nuestro archivo postgres.go.

// internal/data/postgres.go
package data

import (
    "database/sql"

    // registering database driver
    _ "github.com/lib/pq"
)

func getConnection() (*sql.DB, error) {
    uri := "postgres://gopher:gopher@127.0.0.1:5432/microblog?sslmode=disable"
    return sql.Open("postgres", uri)
}
Enter fullscreen mode Exit fullscreen mode

El string que tenemos dentro de la variable uri nos permite conectarnos a la base de datos que creamos anteriormente. Indicando el nombre de usuario, su contraseña, la IP y puerto en el que esta corriendo postgres y el nombre de la base de datos a la que queremos conectarnos.

Sin embargo, similar al caso que del puerto, que tratamos en la publicación anterior, no es conveniente tener esta información directamente dentro del código, así que haremos un par de cambios para obtenera como variable de entorno.

Agregamos una nueva variable a nuestro archivo .env.

# .env
PORT=9000
DATABASE_URI=postgres://gopher:gopher@127.0.0.1:5432/microblog?sslmode=disable
Enter fullscreen mode Exit fullscreen mode

Y modificamos el archivo postgres.go para leer el valor de esta variable de entorno.

package data

import (
    "database/sql"
    "os"

    // registering database driver
    _ "github.com/lib/pq"
)

func getConnection() (*sql.DB, error) {
    uri := os.Getenv("DATABASE_URI")
    return sql.Open("postgres", uri)
}
Enter fullscreen mode Exit fullscreen mode

Ahora regresamos al archivo data.go para hacer algunos cambios. Primero, agregamos una función llamada initDB que creara una nueva conexión al base de datos gracias a la función getConnection que creamos anteriormente e inicializara el la variable data con un nuevo puntero a Data que contiene la conexión que acabamos de crear.

// internal/data/data.go
func initDB() {
    db, err := getConnection()
    if err != nil {
        log.Panic(err)
    }

    data = &Data{
        DB: db,
    }
}
Enter fullscreen mode Exit fullscreen mode

Una función llamada New con la que obtendremos un puntero a Data, para evitar crear una nueva conexión a la base de datos cada vez que llamamos a la función New utilizamos el tipo once del paquete sync que forma parte de la biblioteca estándar.

func New() *Data {
    once.Do(initDB)

    return data
}
Enter fullscreen mode Exit fullscreen mode

Y agregaremos una función llamada Close que usaremos para cerrar los recurso que hayamos abierto, en este caso, la conexión a la base de datos.

Ahora, para comprobar si la conexión se esta realizando correctamente, regresaremos al archivo main.go (cmd/microblog/main.go), y agregamos la llamada a la función New previamente creada y utilizamos la conexión para realizar un Ping a la base de datos y, finalmente, usamos la función Close para cerrar los recursos una vez que interrumpimos la ejecución del programa.

// cmd/microblog/main.go
package main

import (
    "os"
    "os/signal"

    "log"

    "github.com/orlmonteverde/go-postgres-microblog/internal/data"
    "github.com/orlmonteverde/go-postgres-microblog/internal/server"

    _ "github.com/joho/godotenv/autoload"
)

func main() {
    port := os.Getenv("PORT")
    serv, err := server.New(port)
    if err != nil {
        log.Fatal(err)
    }

    // connection to the database.
    d := data.New()
    if err := d.DB.Ping(); err != nil {
        log.Fatal(err)
    }

    // start the server.
    go serv.Start()

    // Wait for an in interrupt.
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    <-c

    // Attempt a graceful shutdown.
    serv.Close()
    data.Close()
}
Enter fullscreen mode Exit fullscreen mode

Por ahora lo dejaremos hasta aquí, en el siguiente articulo continuaremos con el desarrollo de nuestra API. Les comparto el repositorio con el código del proyecto hasta el momento.

Realmente espero que el contenido les sea de utilidad, y cualquier duda o sugerencia, por favor, déjenla en los comentarios y les responderé tan pronto como me resulte posible. Saludos y gracias por leer.

konosuba

Top comments (0)