API RESTful en Go

API RESTful en Go

Integración Fiber y GORM

Introducción

Fiber es un framework web rápido y flexible, inspirado en Express.js, diseñado para facilitar la construcción de aplicaciones y APIs RESTful.

GORM es una biblioteca ORM (Object Relational Mapping) que simplifica la interacción con bases de datos, proporcionando una interfaz elegante y eficiente para realizar operaciones de CRUD en bases de datos.

En este tutorial, exploraremos cómo integrar Fiber y GORM para construir una API RESTful en Go. Aprenderemos a configurar un servidor Fiber, establecer la conexión con una base de datos utilizando GORM y crear rutas y controladores para gestionar los recursos de nuestra API.

Proyecto

Crearemos una API RESTful centrado en dos modelos: Author (autor) y Book (libro), con una relación uno a muchos, donde un autor puede tener muchos libros, y un libro pertenece a un autor.

La API nos permitirá gestionar todas las operaciones CRUD (Create, Read, Update, Delete) de ambos modelos para una base de datos MySQL.

Instalación de paquetes

Comenzamos creando el directorio e inicializando el módulo para nuestro proyecto.

mkdir api_fiber
cd api_fiber

Llamé al directorio api_fiber , una vez dentro de la carpeta, inicializamos el módulo de GO.

go mod init api_fiber

Ahora realizamos la instalación de las dependencias de Fiber, GORM y el driver de MySQL.

go get -u github.com/gofiber/fiber/v2
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

Base de datos

Abre tu gestor de base de datos y crea uno para nuestro proyecto. Yo lo llamé api_fiber .

Estructura de ficheros

Para modularizar el código del proyecto, usaremos la siguiente estructura de carpetas.

- main.go
- api/
    - routes.go
- models/
    - author.go
    - book.go
- database/
    - connection.go
- controllers/
    - author_controller.go
    - book_controller.go

El archivo main.go se encuentra en la raíz del proyecto y es el archivo principal que inicializa el servidor.

La carpeta api contiene el archivo routes.go, donde se definen las rutas de la API.

La carpeta models contiene los archivos author.go y book.go, donde se definen los modelos Author y Book.

La carpeta database contiene el archivo connection.go, donde se establece la conexión a la base de datos.

La carpeta controllers contiene los archivos author_controller.go y book_controller.go, donde se definen los controladores para cada modelo.

Conexión a la base de datos

Abre el fichero database/connection.go y escribe el siguiente código:

package database

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDB() *gorm.DB {
    dsn := "root@tcp(127.0.0.1:3306)/api_fiber?charset=utf8mb4&parseTime=True&loc=Local"
    DB, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{})

    return DB
}

Lo que podemos destacar aquí es la definición de la variable DB de tipo *gorm.DB . Lo hacemos así para poder acceder a la instancia de la conexión desde otros módulos.

La función ConnectDB establece la conexión con la base de datos y retorna un objecto *gorm.DB el cuál se asigna a la variable DB .

Modelos

Comenzamos con el fichero models/author.go para el modelo autor.

package models

import "gorm.io/gorm"

type Author struct {
    gorm.Model

    Name     string `json:"name"`
    Lastname string `json:"lastname"`

    Books []Book `gorm:"foreignKey:AuthorID"`
}

Definimos la estructura Author tiene los siguientes campos:

  • gorm.Model: Este campo incorpora los campos de ID, CreatedAt, UpdatedAt y DeletedAt proporcionados por GORM para realizar operaciones básicas de persistencia.

  • Name: Representa el nombre del autor. Está etiquetado con json:"name", lo que indica que este campo se serializará como "name" al interactuar con la API RESTful.

  • Lastname: Representa el apellido del autor. Al igual que el campo Name, está etiquetado con json:"lastname" para la serialización adecuada.

  • Books: Es un slice (una lista) de objetos Book. Esta relación uno a muchos se establece mediante la etiqueta gorm:"foreignKey:AuthorID", que indica que el campo AuthorID en el modelo Book se utilizará como clave externa para la relación con Author. Esto significa que un autor puede tener muchos libros.

Ahora continuamos con el fichero models/book.go .

package models

import "gorm.io/gorm"

type Book struct {
    gorm.Model

    Title       string `json:"title"`
    Description string `json:"description"`
    AuthorID    uint
    Author      Author `json:"author"`
}

