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!
Indice
- Parte 1 - Preparación
- Parte 2 - Modelos
- Parte 3 - Base de datos
- Parte 4 - Manejo de peticiones
- Parte 5 - Middlewares y JWT
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
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"`
}
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
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
}
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
}
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
}
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
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"`
}
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
}
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
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=#
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';
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;
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
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
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
}
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
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)
}
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
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)
}
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,
}
}
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
}
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()
}
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.
Top comments (0)