APIs en GO

Iván Sarpi (Zhad)
lemontech-engineering
10 min readOct 10, 2020

Hacía rato quería escribir alguna guía para aprender a hacer servicios en GO, y después de muchas vueltas, creo que llegó el momento.

Así que primero lo primero, instalemos go, lo podemos hacer desde la página oficial pero prefiero siempre usar brew.

brew install go
go version

Existen managers de versiones de Go como GVM, pero el lenguaje ya tiene eso incorporado, así que da igual.

Pensemos en una problemática que nos gustaría resolver, de igual manera comenzaremos preparando las bases del servicio que serán bastante comunes para otras ideas, pero siempre es mejor tener claridad del objetivo final. Así con eso resuelto, creo que diseñaré un servicio para enviar correos. Quienes me conocen sabrán que lo más importante es bautizar el servicio.

mkdir don-carter
cd don-carter
go mod init lemontech.com/don-carter

Con esto preparamos un módulo vacío en GO, ahora vamos con la estructura de carpetas. Haré una mezcla entre convenciones del lenguaje y algunas propuestas de Uncle Bob.

.
├── cmd
│ └── don-carter
│ └── main.go
├── domain
├── drivers
├── usecases
├── go.mod
  • cmd: aquí estarán los scripts de inicio del software. Si este mismo repo tuviera múltiples aplicaciones, aquí es donde se definiría cada una.
  • domain: definición de entidades que componen el dominio.
  • usecases: aplicación de las reglas de negocio.
  • drivers: acceso a librerías de soporte, dbs, apis, etc.

y el archivo main.go será solo un hola mundo por ahora.

Por simplicidad, crearé ahora 2 archivos vacíos que necesitaré mas adelante:

touch go.sum
touch .env

Ahora tengo que preparar el ambiente de desarrollo, acostumbro a trabajar todo dockerizado, pero criterio de cada uno :)

├── docker
│ └── dev
│ └── Dockerfile

y el archivo

Sólo falta un Makefile en la raíz del proyecto para ordenar mis scripts.

Y ahora sí, vamos.

make dev-build
make dev-run

Si hicimos todo bien veremos

[gin] Listening on port 3000
[gin] Building...
[gin] Build finished
Hey you

Felicitaciones!! en este punto ya podemos abrir la primera cerveza antes de continuar.

Por dónde comenzamos? Alguien podría decidir primero definir el dominio, otro escribir los casos de uso para acotar el alcance del microservicio. Pero si están comenzando con un lenguaje, les recomiendo ir por donde hay mas incertidumbre. En nuestro caso, queremos un api rest, que envíe correos, y que probablemente lleve el tracking de éstos. Y estas 3 características podrían considerarse de alta incertidumbre.

  • API: Sabemos que framework nos entrega toda la funcionalidad que requeriremos?
  • Mails: Con qué herramienta queremos enviar los correos? existe una librería para GO?
  • DB: Qué database queremos utilizar? y existe la librería en GO? redundante, lo sé, pero cuando aprendí Go o no existían librerías oficiales o estaban en beta.

Para mi caso iré primero a solucionar la parte del mail, y lo haré con sendgrid, tiene un plan gratuito y la librería se ve con buena actividad.

https://github.com/sendgrid/sendgrid-go

Usando el ejemplo de emails dinámicos diseñaré mi propia clase mínima para enviar correos por ahora estáticos.

Para importar fácilmente una nueva librería, simplemente la agrego en la lista de imports como indica la documentación y utilizo una función cualquiera, luego de guardar ejecuto go mod tidy y listo!

├── drivers
│ └── sendgrid
│ └── sendgrid.go

Ahora en main.go podemos llamar a sendgrid.Send() y veremos nuestro primer mail enviado exitosamente.

Al ejecutar

