Go: API REST con MySQL

En este artículo de programación en el lenguaje Go (también conocido como Golang) vamos a ver cómo crear una API REST que se comunica a través de JSON, guardando y mostrando los datos a partir de una base de datos de MySQL / MariaDB.

Al final vamos a tener una API REST con Go usando los 4 métodos: POST, PUT, DELETE y GET usando el enrutador Mux. Además vamos a implementar las 4 operaciones de una base de datos: insert, update, delete y select.

Te dejaré al final el código completo y además una explicación en mi canal de YouTube.

Base de datos

Vamos a consumir una base de datos de videojuegos. Por lo tanto recuerda crear una base de datos con el nombre de tu preferencia (yo la he llamado video_games) y colocar dentro la siguiente tabla:

CREATE TABLE video_games
(
    id    bigint unsigned not null primary key auto_increment,
    name  VARCHAR(255)    NOT NULL,
    genre VARCHAR(255)    NOT NULL,
    year  INTEGER         NOT NULL
);

Los datos que vamos a manejar son id como bigint que se convertirá en un int64 al mapearlo para Go. El nombre y género serán de tipo varchar, convertidos a string en Go. Y finalmente el año será un entero igualmente de tipo int64 para golang.

Configurando credenciales de acceso a base de datos

Lo primero que tienes que hacer es configurar las credenciales de acceso a la base de datos en el archivo llamado .env. Si no existe, debes crearlo basándote en el archivo .env.example. En mi caso se ve así:

user="root"
pass=""
host="localhost"
port="3306"
db_name="video_games"

Del archivo podemos notar varias cosas; todas relacionadas con las credenciales de acceso a la base de datos de MySQL.

En este caso mi usuario es root, no cuento con contraseña; el host es localhost en el puerto 3306 y el nombre de la base de datos es video_games (recuerda crearla desde la consola de MySQL o phpmyadmin).

El archivo de entorno se va a leer y cargar en las variables. En el archivo de variables además definimos el dominio permitido para CORS, pues de este modo podemos consumir esta API desde otro dominio:

package main

import (
	"fmt"
	"github.com/joho/godotenv"
	"os"
)

var _ = godotenv.Load(".env") // Cargar del archivo llamado ".env"
var (
	ConnectionString = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s",
		os.Getenv("user"),
		os.Getenv("pass"),
		os.Getenv("host"),
		os.Getenv("port"),
		os.Getenv("db_name"))
)

const AllowedCORSDomain = "http://localhost"

Como lo puedes notar, en la línea 10 estamos definiendo la cadena de conexión a la base de datos. Finalmente definimos la función que nos ayudará a conectar a MySQL desde Go:

package main

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

func getDB() (*sql.DB, error) {
	return sql.Open("mysql", ConnectionString)
}

Recuerda que anteriormente ya te mostré cómo conectar MySQL con Go solo que el ejemplo era por consola.

Structs y funciones útiles

En toda esta API que estamos creando vamos a manejar videojuegos para ejemplificar de una manera sencilla.

Para ello definimos el struct de Videojuego que tiene los mismos campos que definimos en la base de datos. Lo que está entre backticks es para indicar el nombre que tendrá la propiedad al leerse y decodificarse de JSON.

type VideoGame struct {
	Id    int64  `json:"id"`
	Name  string `json:"name"`
	Genre string `json:"genre"`
	Year  int64  `json:"year"`
}

Adicional al struct definimos algunos útiles, por ejemplo, una función para convertir de cadena a int64 que nos servirá para leer variables de la URL y convertirlas a entero.

package main

import "strconv"

func stringToInt64(s string) (int64, error) {
	numero, err := strconv.ParseInt(s, 0, 64)
	if err != nil {
		return 0, err
	}
	return numero, err
}

Controlador

No planeo utilizar ninguna metodología ni cosas de esas; pero he colocado todas las operaciones del modelo (el CRUD) en un mismo archivo. Queda así:

package main

func createVideoGame(videoGame VideoGame) error {
	bd, err := getDB()
	if err != nil {
		return err
	}
	_, err = bd.Exec("INSERT INTO video_games (name, genre, year) VALUES (?, ?, ?)", videoGame.Name, videoGame.Genre, videoGame.Year)
	return err
}

func deleteVideoGame(id int64) error {

	bd, err := getDB()
	if err != nil {
		return err
	}
	_, err = bd.Exec("DELETE FROM video_games WHERE id = ?", id)
	return err
}

