Golang GraphQL para una API usando PostgreSQL

Crearemos un servidor web con el lenguaje de consulta GraphQL, Golang y en base de datos PostgreSQL. Las librerías o frameworks que estaremos usando son Gin, GORM y graphql-go. Gin es el framework web, GORM es el ORM para la base de datos PostgreSQL y graphql-go es para la resolución de las consultas.

Prerrequisitos y consideraciones

El sistema operativo donde se desarrolló la explicación es macOS 10.15.4, estaremos usando algunos comandos de terminal, todos se ejecutaran con usuario simple. La versión de Golang 1.14 y PostgreSQL 12.2, las cuales se da por supuesto que han sido instaladas previamente.

Se puede seguir los siguientes enlaces para descargar e instalar Golang y PostgreSQL en los distintos sistemas operativos, sea Windows, Linux y MacOS.

Creando el proyecto TODO

Lo primero es crear un directorio llamado todo, nos colocamos dentro del directorio creado e iniciamos la gestión de dependencias.

$ mkdir todo
$ cd todo
$ go mod init todo

Luego obtenemos las dependencias necesarias para el proyecto, en nuestro caso son Gin, GORM y graphql-go.

$ go get github.com/gin-gonic/gin github.com/graphql-go/graphql github.com/jinzhu/gorm

Con la línea anterior se descargaran la dependencias, además se incluirán en nuestro archivo go.mod.

Iniciamos el servidor GraphQL con Golang y Gin

Creamos el archivo main.go e iniciamos el servicio web.

package main

import (
  "todo/models"
  "todo/controllers"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()

  db := models.SetupModels()
  defer db.Close()

  r.Use(func(c *gin.Context) {
    c.Set("db", db)
    c.Next()
  })

  r.GET("/graphql", controllers.Graphql)

  r.Run()
}

En el código anterior, iniciamos el servidor con las configuraciones por defecto de Gin, levantamos la conexión a la base de datos con el método SetupModels que crearemos en la próxima sección, indicamos que al finalizar la función, se ejecute la función para cerrar la conexión a la base de datos.

También creamos un middleware donde establecemos la asociación entre la conexión anteriormente creada con una variable en el servidor web, nos servirá a la hora de hacer el o los controladores. En la línea 20, creamos la ruta o endpoint para la resolución de las peticiones en nuestro controlador.

Generando el modelo TODO y conexión a la base de datos

Dentro del proyecto crearemos un directorio llamado models y dentro un archivo llamada todo.go, donde especificaremos la estructura del modelo y el type para GraphQL.

package models

import (
  "github.com/graphql-go/graphql"
)

type Todo struct {
  ID int `json:"id" gorm:"primary_key"`
  Name string `json:"name"`
  Status int `json:"status"`
}

var TodoType = graphql.NewObject(graphql.ObjectConfig{
  Name: "Todo",
  Fields: graphql.Fields{
    "id": &graphql.Field{
      Type: graphql.Int,
    },
    "name": &graphql.Field{
      Type: graphql.String,
    },
    "status": &graphql.Field{
      Type: graphql.Int,
    },
  },
})

En models también creamos la conexión hacia la base de datos en el archivo setup.go, con los parametros necesarios para ingresar al PostgreSQL y una migración de la estructura.

package models

import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/postgres"
)

func SetupModels() *gorm.DB {
  db, err := gorm.Open("postgres", "user= dbname= sslmode=disable")

  if err != nil {
    panic(err)
  }

  db.AutoMigrate(&Todo{})

  return db
}

En el directorio models dejamos las definiciones con las estructuras necesarias.

Controlador para resolver la petición

En está sección desarrollaremos el procesamiento del query o mutation de la petición. Procedemos a hacer un directorio controllers, allí creamos un archivo llamado graphql.go donde tendremos una pequeña estructura para obtener el query de la petición y la función para procesar la petición del cliente.

package controllers

import (
  "bytes"
  "encoding/json"
  "fmt"
  "net/http"
  "todo/models"

  "github.com/gin-gonic/gin"
  "github.com/graphql-go/graphql"
  "github.com/jinzhu/gorm"
)

type reqBody struct {
  Query string `json:"query"`
}