Hey you
202
map[Access-Control-Allow-Headers:[Authorization, Content-Type, On-behalf-of, x-sg-elas-acl] Access-Control-Allow-Methods:[POST] Access-Control-Allow-Origin:[https://sendgrid.api-docs.io] Access-Control-Max-Age:[600] Connection:[keep-alive] Content-Length:[0] Date:[Fri, 11 Sep 2020 19:38:45 GMT] Server:[nginx] X-Message-Id:[######################] X-No-Cors-Reason:[https://sendgrid.com/docs/Classroom/Basics/API/cors.html]]

Y podremos ver en nuestra bandeja de entrada nuestro glorioso primer correo, esto ya amerita un par de cervezas más! pero aún queda camino.

Para seguir avanzando, podría añadir customizaciones al envío de correos , pero para facilitarme el trabajo, antes agregaré una librería para crear el API, y mi favorita es Gin-Gonic

https://github.com/gin-gonic/gin

Al igual que antes, basándome en algún ejemplo de Gin escribiré una implementación mínima para comenzar.

├── drivers
│ ├── sendgrid
│ │ └── sendgrid.go
│ └── server
│ └── server.go

Y cambiaré main.go para esta vez preparar y correr el servidor

Si todo sale bien veremos esto por terminal

Hey you
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /health --> lemontech.com/don-carter/drivers/server.Setup.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :3000
[GIN] 2020/09/15 - 02:44:58 | 200 | 113.3µs | 172.17.0.1 | GET "/health"

Ignoremos los warning por ahora, gin indica que en el puerto 3000 esta corriendo un servidor con la ruta GET /health

Puedo pegarle a esa ruta para obtener el resultado que seteamos

% curl -X GET localhost:3000/health | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 13 100 13 0 0 812 0 --:--:-- --:--:-- --:--:-- 812
{
"hey": "you"
}

Y en la terminal de gin

[GIN] 2020/09/15 - 02:50:11 | 200 |      1.3974ms |      172.17.0.1 | GET      "/health"

Bien! ya sabemos como enviar correos y como levantar un servidor, conectar un endpoint del server con la acción de enviar el correo parece trivial, pero si queremos una aplicación mejor diseñada, nos daremos una vuelta un poco mas larga que nos ayudará a aislar los distintos componentes y a futuro nos podría permitir cambiar la implementación del server, del envío de correo, o derechamente no usar API en favor de otro tipo de comunicación sin mayores complicaciones :)

No me he olvidado de la DB! pero haremos esa implementación una vez ordenemos un poco la cosa.

Ahora sí, llegó el momento de definir el dominio. Por ahora nuestra única entidad conocida es la de un correo, así que comencemos con una definición básica, no incluiré adjuntos por ahora. Podemos apoyarnos con la propia documentación de sendgrid si no tenemos una definición propia del dominio.

├── domain
│ └── email.go

En mi caso definí algo bastante simple de entender, tal vez con la excepción de los últimos atributos, donde Tags me servirá para categorizar los correos por app y tipo de correo, Params serán los parámetros que reemplazaré en el template y Meta permitirá la recepción de cualquier atributo extra para indexar y poder realizar búsquedas custom en la DB, recordemos que un requerimiento es poder trackear el estado de los correos, así que poder buscar por campos custom será de utilidad para quien use el servicio.

Con el dominio básico ya definido, puedo proceder a escribir mi primer caso de uso, el envío del correo. Primero haré un draft definiendo todo lo que deberíamos hacer en este caso de uso, y luego iré implementando los TODO.

└── usecases
└── send.go

De todas las acciones que quiero realizar en este caso de uso sólo sabemos cómo enviar el correo por sendgrid, así que hagamos eso! pero, como dije antes, no será tan directo, siguiendo el diagrama de Uncle Bob, desde nuestro caso de uso podemos conocer el dominio, pero no los drivers o capas externas.

Así que primero haremos una interfaz por la cual llamaremos el envío de sendgrid sin que el caso de uso se entere de la implementación.

Recordemos que nuestra implementación de sendgrid hasta el momento es bastante básica y no recibe ni devuelve datos, por lo que este es un buen momento para definir una mejor interfaz, y luego actualizaremos el driver para que cumpla la nueva definición.

└── usecases
├── bridges.go
└── send.go

Con esto estoy indicando que existe una variable mailer que satisface la interfaz emailBridge y por lo tanto tendrá un método Send y ahora podemos invocar mailer.Send desde nuestro caso de uso, lo que producirá un espectacular error si intentamos ejecutar sin antes inicializar mailer!

Vamos ahora con las mejoras en sendgrid.go para satisfacer esta nueva firma de Send y encapsulemos la llamada para transformar una función en método, esto significa que no podremos llamar Send directamente desde la librería, sino que necesitaremos una variable como intermediaria.

Los únicos cambios fueron la creación de la struct Handler, la firma de Send, y el retorno de datos, para que en caso que no existan errores http, pero sendgrid de igual manera nos devuelva un error, lo escalemos como error hacia afuera.

Y ahora desde main.go haremos la implementación y así será agnóstico para el caso de uso.

Luego podemos activar el uso de la interface desde el caso de uso.

Y como toque final para probar, llamaremos el caso de uso desde un nuevo endpoint del api rest.

├── drivers
│ ├── sendgrid
│ │ └── sendgrid.go
│ └── server
│ ├── controllers.go
│ └── server.go

Al ejecutar el POST veremos un track_id que aún no implementamos, pero también un flamante nuevo correo en nuestra bandeja!

% curl -X POST localhost:3000/email | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 15 100 15 0 0 7 0 0:00:02 0:00:01 0:00:01 7
{
"track_id": ""
}

Un pequeño repaso de cómo se ve nuestra estructura de archivos hasta ahora.

.
├── cmd
│ └── don-carter
│ └── main.go
├── docker
│ └── dev
│ └── Dockerfile
├── domain
│ └── email.go
├── drivers
│ ├── sendgrid
│ │ └── sendgrid.go
│ └── server
│ ├── controllers.go
│ └── server.go
├── usecases
│ ├── bridges.go
│ └── send.go
├── .env
├── Makefile
├── go.mod
└── go.sum
9 directories, 12 files

Para terminar la integración del envío de correos con sendgrid, antes de pasar a db y otras cosas, vamos por fin a enviar un payload al endpoint, validar los datos y enviar un correo dinámico!

Definamos un payload basado en el domain que haga uso de todo lo que queremos exponer en esta versión de la api.

{
"from": {
"alias": "Softllama",
"email": "hey@softllama.com"
},
"tos": [{
"alias": "Ivan Sarpi",
"email": "###########@lemontech.com"
}],
"ccs": [{
"alias": "Ivan Sarpi",
"email": "###########@gmail.com"
}],
"template": "d-4f74e70e7def4e6baf4da2db73adeece",
"params": {
"subject": "Hey you!",
"name": "Ivan"
},
"meta": {
"tenant": "softllama",
"user": "isarpi"
},
"tags": ["reporte-x"]
}

Y ahora para parsear este body en gin solo debemos cambiar

em := domain.Email{}
id, status, err := usecases.Send(em)

por:

var em domain.Email
c.ShouldBindJSON(&em)
id, status, err := usecases.Send(em)

Con esto le estamos diciendo a gin que el payload recibido sea cargado sobre una struct del tipo domain.Email, por eso fue que en la definición de ésta entidad agregamos los tags json.

Podemos agregar unos Println para ver como quedaron los datos cargados sobre em, y luego probar el endpoint. Como ahora estamos enviando data dejaré de lado curl, y para hacerlo mas sencillo me crearé un archivo .rest que luego ejecutaré gracias al plugin rest de vscode.

GET http://localhost:3000/health

###

POST http://localhost:3000/email
content-type: application/json

{
"from": {
"alias": "Softllama",
"email": "hey@softllama.com"
},
"tos": [{
"alias": "Ivan Sarpi",
"email": "###########@lemontech.com"
}],
"ccs": [{
"alias": "Ivan Sarpi",
"email": "###########@gmail.com"
}],
"template": "d-4f74e70e7def4e6baf4da2db73adeece",
"params": {
"subject": "Hey you!",
"name": "Ivan",
},
"meta": {
"tenant": "softllama",
"user": "isarpi"
},
"tags": ["reporte-x"]
}

Volvamos a revisar nuestro checklist de pendientes en el caso de uso

// TODO: generate unique id// TODO: persist request to db// TODO: validate attributes of em// TODO: send it through sendgridstatus, err = mailer.Send(em)fmt.Println(status, err)// TODO: update status on db// TODO: return tracking id or error

Antes de avanzar a terminar de generar los parámetros dinámicos en la implementación de sendgrid podemos resolver todos los TODO no relacionados a db.

Primero, la generación de un ID único, utilizaré una librería de google.

https://github.com/google/uuid

Y rápidamente haré la implementación con interfaces.

├── drivers
│ ├── sendgrid
│ │ └── sendgrid.go
│ ├── server
│ │ ├── controllers.go
│ │ └── server.go
│ └── uuid
│ └── uuid.go

Si volvemos a enviar un correo veremos que ya nos retorna un id de trackeo

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Sep 2020 16:10:06 GMT
Content-Length: 51
Connection: close
{
"track_id": "54a2860d-00e5-4dd1-841d-4e5b9cca7451"
}

Pasemos a validar los atributos del correo, esto podría ser parte del mismo dominio, así que agregaré un método a la struct. Podría escribir mis propias validaciones, lo que sería ideal para estructuras sencillas, pero ya que gin utiliza una librería yo utilizaré la misma.

https://github.com/go-playground/validator

Esta librería me permite agregar nuevos tags a la struct y al pasarle una instancia a validator verificará que los valores cumplan estas reglas. Como ejemplo, agreguemos una validación al ID.

Primero extenderé el domain para agregar la librería de validaciones.

├── domain
│ ├── domain.go
│ └── email.go

Y ahora escribiré el método de la struct.

Finalmente llamaré el método validate desde el usecase y probaré llenando el campo ID, y luego comentando esa línea.

Al grabar el ID

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Sep 2020 23:59:05 GMT
Content-Length: 51
Connection: close

{
"track_id": "acf669b6-94d4-4d6a-b057-402a15f72b27"
}

Con la línea 10 comentada

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Sep 2020 23:59:37 GMT
Content-Length: 135
Connection: close

{
"error": "Key: 'Email.ID' Error:Field validation for 'ID' failed on the 'uuid4' tag",
"track_id": "93471719-3634-479a-b333-1dfeb600674b"
}

Funciona impecable para lo que necesitamos, ahora solo debemos agregar el resto de validaciones.

Con los datos ya validados, vamos a agregarlos al mapeo dinámico en la implementación de sendgrid.

Y ahora sí, por fin, ya podemos enviar correos con datos dinámicos. Solo nos está faltando la persistencia en DB.

Haré una implementación sencilla con mongoDB utilizando el driver oficial.

Primero actualizaré el dominio para registrar el status del correo.

Ahora voy por la interface.

Con esta definición ya puedo ver cómo quedará mi caso de uso.

Y bueno, solo nos queda implementar la librería de mongo, lo haré en 2 archivos, uno para gestionar la conexión y otro para los métodos de la collection.

https://github.com/mongodb/mongo-go-driver

Creo la estructura de carpetas.

├── drivers
│ ├── mongodb
│ │ ├── history.go
│ │ └── mongodb.go
│ ├── sendgrid
│ │ └── sendgrid.go
│ ├── server
│ │ ├── controllers.go
│ │ └── server.go
│ └── uuid
│ └── uuid.go

Y a escribir código!

Antes de celebrar, sólo nos va quedando iniciar el bridge con nuestra implementación de mongo.

Levantar una MongoDB

mongo:
docker run --rm -it \
-p 27017:27017 \
--name mongodb \
mongo:4

Agregar las correspondientes env

DBURL=mongodb://host.docker.internal:27017/don-carter
DBNAME=don-carter
HISTORY_TABLE=history

Y ahora sí! a ejecutar!

Mientras celebras que todo se ejecuta a la perfección, el correo llega, tienes tu ID de trackeo y lo puedes ver en la DB, yo me castigo por tomar tanto tiempo y no incluir todo lo que quería hacer. Tal vez algún día continue agregando algunas cosas nuevas :)

Muestro por última vez mi estructura de archivos ya final.

.
├── cmd
│ └── don-carter
│ └── main.go
├── docker
│ └── dev
│ └── Dockerfile
├── domain
│ ├── domain.go
│ └── email.go
├── drivers
│ ├── mongodb
│ │ ├── history.go
│ │ └── mongodb.go
│ ├── sendgrid
│ │ └── sendgrid.go
│ ├── server
│ │ ├── controllers.go
│ │ └── server.go
│ └── uuid
│ └── uuid.go
├── usecases
│ ├── bridges.go
│ └── send.go
├── .env
├── Makefile
├── api.rest
├── go.mod
└── go.sum
11 directories, 17 files

Salud!

--

--