// It takes the ID to make the update
func updateVideoGame(videoGame VideoGame) error {
	bd, err := getDB()
	if err != nil {
		return err
	}
	_, err = bd.Exec("UPDATE video_games SET name = ?, genre = ?, year = ? WHERE id = ?", videoGame.Name, videoGame.Genre, videoGame.Year, videoGame.Id)
	return err
}
func getVideoGames() ([]VideoGame, error) {
	//Declare an array because if there's error, we return it empty
	videoGames := []VideoGame{}
	bd, err := getDB()
	if err != nil {
		return videoGames, err
	}
	// Get rows so we can iterate them
	rows, err := bd.Query("SELECT id, name, genre, year FROM video_games")
	if err != nil {
		return videoGames, err
	}
	// Iterate rows...
	for rows.Next() {
		// In each step, scan one row
		var videoGame VideoGame
		err = rows.Scan(&videoGame.Id, &videoGame.Name, &videoGame.Genre, &videoGame.Year)
		if err != nil {
			return videoGames, err
		}
		// and append it to the array
		videoGames = append(videoGames, videoGame)
	}
	return videoGames, nil
}

func getVideoGameById(id int64) (VideoGame, error) {
	var videoGame VideoGame
	bd, err := getDB()
	if err != nil {
		return videoGame, err
	}
	row := bd.QueryRow("SELECT id, name, genre, year FROM video_games WHERE id = ?", id)
	err = row.Scan(&videoGame.Id, &videoGame.Name, &videoGame.Genre, &videoGame.Year)
	if err != nil {
		return videoGame, err
	}
	// Success!
	return videoGame, nil
}

Todas las funciones devuelven al menos un argumento que es de tipo error, pues así se puede manejar las posibles fallas que se encuentren en la ejecución. Además, estamos evitando inyecciones SQL pues si te fijas usamos placeholders en lugar de concatenar cadenas.

Por cierto, todas las funciones reciben o devuelven el tipo de dato VideoGame que vimos anteriormente. Las funciones son:

  • createVideoGame – Insertar un videojuego en la base de datos de MySQL
  • deleteVideoGame – Eliminar un videojuego a partir de su id usando Go
  • updateVideoGame – Actualizar un videojuego en la base de datos
  • getVideoGames – Obtener todos los videojuegos existentes como un arreglo de tipo VideoGame
  • getVideoGameById – Obtener un videojuego por id

Estas funciones serán expuestas con el enrutador, ahí es en donde vamos a conectar a MySQL con Go para exponer o traer los datos.

Definiendo rutas de la API con Go

Llegamos al punto en donde vamos a exponer nuestro controlador y base de datos a través de una API usando Go. Las definimos dentro de una función que recibe el enrutador, de este modo hacemos las cosas más simples y separamos conceptos.

package main

import (
	"encoding/json"
	"github.com/gorilla/mux"
	"net/http"
)

func setupRoutesForVideoGames(router *mux.Router) {
	// First enable CORS. If you don't need cors, comment the next line
	enableCORS(router)

	router.HandleFunc("/videogames", func(w http.ResponseWriter, r *http.Request) {
		videoGames, err := getVideoGames()
		if err == nil {
			respondWithSuccess(videoGames, w)
		} else {
			respondWithError(err, w)
		}
	}).Methods(http.MethodGet)
	router.HandleFunc("/videogame/{id}", func(w http.ResponseWriter, r *http.Request) {
		idAsString := mux.Vars(r)["id"]
		id, err := stringToInt64(idAsString)
		if err != nil {
			respondWithError(err, w)
			// We return, so we stop the function flow
			return
		}
		videogame, err := getVideoGameById(id)
		if err != nil {
			respondWithError(err, w)
		} else {
			respondWithSuccess(videogame, w)
		}
	}).Methods(http.MethodGet)

	router.HandleFunc("/videogame", func(w http.ResponseWriter, r *http.Request) {
		// Declare a var so we can decode json into it
		var videoGame VideoGame
		err := json.NewDecoder(r.Body).Decode(&videoGame)
		if err != nil {
			respondWithError(err, w)
		} else {
			err := createVideoGame(videoGame)
			if err != nil {
				respondWithError(err, w)
			} else {
				respondWithSuccess(true, w)
			}
		}
	}).Methods(http.MethodPost)

	router.HandleFunc("/videogame", func(w http.ResponseWriter, r *http.Request) {
		// Declare a var so we can decode json into it
		var videoGame VideoGame
		err := json.NewDecoder(r.Body).Decode(&videoGame)
		if err != nil {
			respondWithError(err, w)
		} else {
			err := updateVideoGame(videoGame)
			if err != nil {
				respondWithError(err, w)
			} else {
				respondWithSuccess(true, w)
			}
		}
	}).Methods(http.MethodPut)
	router.HandleFunc("/videogame/{id}", func(w http.ResponseWriter, r *http.Request) {
		idAsString := mux.Vars(r)["id"]
		id, err := stringToInt64(idAsString)
		if err != nil {
			respondWithError(err, w)
			// We return, so we stop the function flow
			return
		}
		err = deleteVideoGame(id)
		if err != nil {
			respondWithError(err, w)
		} else {
			respondWithSuccess(true, w)
		}
	}).Methods(http.MethodDelete)
}