func Graphql(c *gin.Context) {

  db := c.MustGet("db").(*gorm.DB)

  rawBody, _ := c.GetRawData()
  b := bytes.NewBuffer(rawBody)

  var rBody reqBody
  err := json.NewDecoder(b).Decode(&rBody)
  if err != nil {
    fmt.Println(err)
  }

  rootQuery := graphql.NewObject(graphql.ObjectConfig{
    Name: "Query",
    Fields: graphql.Fields{
      "todo": &graphql.Field{
        Type: models.TodoType,
        Args: graphql.FieldConfigArgument{
          "id": &graphql.ArgumentConfig{
            Type: graphql.Int,
          },
          "name": &graphql.ArgumentConfig{
            Type: graphql.String,
          },
          "status": &graphql.ArgumentConfig{
            Type: graphql.Int,
          },
        },
        Resolve: func(params graphql.ResolveParams) (interface{}, error) {
          var find models.Todo
          if id, isOK := params.Args["id"].(int); isOK == true {
            find.ID = id
          }
          if name, isOK := params.Args["name"].(string); isOK == true {
            find.Name = name
          }
          if status, isOK := params.Args["status"].(int); isOK == true {
            find.Status = status
          }

          var todo models.Todo
          if err := db.Where(find).First(&todo).Error; err != nil {
            return nil, err
          }

          return todo, nil
        },
      },
      "todos": &graphql.Field{
        Type: graphql.NewList(models.TodoType),
        Args: graphql.FieldConfigArgument{
          "id": &graphql.ArgumentConfig{
            Type: graphql.Int,
          },
          "name": &graphql.ArgumentConfig{
            Type: graphql.String,
          },
          "status": &graphql.ArgumentConfig{
            Type: graphql.Int,
          },
        },
        Resolve: func(params graphql.ResolveParams) (interface{}, error) {
          var find models.Todo
          if id, isOK := params.Args["id"].(int); isOK == true {
            find.ID = id
          }
          if name, isOK := params.Args["name"].(string); isOK == true {
            find.Name = name
          }
          if status, isOK := params.Args["status"].(int); isOK == true {
            find.Status = status
          }

          var todos []models.Todo
          db.Where(find).Find(&todos)

          return todos, nil
        },
      },
    },
  })

  rootMutation := graphql.NewObject(graphql.ObjectConfig{
    Name: "Mutation",
    Fields: graphql.Fields{
      "createTodo": &graphql.Field{
        Type: models.TodoType,
        Args: graphql.FieldConfigArgument{
          "name": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
          },
        },
        Resolve: func(params graphql.ResolveParams (interface{}, error) {
          var todo models.Todo
          todo.Name = params.Args["name"].(string)
          todo.Status = 1
          db.Create(&todo)

          return todo, nil
        },
      },
      "updateTodo": &graphql.Field{
        Type: models.TodoType,
        Args: graphql.FieldConfigArgument{
          "id": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Int),
          },
          "name": &graphql.ArgumentConfig{
            Type: graphql.String,
          },
          "status": &graphql.ArgumentConfig{
            Type: graphql.Int,
          },
        },
        Resolve: func(params graphql.ResolveParams (interface{}, error) {
          id := params.Args["id"].(int)

          var todo models.Todo
          if err := db.Where("id = ?", id).First(&todo).Error; err != nil {
            return nil, err
          }

          if name, isOK := params.Args["name"].(string); isOK == true {
            todo.Name = name
          }

          if status, isOK := params.Args["status"].(int); isOK == true {
            todo.Status = status
          }

          db.Save(&todo)

          return todo, nil
        },
      },
      "deleteTodo": &graphql.Field{
        Type: models.TodoType,
        Args: graphql.FieldConfigArgument{
          "id": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Int),
          },
        },
        Resolve: func(params graphql.ResolveParams) (interface{}, error) {
          id := params.Args["id"].(int)

          var todo models.Todo
          if err := db.Where("id = ?", id).First(&todo).Delete(&todo).Error; err != nil {
            return nil, err
          }

          return todo, nil
        },
      },
    },
  })

  schemaConfig := graphql.SchemaConfig{
    Query: rootQuery,
    Mutation: rootMutation,
  }

  schema, _ := graphql.NewSchema(schemaConfig) // Query

  params := graphql.Params{Schema: schema, RequestString: rBody.Query}
  r := graphql.Do(params)

  c.JSON(http.StatusOK, r)
}

En la función Graphql lo primero es tomar la conexión hacia la base de datos y procesar body de la petición, definimos los query con la variable rootQuery y los mutation con rootMutation. Configuramos el schema y en la línea 184 hacemos la ejecución de los parametros de la petición y el schema.

La librería graphql-go es bastante versátil y vemos validaciones como NewNonNull para hacer obligatorio algún parametro en la definición del query o mutation.

Conclusión

En este ejemplo vemos lo rápido y fácil generar un API de tipo GraphQL con Golang, usando unos cuantos módulos de tantos que tiene Golang para hacer este tipo de proyectos.

En mi repositorio se encuentra el código de ejemplo https://github.com/ajdelgados/golang-graphql

Golang GraphQL para una API usando PostgreSQL
Etiquetado en:                     

2 pensamientos en “Golang GraphQL para una API usando PostgreSQL

  • 25 agosto, 2020 a las 1:17 pm
    Permalink

    Genial!
    Estaba buscando info acerca de golang y graphql.
    Muchas gracias!!

    Responder
  • 30 agosto, 2024 a las 9:26 am
    Permalink

    Saludos, excelente explicación, estoy en busca de un asesor para desarrollar backend con GIN, GORM GRAPHQL, POSTGRES , ojalá tenga tiempo para que me pueda asesorar

    Responder

Deja un comentario

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