La estructura Book tiene los siguientes campos:

  • gorm.Model: Al igual que en el modelo Author, este campo incorpora los campos de ID, CreatedAt, UpdatedAt y DeletedAt proporcionados por GORM para realizar operaciones básicas de persistencia.

  • Title: Representa el título del libro. Está etiquetado con json:"title", lo que indica que este campo se serializará como "title" al interactuar con la API RESTful.

  • Description: Representa la descripción del libro. Al igual que el campo Title, está etiquetado con json:"description" para la serialización adecuada.

  • AuthorID: Es un campo de tipo uint que representa el ID del autor asociado al libro. Este campo se utilizará como clave externa para establecer la relación con el modelo Author.

  • Author: Es un objeto del tipo Author que representa al autor del libro. Está etiquetado con json:"author" para la serialización adecuada al interactuar con la API RESTful.

Configuración del servidor Fiber

Abrimos el fichero principal de nuestra aplicación main.go .

package main

import (
    "github.com/gofiber/fiber/v2"

    "github.com/alegrecode/api_fiber/api"
    "github.com/alegrecode/api_fiber/database"
    "github.com/alegrecode/api_fiber/models"
)

func main() {
    app := fiber.New()

    db := database.ConnectDB()

    db.AutoMigrate(&models.Author{}, &models.Book{})

    api.Router(app)

    app.Listen(":3000")
}

Creamos una instancia de la aplicación Fiber con fiber.New() y lo guardamos en la variable app .

Realizamos la conexión a la base de datos con database.ConnectDB() y guardamos dicha conexión en la variable db.

Corremos la función db.AutoMigrate() de GORM para realizar las migraciones de los modelos Author y Book y crear las respectivas tablas en la base de datos.

La función api.Router() corresponde la enrutador de nuestra aplicación, que pasaremos a configurar más tarde. Recibe como parámetro la instancia de Fiber .

Finalmente, se inicia el servidor web utilizando app.Listen(":3000"), lo que indica que el servidor escuchará en el puerto 3000.

Controladores

Abrimos el fichero controllers/author_controller.go y escribimos el siguiente código.

package controllers

import (
    "github.com/gofiber/fiber/v2"

    "github.com/alegrecode/api_fiber/database"
    "github.com/alegrecode/api_fiber/models"
)

type AuthorController struct {
}

func (ac *AuthorController) GetAllAuthors(c *fiber.Ctx) error {
    var authors []models.Author
    database.DB.Preload("Books").Find(&authors)
    return c.JSON(authors)
}

Definimos la estructura AuthorController sin campos que actuará como contenedor de las funciones relacionadas con los autores. Luego le añadimos la función GetAllAuthors que se encargará de obtener todos los autores. Recibe un parámetro c de tipo *fiber.Ctx, que representa el contexto de la solicitud HTTP.

Declaramos una variable authors de tipo []models.Author, que será utilizada para almacenar los autores obtenidos de la base de datos.

La función database.DB.Preload("Books").Find(&authors) recupera todos los autores de la base de datos. La función Preload("Books") se utiliza para cargar automáticamente los libros relacionados con cada autor en la consulta.

Finalmente, se utiliza c.JSON(authors) para enviar una respuesta JSON al cliente con los autores obtenidos de la base de datos.

Ahora pasamos a crear la función para obtener un autor por su ID .

func (ac *AuthorController) GetAuthor(c *fiber.Ctx) error {
    id := c.Params("id")
    var author models.Author

    result := database.DB.Preload("Books").First(&author, id)

    if result.Error != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "message": "Author not found",
        })
    }

    return c.JSON(author)
}

Obtenemos el parámetro id de la URL utilizando c.Params("id").

La variable author de tipo models.Author se utilizará para almacenar el autor obtenido de la base de datos.

La función database.DB.Preload("Books").First(&author, id) realiza una consulta a la base de datos y obtiene el primer autor que coincida con el ID especificado.

Verificamos si hay algún error en la consulta utilizando result.Error. Si hay un error, se devuelve una respuesta JSON con un estado de fiber.StatusNotFound y un mensaje de "Author not found".

Si no hay errores, se devuelve una respuesta JSON con el autor obtenido utilizando c.JSON(author).

Añadimos el código para crear un nuevo autor.

func (ac *AuthorController) CreateAuthor(c *fiber.Ctx) error {

    author := new(models.Author)

    if err := c.BodyParser(author); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Could not create author",
            "error":   err.Error(),
        })
    }

    database.DB.Create(&author)
    return c.JSON(author)
}