Básicamente es configurar los endpoint de la API creada con Go, invocando a las funciones del controlador. Veamos a continuación las funciones enableCORS, respondWithSuccess y respondWithError.

Una cosa importante es la parte en donde se decodifica todo el cuerpo de la petición desde JSON al struct, lo cual puedes ver en la línea 40, leemos la petición desde r.Body y luego la decodificamos dentro de la variable creada anteriormente.

Hacemos lo mismo (decodificar JSON de la petición) para hacer el update, eso en la línea 56.

Middleware de CORS

CORS permite compartir recursos en dominios distintos. Es decir, permite que consumas localhost:8000 desde localhost:80 o similares; algo así como si fuera posible consumir facebook.com desde google.com

Si tú no usarás esta característica, simplemente comenta el código que la usa en el enrutador. Lo que vamos a hacer es agregar encabezados que van a permitir CORS para el dominio configurado previamente en las variables.

Para lograrlo, vamos a interceptar todas las peticiones para agregar estos encabezados.

func enableCORS(router *mux.Router) {
	router.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", AllowedCORSDomain)
	}).Methods(http.MethodOptions)
	router.Use(middlewareCors)
}
func middlewareCors(next http.Handler) http.Handler {
	return http.HandlerFunc(
		func(w http.ResponseWriter, req *http.Request) {
			// Just put some headers to allow CORS...
			w.Header().Set("Access-Control-Allow-Origin", AllowedCORSDomain)
			w.Header().Set("Access-Control-Allow-Credentials", "true")
			w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
			w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
			// and call next handler!
			next.ServeHTTP(w, req)
		})
}

Responder al cliente

También tenemos dos funciones que nos van a ayudar a responderle al cliente que consuma la API; una es para responder con éxito (código HTTP 200) y otra para responder con error (código 500).

La que responde con error recibe el error y lo muestra como cadena. La función que responde con éxito recibe un parámetro de tipo interface, que es algo así como un tipo de dato genérico en Go. Ambas funciones codifican la respuesta como JSON.

// Helper functions for respond with 200 or 500 code
func respondWithError(err error, w http.ResponseWriter) {
	w.WriteHeader(http.StatusInternalServerError)
	json.NewEncoder(w).Encode(err.Error())
}

func respondWithSuccess(data interface{}, w http.ResponseWriter) {

	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(data)
}

Poniendo todo junto

API REST con Go y MySQL codificando con JSON

Para terminar este tutorial veamos cómo es el método main. Este método es el punto de entrada de los programas en Go, por lo tanto en esta función es en donde hacemos un ping a la base de datos, configuramos las rutas y encendemos el servidor en el puerto especificado.

package main

import (
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"time"
)

func main() {
	// Ping database
	bd, err := getDB()
	if err != nil {
		log.Printf("Error with database" + err.Error())
		return
	} else {
		err = bd.Ping()
		if err != nil {
			log.Printf("Error making connection to DB. Please check credentials. The error is: " + err.Error())
			return
		}
	}
	// Define routes
	router := mux.NewRouter()
	setupRoutesForVideoGames(router)
	// .. here you can define more routes
	// ...
	// for example setupRoutesForGenres(router)

	// Setup and start server
	port := ":8000"

	server := &http.Server{
		Handler: router,
		Addr:    port,
		// timeouts so the server never waits forever...
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	log.Printf("Server started at %s", port)
	log.Fatal(server.ListenAndServe())
}

Recuerda que para compilar debes haber instalado Go. Luego de eso, en el directorio en donde está el código, ejecuta go build y ejecuta el archivo que aparezca. Si el firewall pide permisos, dáselos. De igual modo he escrito un poco de documentación en el readme del proyecto.

Puedes probar la API desde cualquier lenguaje de programación o usando Postman como yo lo he hecho en mi vídeo:

Aquí te dejo el código completo, como siempre, en mi GitHub. Te invito también a leer más sobre Go en mi blog.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *