sexta-feira, 15 de março de 2024

Criando uma API em GO com Swagger e MySQL para uma aplicação de TO-DO

Neste tutorial, vamos criar uma API em GO para uma aplicação de TO-DO, utilizando Swagger para documentação e MySQL como banco de dados. Vamos seguir a arquitetura hexagonal e orientação a objetos para manter o código organizado e fácil de manter. 

Passo 1: Configuração do ambiente


Certifique-se de ter o GO instalado em seu sistema. Você também precisará do MySQL instalado e configurado, você pode usar o post "Desenvolvendo uma API TO-DO com Nest.js, Swagger, MySQL e Docker" para usar o mesmo docker do MySQL e estrutura de tabela.

Passo 2: Crie o banco de dados e a tabela


Caso não esteja usando o mesmo docker para o MySQL conforme mencionado no passo anterior, vamos criar o banco de dados e a tabela necessária para nossa aplicação de TO-DO. Execute o seguinte script SQL:

CREATE DATABASE IF NOT EXISTS todo_db;
USE todo_db;

CREATE TABLE IF NOT EXISTS tasks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT
);

Passo 3: Crie a estrutura do projeto


Crie a estrutura do projeto da seguinte forma:

go-todo-api/
├── docs/
├── src/
│   ├── handlers/
│   │   └── todo_handler.go
│   ├── models/
│   │   └── todo.go
│   ├── repositories/
│   │   └── todo_repository.go
│   ├── router/
│   │   └── router.go
│   └── config/
│       ├── db.go
│       └── swagger.go
├── main.mod
└── go.mod

Gere o arquivo go.mod executando o comando:

go init go-todo-api

Passo 4: Instale as dependências


Vamos precisar do pacote gorilla/mux para lidar com as rotas da API, swaggo/http-swagger para a documentação com Swagger e go-sql-driver/mysql para o acesso ao MySQL. Instale-os usando o seguinte comando:

go get -u github.com/gorilla/mux
go get -u github.com/swaggo/http-swagger/v2
go get -u github.com/go-sql-driver/mysql

Passo 5: Implemente a lógica da aplicação


Vamos implementar a lógica da aplicação seguindo a arquitetura hexagonal. Aqui está um exemplo básico de como seria a estrutura do código:

models/todo.go

package models

type Task struct {
    ID          int    `json:"id"`
    Title       string `json:"title"`
    Description string `json:"description"`
}

repositories/todo_repository.go

package repositories

import (
    "database/sql"
    "log"

    "go-todo-api/src/models"
)

type TodoRepository struct {
    db *sql.DB
}

func NewTodoRepository(db *sql.DB) TodoRepository {
    return TodoRepository{
        db: db,
    }
}

func (r *TodoRepository) Create(task *models.Task) error {
    query := "INSERT INTO tasks (title, description) VALUES (?, ?)"
    _, err := r.db.Exec(query, task.Title, task.Description)
    if err != nil {
        log.Printf("Erro ao inserir tarefa no banco de dados: %v", err)
        return err
    }
    return nil
}

func (r *TodoRepository) Update(id int, task *models.Task) error {
    query := "UPDATE tasks SET title = ?, description = ? WHERE id = ?"
    _, err := r.db.Exec(query, task.Title, task.Description, id)
    if err != nil {
        log.Printf("Erro ao atualizar tarefa no banco de dados: %v", err)
        return err
    }
    return nil
}

func (r *TodoRepository) Delete(id int) error {
    query := "DELETE FROM tasks WHERE id = ?"
    _, err := r.db.Exec(query, id)
    if err != nil {
        log.Printf("Erro ao excluir tarefa no banco de dados: %v", err)
        return err
    }
    return nil
}

func (r *TodoRepository) GetByID(id int) (*models.Task, error) {
    query := "SELECT id, title, description FROM tasks WHERE id = ?"
    row := r.db.QueryRow(query, id)

    var task models.Task
    err := row.Scan(&task.ID, &task.Title, &task.Description)
    if err != nil {
        log.Printf("Erro ao obter tarefa do banco de dados: %v", err)
        return nil, err
    }
    return &task, nil
}

