Golang 05 - Adicionando nossa API
Adicionando DB, e nossa API.
Adicionando Gin
A lib Gin é uma lib muito produtiva para escrever aplicações Web, e como ela tem uma escrita muito próxima ao Express do Node, me agrada muito, então será ela que iremos utilizar.
Primeiro rode:
go get github.com/gin-gonic/gin
Isso deve atualizar ambos seus go.mod
e go.sum
para incluir a lib gin
. Em seguida vamos fazer as adaptações necessárias para nossa aplicação.
Vamos para o render/page.go
, nesse arquivo nós vamos mudar principalmente a função Write, pois agora ao invés de usarmos o ResponseWriter
, vamos usar o render do Gin
, e também vamos remover o prefixo templates/
do nosso AsHome
, também porque o Gin
já lida com isso.
package render
import (
"net/http"
"github.com/gin-gonic/gin"
)
/* ... */
func (page *Page) AsHome() *Page {
return page.SetMeta(
"QR Code Generator",
"A page to generate QR",
"index.html",
http.StatusOK,
)
}
/* ... */
func (page *Page) Write(c *gin.Context) *Page {
c.HTML(
page.Status,
page.Template,
gin.H{
"Title": page.Title,
"Description": page.Description,
"Error": page.Error,
},
)
return page
}
No handlers/html.go
, nós vamos basicamente trocar o w http.ResponseWriter, r *http.Request
pelo c *gin.Context
, e perceba que mudamos um pouco a forma que pegamos o dado do form, e a forma que escrevemos enviamos o Writer:
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
render "github.com/joaomarcuslf/qr-generator/render"
generator "github.com/joaomarcuslf/qr-generator/services/generators"
)
func Home(c *gin.Context) {
render.NewPage().AsHome().Write(c)
}
func GenerateQr(c *gin.Context) {
qr := generator.NewQRCode()
err := qr.SetBarcode(c.PostForm("dataString")).ToPNG(c.Writer)
if err != nil {
render.NewPage().AsHome().SetError(err, http.StatusBadRequest).Write(c)
}
}
Já no server/http.go
, é onde teremos uma mudança maior, mas vamos por partes:
package server
import (
"github.com/gin-gonic/gin"
web "github.com/joaomarcuslf/qr-generator/handlers/web"
)
/* ... */
func (a *Server) Run() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.Static("/static", "./static")
router.GET("/", web.Home)
router.POST("/generator", web.GenerateQr)
router.Run(":" + s.Port)
}
Nós tiramos o IO, já que não é mais necessário.
router.LoadHTMLGlob("templates/*")
: Aqui nós estamos declarando qual nossa pasta de templates.router.Static("/static", "./static")
: Aqui nós estamos declarando qual nossa pasta de arquivos estáticos.router.GET("/", web.Home)
: Aqui nós estamos declarando o endpoint/
, que é o endpoint que vai ser chamado quando o usuário acessar a página inicial.router.POST("/generator", web.GenerateQr)
: Aqui nós estamos declarando o endpoint/generator
, que é o endpoint que vai ser chamado quando o usuário acessar a página de geração de QR.
E perceba que agora nós diferenciamos quando é um GET de um POST.
Com isso nós refatoramos nosso servidor para algo mais próximo de um servidor do mundo real.
A partir daqui, nós iremos criar um Banco de Dados dummy, para escrevermos uma API, então se torna 100% opicional seguir.
Dummy DB
Nós vamos implementar um Dummy DB em JSON, e vamos utilizar a Design Pattern Singleton, assim ao invés de abrirmos várias instâncias para ler o arquivo JSON, nós vamos reaproveitar a instância anterior.
mkdir services/readers
touch services/readers/json.go
Vamos preencher nosso services/readers/json.go
com os seguintes métodos:
package services
import (
"encoding/json"
"io/ioutil"
"os"
)
func Read(path string) ([]byte, error) {
jsonFile, err := os.Open(path)
if err != nil {
return nil, err
}
defer jsonFile.Close()
byteValue, err := ioutil.ReadAll(jsonFile)
return byteValue, err
}
func Save(path string, data interface{}) {
file, err := json.MarshalIndent(data, "", " ")
if err != nil {
panic(err)
}
err = ioutil.WriteFile(path, file, 0644)
if err != nil {
panic(err)
}
}
Agora vamos ver como vamos utilizar esses métodos no nosso Dummy DB, rode os seguintes comandos no seu terminal:
mkdir database
touch database/db.go
touch ./db.json
echo '{"Sites": []}' >> ./db.json
Vamos preencher nosso database/db.go
como se fosse um banco de dados local:
package database
import (
"encoding/json"
"fmt"
"math/rand"
json_reader "github.com/joaomarcuslf/qr-generator/services/readers"
)
type Site struct {
URL string
Id string
}
type DB struct {
Sites []Site
}
var instance *DB
var file string = "./db.json"
func NewDB() *DB {
if instance == nil {
byteValue, err := json_reader.Read(file)
if err != nil {
panic(err)
}
var db DB
json.Unmarshal(byteValue, &db)
instance = &db
}
return instance
}
func Close() {
fmt.Println("closing database")
if instance != nil {
fmt.Println("Creating database file")
json_reader.Save(file, instance)
}
}
func (db *DB) Save() {
if instance != nil {
json_reader.Save(file, instance)
}
}
func (db *DB) Add(url string) {
min := 10
max := 9999
id := fmt.Sprintf("%d", rand.Intn(max-min)+min)
db.Sites = append(db.Sites, Site{
URL: url,
Id: id,
})
db.Save()
}
func (db *DB) Get(id string) (Site, error) {
for _, site := range db.Sites {
if site.Id == id {
return site, nil
}
}
return Site{}, fmt.Errorf("no site found, with id: %s", id)
}
func (db *DB) GetAll() []Site {
return db.Sites
}
func (db *DB) Update(id, url string) {
for i, site := range db.Sites {
if site.Id == id {
db.Sites[i].URL = url
}
}
db.Save()
}
func (db *DB) Remove(id string) {
for i, site := range db.Sites {
if site.Id == id {
db.Sites = append(db.Sites[:i], db.Sites[i+1:]...)
return
}
}
db.Save()
}
Espero que compreenda, o objetivo desses posts não é aprofundar em certas implementações, por isso não irei abordar como funciona DB locais, DB gerais, e etc. Caso surja curiosidade tente analisar o código acima, e em caso de dúvidas você pode entrar em contato comigo.
Agora abra server/http.go
:
package server
import (
"github.com/gin-gonic/gin"
database "github.com/joaomarcuslf/qr-generator/database"
web "github.com/joaomarcuslf/qr-generator/handlers/web"
)
/* ... */
func (a *Server) Run() {
database.NewDB()
defer database.Close()
/* ... */
}
Agora que temos nosso DB rodando, vamos abrir o handlers/web/html.go
e vamos salvar executar um .Add
para salvar o site que foi enviado.
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
database "github.com/joaomarcuslf/qr-generator/database"
render "github.com/joaomarcuslf/qr-generator/render"
generator "github.com/joaomarcuslf/qr-generator/services/generators"
)
var db *database.DB = database.NewDB()
func Home(c *gin.Context) {
render.NewPage().AsHome().Write(c)
}
func GenerateQr(c *gin.Context) {
qr := generator.NewQRCode()
input := c.PostForm("dataString")
db.Add(input)
err := qr.SetBarcode(input).ToPNG(c.Writer)
if err != nil {
render.NewPage().AsHome().SetError(err, http.StatusBadRequest).Write(c)
}
}
Se você abrir sua aplicação agora, e gerar um QR code, você vai ver que o db.json
foi atualizado com novos valores. Com isso vamos criar uma mini-api que possa retornar todos os Sites, e permita edição e deletar.
Preenchendo nossa API
Vamos escrever uma API:
mkdir handlers/api
touch handlers/api/sites.go
Abra o handlers/api/sites.go
e vamos preencher com nossos métodos CRUD:
package handlers
import (
"github.com/gin-gonic/gin"
database "github.com/joaomarcuslf/qr-generator/database"
)
type SiteController struct {
db *database.DB
}
func NewSiteController() *SiteController {
return &SiteController{
db: database.NewDB(),
}
}
Primeiro método seria o GET /sites
, que é o método de retornar todos.
func (sc *SiteController) List(c *gin.Context) {
c.JSON(200, gin.H{
"sites": sc.db.GetAll(),
})
}
Segundo método seria o POST /sites
, que é o método de criar um novo site.
func (sc *SiteController) Create(c *gin.Context) {
site := &database.Site{}
c.BindJSON(site)
sc.db.Add(site.URL)
c.JSON(204, gin.H{})
}
Terceiro método seria o GET /sites/:id
, que é o método de retornar um site específico.
func (sc *SiteController) Show(c *gin.Context) {
id := c.Param("id")
site, err := sc.db.Get(id)
if err != nil {
c.JSON(404, gin.H{
"error": err.Error(),
})
return
}
c.JSON(200, gin.H{
"site": site,
})
}
Quarto método seria o PUT /sites/:id
, que é o método de atualizar um site específico.
func (sc *SiteController) Update(c *gin.Context) {
id := c.Param("id")
var site database.Site
c.BindJSON(&site)
sc.db.Update(id, site.URL)
c.JSON(204, gin.H{})
}
Quinto método seria o DELETE /sites/:id
, que é o método de deletar um site específico.
func (sc *SiteController) Delete(c *gin.Context) {
id := c.Param("id")
sc.db.Remove(id)
c.JSON(200, gin.H{
"message": "Site deleted",
})
}
Você que já conhece CRUD talvez estranhe a nomenclatura dos métodos que estou utilizando, eu estou me baseando na nomenclatura utilizada pelo Ruby on Rails na hora de definir os nomes do métodos, ela não é obrigatória, nem necessária, é mais uma questão de preferência. E vamos atualizar nosso server/http.go
para chamar nosso SiteController
:
func (a *Server) Run() {
/* ... */
sc := api.NewSiteController()
router.GET("/api/sites", sc.List)
router.POST("/api/sites", sc.Create)
router.GET("/api/sites/:id", sc.Show)
router.PUT("/api/sites/:id", sc.Update)
router.DELETE("/api/sites/:id", sc.Delete)
router.Run(":" + a.Port)
}
Caso você não esteja entendendo esses nomes, isso é um padrão REST de APIs, você pode ver um pouco mais sobre aqui
Com isso você tem ambos um WEB App, e uma API, na próxima aula nós vamos introduzir o React para consumir a API. Espero você na próxima aula.