La función CreateAuthor maneja la solicitud de crear un nuevo autor. Utiliza c.BodyParser para deserializar los datos JSON recibidos en la solicitud y asignarlos a una instancia del modelo Author. Luego, utiliza database.DB.Create para guardar el nuevo autor en la base de datos. Se envía una respuesta JSON con el autor creado si todo es exitoso, o una respuesta de error si hay algún problema en el análisis o en la creación del autor en la base de datos.

Ahora vamos con la función para actualizar un autor.

func (ac *AuthorController) UpdateAuthor(c *fiber.Ctx) error {
    id := c.Params("id")
    var author models.Author

    result := database.DB.First(&author, id)

    if result.Error != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "message": "Author not found",
        })
    }

    if err := c.BodyParser(&author); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Could not update author",
            "error":   err.Error(),
        })
    }

    database.DB.Model(&author).Updates(&author)

    return c.JSON(author)
}

La función UpdateAuthor gestiona la actualización de un autor existente. Utiliza database.DB.First para obtener el autor existente de la base de datos por su ID. Luego, utiliza c.BodyParser para analizar los datos JSON recibidos en la solicitud y asignarlos a la instancia del autor existente. A continuación, utiliza database.DB.Model(...).Updates(...) para actualizar los campos del autor en la base de datos. Se envía una respuesta JSON con el autor actualizado si todo es exitoso, o una respuesta de error si el autor no se encuentra en la base de datos o hay algún problema en el análisis o en la actualización del autor.

Por último creamos la función para borrar un autor.

func (ac *AuthorController) DeleteAuthor(c *fiber.Ctx) error {
    id := c.Params("id")
    var author models.Author

    result := database.DB.First(&author, id)

    if result.Error != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "message": "Author not found",
        })
    }

    database.DB.Delete(&author)

    return c.Status(fiber.StatusOK).JSON(fiber.Map{
        "message": "Author deleted",
    })
}

La función DeleteAuthor se encarga de eliminar un autor existente. Utiliza database.DB.First para obtener el autor existente de la base de datos por su ID. Luego, utiliza database.DB.Delete para eliminar el autor de la base de datos. Se envía una respuesta JSON con un mensaje de "Author deleted" si todo es exitoso, o una respuesta de error si el autor no se encuentra en la base de datos.

Con esto finalizamos todas las operaciones CRUD para los autores.

A continuación abrimos el fichero controllers/book_controller.go y añadimos el siguiente código.

package controllers

import (
    "github.com/alegrecode/api_fiber/database"
    "github.com/alegrecode/api_fiber/models"
    "github.com/gofiber/fiber/v2"
)

type BookController struct {
}

func (bc *BookController) CreateBook(c *fiber.Ctx) error {
    var book models.Book
    var author models.Author

    if err := c.BodyParser(&book); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Could not create book",
            "error":   err.Error(),
        })
    }

    result := database.DB.First(&author, book.AuthorID)

    if result.Error != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Invalid author ID",
        })
    }

    book.Author = author
    database.DB.Create(&book)
    return c.JSON(book)
}

func (bc *BookController) GetAllBooks(c *fiber.Ctx) error {
    var books []models.Book
    database.DB.Preload("Author").Find(&books)
    return c.JSON(books)
}

func (bc *BookController) GetBook(c *fiber.Ctx) error {
    id := c.Params("id")
    var book models.Book
    result := database.DB.Preload("Author").First(&book, id)
    if result.Error != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Book not found",
        })
    }
    return c.JSON(book)
}

func (bc *BookController) UpdateBook(c *fiber.Ctx) error {
    id := c.Params("id")
    var book models.Book

    result := database.DB.First(&book, id)

    if result.Error != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Invalid book ID",
        })
    }

    if err := c.BodyParser(&book); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Could not update author",
            "error":   err.Error(),
        })
    }

    database.DB.Model(&book).Updates(&book)

    return c.JSON(book)
}

func (bc *BookController) DeleteBook(c *fiber.Ctx) error {
    id := c.Params("id")
    var book models.Book

    result := database.DB.First(&book, id)

    if result.Error != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Invalid book ID",
        })
    }

    database.DB.Delete(&book)
    return c.Status(fiber.StatusOK).JSON(fiber.Map{
        "message": "Book deleted",
    })
}