func (r *TodoRepository) GetAll() ([]*models.Task, error) {
    query := "SELECT id, title, description FROM tasks"
    rows, err := r.db.Query(query)
    if err != nil {
        log.Printf("Erro ao obter tarefas do banco de dados: %v", err)
        return nil, err
    }
    defer rows.Close()

    var tasks []*models.Task
    for rows.Next() {
        var task models.Task
        err := rows.Scan(&task.ID, &task.Title, &task.Description)
        if err != nil {
            log.Printf("Erro ao ler linha de tarefa do banco de dados: %v", err)
            return nil, err
        }
        tasks = append(tasks, &task)
    }

    return tasks, nil
}


handlers/todo_handler.go

package handlers

import (
	"encoding/json"
	"net/http"
	"strconv"

	"github.com/gorilla/mux"
	"go-todo-api/src/models"
	"go-todo-api/src/repositories"
)

type TodoHandler struct {
	Repository repositories.TodoRepository
}

// @Summary      Create a task
// @Description  create a task
// @Tags         tasks
// @Accept       json
// @Produce      json
// @Param        task   body   models.Task true  "Task"
// @Success      201  {object}  models.Task
// @Failure      400  {object}  string
// @Failure      500  {object}  string
// @Router       /tasks [post]
func (h *TodoHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
	var task models.Task
	err := json.NewDecoder(r.Body).Decode(&task)
	if err != nil {
		http.Error(w, "Erro ao decodificar JSON", http.StatusBadRequest)
		return
	}

	err = h.Repository.Create(&task)
	if err != nil {
		http.Error(w, "Erro ao criar tarefa", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(task)
}

// @Summary      Update a task
// @Description  update a task
// @Tags         tasks
// @Accept       json
// @Produce      json
// @Param        id   path      int  true  "Task ID"
// @Param        task   body   models.Task true  "Task"
// @Success      200  {object}  models.Task
// @Failure      400  {object}  string
// @Failure      500  {object}  string
// @Router       /tasks/{id} [put]
func (h *TodoHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.Atoi(vars["id"])
	if err != nil {
		http.Error(w, "ID inválido", http.StatusBadRequest)
		return
	}

	var task models.Task
	err = json.NewDecoder(r.Body).Decode(&task)
	if err != nil {
		http.Error(w, "Erro ao decodificar JSON", http.StatusBadRequest)
		return
	}

	err = h.Repository.Update(id, &task)
	if err != nil {
		http.Error(w, "Erro ao atualizar tarefa", http.StatusInternalServerError)
		return
	}

	json.NewEncoder(w).Encode(task)
}

// @Summary      Delete a task
// @Description  delete a task
// @Tags         tasks
// @Accept       json
// @Produce      json
// @Param        id   path      int  true  "Task ID"
// @Success      204
// @Failure      400  {object}  string
// @Failure      500  {object}  string
// @Router       /tasks/{id} [delete]
func (h *TodoHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.Atoi(vars["id"])
	if err != nil {
		http.Error(w, "ID inválido", http.StatusBadRequest)
		return
	}

	err = h.Repository.Delete(id)
	if err != nil {
		http.Error(w, "Erro ao excluir tarefa", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// @Summary      Show a task
// @Description  get task by ID
// @Tags         tasks
// @Accept       json
// @Produce      json
// @Param        id   path      int  true  "Task ID"
// @Success      200  {object}  models.Task
// @Failure      404  {object}  string
// @Router       /tasks/{id} [get]
func (h *TodoHandler) GetTaskByID(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.Atoi(vars["id"])
	if err != nil {
		http.Error(w, "ID inválido", http.StatusBadRequest)
		return
	}

	task, err := h.Repository.GetByID(id)
	if err != nil {
		http.Error(w, "Tarefa não encontrada", http.StatusNotFound)
		return
	}

	json.NewEncoder(w).Encode(task)
}

// @Summary 	 Get Task List
// @Description  get all tasks
// @Tags         tasks
// @Accept   	 json
// @Produce  	 json
// @Success      200  {object}  []models.Task
// @Failure      500  {object}  string
// @Router  	/tasks [get]
func (h *TodoHandler) GetAllTasks(w http.ResponseWriter, r *http.Request) {
	tasks, err := h.Repository.GetAll()
	if err != nil {
		http.Error(w, "Erro ao obter tarefas", http.StatusInternalServerError)
		return
	}

	json.NewEncoder(w).Encode(tasks)
}


router/router.go

package router

import (
    "github.com/gorilla/mux"
    "go-todo-api/src/handlers"
)

func NewRouter(handler *handlers.TodoHandler) *mux.Router {
    r := mux.NewRouter()
    s := r.PathPrefix("/todo/v1").Subrouter()
    s.HandleFunc("/tasks", handler.CreateTask).Methods("POST")
    s.HandleFunc("/tasks/{id}", handler.UpdateTask).Methods("PUT")
    s.HandleFunc("/tasks/{id}", handler.DeleteTask).Methods("DELETE")
    s.HandleFunc("/tasks/{id}", handler.GetTaskByID).Methods("GET")
    s.HandleFunc("/tasks", handler.GetAllTasks).Methods("GET")
    return s
}

A implementação da API está pronta, agora vamos as configurações para podermos gerar o Swagger, conexão com o banco de dados e execução da API

Passo 6: Configuração da conexão com o banco de dados

config/db.go

package config

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/go-sql-driver/mysql"
)

var DB *sql.DB

func InitDB() {
    dataSourceName := "seu_user:sua_senha@tcp(localhost:3306)/todo_db"
    var err error
    DB, err = sql.Open("mysql", dataSourceName)
    if err != nil {
        log.Fatalf("Erro ao conectar ao banco de dados: %v", err)
    }

    err = DB.Ping()
    if err != nil {
        log.Fatalf("Erro ao pingar o banco de dados: %v", err)
    }

    fmt.Println("Conexão com o banco de dados estabelecida com sucesso")
}

func CloseDB() {
    err := DB.Close()
    if err != nil {
        log.Fatalf("Erro ao fechar conexão com o banco de dados: %v", err)
    }

    fmt.Println("Conexão com o banco de dados fechada com sucesso")
}

Passo 7: Configuração do Swagger


config/swagger.go

package config

import (
    "net/http"

    httpSwagger "github.com/swaggo/http-swagger/v2"
	_ "go-todo-api/docs"
)

func SetupSwagger() http.Handler {
    return httpSwagger.Handler(
        httpSwagger.URL("http://localhost:8080/todo/v1/swagger/doc.json"), // URL para acessar a documentação do Swagger
    )
}

Passo 8: Implementação da função main


Implemente a função main no arquivo main.go:

package main

import (
    "log"
    "net/http"

	db "go-todo-api/src/config"
	swagger "go-todo-api/src/config"
    "go-todo-api/src/handlers"
    "go-todo-api/src/repositories"
    "go-todo-api/src/router"

)

// @title           TODO API
// @version         1.0
// @description     This is a sample todo api.
// @termsOfService  http://swagger.io/terms/

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html

// @host      localhost:8080
// @BasePath  /todo/v1

// @externalDocs.description  OpenAPI
// @externalDocs.url          https://swagger.io/resources/open-api/
func main() {
	// Inicializa conexão com o banco de dados
    db.InitDB()
    defer db.CloseDB()

    // Configuração do repositório
    repo := repositories.NewTodoRepository(db.DB)

    // Configuração do handler
    handler := &handlers.TodoHandler{
        Repository: repo,
    }

    // Configuração do roteamento
    r := router.NewRouter(handler)

    // Configuração do Swagger
	r.PathPrefix("/swagger/").Handler(swagger.SetupSwagger()).Methods(http.MethodGet)


    // Inicialização do servidor
    log.Println("Servidor rodando em http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Passo 9: Gerando a documentação da API


As documentações das rotas se encotram no handler e a documentação da API está no main, vamos executar o comando abaixo para gerar os arquivos na pasta docs:
Instalação do cmd swag

go install github.com/swaggo/swag/cmd/swag@latest

Geração da documentação da API

swag init

Passo 10: Executando a aplicação


Para executar a aplicação, basta rodar o seguinte comando abaixo:

go run main.go


Acesse o Swagger em http://localhost:8080/todo/v1/swagger/index.html para visualizar e testar as operações CRUD da sua API TO-DO.

Agora você tem uma API em GO para uma aplicação de TO-DO, com Swagger para documentação e MySQL como banco de dados. Você pode usar esse exemplo como base e expandir conforme necessário para atender às suas necessidades.

O projeto deste post está no meu github link.