Como se puede observar las funciones para las operaciones CRUD de los libros son muy similares a las funciones del controlador para los autores, con excepción de la función CreateBook , el cual contiene la siguiente validación.

result := database.DB.First(&author, book.AuthorID)

    if result.Error != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": "Invalid author ID",
        })
    }

Este fragmento de código verifica que el autor asociado al libro exista en la base de datos, si el autor no existe, retorna un mensaje de error.

Finalizamos con los controladores, ahora seguimos con el enrutador.

Enrutador

Abrimos el fichero api/router.go y añadimos el siguiente código.

package api

import (
    "github.com/alegrecode/api_fiber/controllers"
    "github.com/gofiber/fiber/v2"
)

func Router(app *fiber.App) {

    var authorController = &controllers.AuthorController{}
    var bookController = &controllers.BookController{}

    app.Get("/authors", authorController.GetAllAuthors)

    app.Post("/author", authorController.CreateAuthor)

    app.Get("/author/:id", authorController.GetAuthor)

    app.Put("/author/:id", authorController.UpdateAuthor)

    app.Delete("/author/:id", authorController.DeleteAuthor)

    app.Get("/books", bookController.GetAllBooks)

    app.Post("/book", bookController.CreateBook)

    app.Get("/book/:id", bookController.GetBook)

    app.Put("/book/:id", bookController.UpdateBook)

    app.Delete("/book/:id", bookController.DeleteBook)
}

La función Router se encarga de configurar las rutas de la API. Inicializamos los controladores AuthorController y BookController utilizando &controllers.AuthorController{} y &controllers.BookController{}, respectivamente.

Definimos las rutas para realizar las operaciones CRUD de autores y libros y los asociamos a su controlador correspondiente.

Probando el API

Para correr el servidor ejecutamos el siguiente comando.

go run main.go

Si todo ha ido bien podrás ver las tablas en tu base de datos.

También se puede observar que la realción "uno a muchos" se ha establecido correctamete.

Para probar las operaciones vamos a usar Postman.

Comencemos agragando algunos autores. Voy a añadir dos autores: Agatha Christie y Jack London.

En la parte inferior, podemos ver la respueta JSON que indica que la operación se realizó correctamente.

Lo podemos verificar en la tabla authors.

Ahora los libros, voy a añadir dos para Agatha Christie, "Asesinato en el Orient Express" y "Muerte en el Nilo", y uno para Jack London, "Colmillo Blanco".

Hay que destacar el campo authorID que se encarga de establecer el vínculo con el autor del libro.

Podemos ver la respuesta JSON que indica que la operación se realizó con éxito.

Lo verificamos en la base de datos.

Observa el último campo author_id que apunta hacia el autor correspondiente del libro.

Ahora que tenemos los datos guardados correctamente, vamos a ver como funciona la operación GET.

Primero probamos obtener todos los libros. Realizamos un petición GET a la ruta localhost:3000/books . La respuesta JSON es la siguiente.

[
    {
        "ID": 1,
        "CreatedAt": "2023-06-04T18:23:35.193-03:00",
        "UpdatedAt": "2023-06-04T18:23:35.193-03:00",
        "DeletedAt": null,
        "title": "Asesinato en el Orient Express",
        "description": "Misterio",
        "AuthorID": 1,
        "author": {
            "ID": 1,
            "CreatedAt": "2023-05-31T18:54:07.113-03:00",
            "UpdatedAt": "2023-05-31T18:54:07.113-03:00",
            "DeletedAt": null,
            "name": "Agatha",
            "lastname": "Christie",
            "Books": null
        }
    },
    {
        "ID": 2,
        "CreatedAt": "2023-06-04T18:27:31.671-03:00",
        "UpdatedAt": "2023-06-04T18:27:31.671-03:00",
        "DeletedAt": null,
        "title": "Muerte en el Nilo",
        "description": "Misterio",
        "AuthorID": 1,
        "author": {
            "ID": 1,
            "CreatedAt": "2023-05-31T18:54:07.113-03:00",
            "UpdatedAt": "2023-05-31T18:54:07.113-03:00",
            "DeletedAt": null,
            "name": "Agatha",
            "lastname": "Christie",
            "Books": null
        }
    },
    {
        "ID": 3,
        "CreatedAt": "2023-06-04T18:29:05.712-03:00",
        "UpdatedAt": "2023-06-04T18:29:05.712-03:00",
        "DeletedAt": null,
        "title": "Colmillo Blanco",
        "description": "Aventura",
        "AuthorID": 2,
        "author": {
            "ID": 2,
            "CreatedAt": "2023-05-31T18:58:59.357-03:00",
            "UpdatedAt": "2023-05-31T18:58:59.357-03:00",
            "DeletedAt": null,
            "name": "Jack",
            "lastname": "London",
            "Books": null
        }
    }
]

Se puede observar que se obtiene los datos de los libros y cada uno de los libros tiene un campo específico author , el cual es un objecto con los datos del autor.

Ahora probemos obtener todos los autores. Realizamos la operación GET localhost:3000/authors . Obtenemos la siguiente respuesta.

[
    {
        "ID": 1,
        "CreatedAt": "2023-05-31T18:54:07.113-03:00",
        "UpdatedAt": "2023-05-31T18:54:07.113-03:00",
        "DeletedAt": null,
        "name": "Agatha",
        "lastname": "Christie",
        "Books": [
            {
                "ID": 1,
                "CreatedAt": "2023-06-04T18:23:35.193-03:00",
                "UpdatedAt": "2023-06-04T18:23:35.193-03:00",
                "DeletedAt": null,
                "title": "Asesinato en el Orient Express",
                "description": "Misterio",
                "AuthorID": 1,
                "author": {
                    "ID": 0,
                    "CreatedAt": "0001-01-01T00:00:00Z",
                    "UpdatedAt": "0001-01-01T00:00:00Z",
                    "DeletedAt": null,
                    "name": "",
                    "lastname": "",
                    "Books": null
                }
            },
            {
                "ID": 2,
                "CreatedAt": "2023-06-04T18:27:31.671-03:00",
                "UpdatedAt": "2023-06-04T18:27:31.671-03:00",
                "DeletedAt": null,
                "title": "Muerte en el Nilo",
                "description": "Misterio",
                "AuthorID": 1,
                "author": {
                    "ID": 0,
                    "CreatedAt": "0001-01-01T00:00:00Z",
                    "UpdatedAt": "0001-01-01T00:00:00Z",
                    "DeletedAt": null,
                    "name": "",
                    "lastname": "",
                    "Books": null
                }
            }
        ]
    },
    {
        "ID": 2,
        "CreatedAt": "2023-05-31T18:58:59.357-03:00",
        "UpdatedAt": "2023-05-31T18:58:59.357-03:00",
        "DeletedAt": null,
        "name": "Jack",
        "lastname": "London",
        "Books": [
            {
                "ID": 3,
                "CreatedAt": "2023-06-04T18:29:05.712-03:00",
                "UpdatedAt": "2023-06-04T18:29:05.712-03:00",
                "DeletedAt": null,
                "title": "Colmillo Blanco",
                "description": "Aventura",
                "AuthorID": 2,
                "author": {
                    "ID": 0,
                    "CreatedAt": "0001-01-01T00:00:00Z",
                    "UpdatedAt": "0001-01-01T00:00:00Z",
                    "DeletedAt": null,
                    "name": "",
                    "lastname": "",
                    "Books": null
                }
            }
        ]
    }
]

Observemos que obtenemos los autores y cada uno tiene un campo específico Books el cuál es un array de libros pertenecientes al autor.

Con esto verificamos que la relación entre los modelos Author y Book se configuró correctamente.

Puedes ir probando el resto de las operaciones CRUD, para comprobar que todo funciona de forma adecuada.

Conclusión

Hemos finalizado con el proyecto. En este tutorial, hemos explorado cómo construir una API REST utilizando Fiber y Gorm en Go. Hemos aprendido a configurar el enrutador de Fiber y a utilizar Gorm como ORM para interactuar con la base de datos. Hemos creado modelos para representar los recursos 'Author' y 'Book', y hemos establecido una relación de uno a muchos entre ellos. Además, hemos implementado las operaciones CRUD básicas para cada recurso utilizando los controladores.

Puedes encontrar el proyecto completo en mi repositorio de GitHub haciendo clic AQUÍ.

Espero que lo hayas encontrado entretenido, instructivo y claro. Si tienes alguna duda, puedes hacérmelo saber en los comentarios. Pronto estaré subiendo más tutoriales.

Nos vemos en la próxima. Saludos!👋😊