¡Compártelo!

Cómo implementar GraphQL en un backend basado en Go

GraphQL propone una nueva manera de estructurar las APIs: en lugar de múltiples endpoints como en REST, todo gira en torno a un único punto de entrada donde los clientes definen exactamente qué datos necesitan.

En el desarrollo actual de aplicaciones es muy común utilizar APIs (Application Programming Interface) para el intercambio de datos entre distintos softwares, ya sea para realizar operaciones básicas con bases de datos, para realizar integraciones con servicios externos o cualquier otra operativa.

Históricamente, el formato de API REST (Representational State Transfer) ha sido el más popular desde que fuera introducido por Roy Fielding en el año 2000 por su simplicidad.

Sin embargo; en 2012, Facebook comenzó a trabajar en el desarrollo de una alternativa más flexible a este paradigma: GraphQL, con Lee Byron liderando el equipo de desarrollo. En 2015 fue liberado como proyecto de código abierto, y actualmente es mantenido por la GraphQL Foundation.

¿Por qué elegir GraphQL?

Algunas ventajas clave de GraphQL frente a REST:

  • Flexibilidad: los clientes solicitan solo los datos que necesitan, evitando el overfetching y underfetching, que sería recuperar más y menos datos de los que se necesitan.
  • Documentación autogenerada: GraphQL genera documentación en tiempo real a partir del esquema, eliminando la necesidad de herramientas externas como Swagger.
  • Un solo endpoint: todas las operaciones se realizan desde un único punto de entrada.
  • Tipado fuerte: el esquema actúa como contrato entre cliente y servidor, mejorando la validación y el desarrollo asistido.

Entrando un poco más en materia, un servicio GraphQL se crea definiendo tipos con sus  campos y luego se implementan funciones para resolver los datos de cada campo. Los tipos y campos conforman lo que se conoce como la definición del esquema. Las funciones que recuperan y mapean los datos se denominan resolvers.

Teniendo esta idea sobre el ecosistema, vamos a implementar GraphQL en un backend basado en el lenguaje de programación Go.

Instalación de dependencias


Para ello, primero vamos a instalar en el directorio de nuestro proyecto las dependencias necesarias, en este caso la biblioteca gqlgen:

go install github.com/99designs/gqlgen@latest
go mod init profile-graphql-go
go get github.com/99designs/gqlgen (accede aquí)
go run github.com/99designs/gqlgen init

Definición de los tipos y el esquema

Vamos a explorar un ejemplo sencillo de API. Diseñaremos los siguientes tipos:

  • User con id, name y una lista de posts.
  • Post con id, title, content y referencia al authorID.
  • Queries (consultas):
    • users: lista de todos los usuarios.
    • user(id: ID!): obtener un usuario específico.
    • postsByAuthor(userID: ID!): obtener posts de un usuario.

En nuestro proyecto, editaremos el fichero graph/schema.graphqls para adecuarlo a nuestra especificación. Este esquema sirve para describir los tipos de datos y sus relaciones. Debería quedarnos la siguiente estructura.

type Query {
  users: [User!]!
  user(id: ID!): User
  postsByAuthor(userID: ID!): [Post!]!
}


type User {
  id: ID!
  name: String!
  posts: [Post!]!
}


type Post {
  id: ID!
  title: String!
  content: String!
  authorID: String!
}

Simulación con datos de prueba

En el directorio raíz de nuestro proyecto, vamos a crear un fichero llamado data.json con datos de prueba que nos agilizará más tarde la puesta en marcha del proyecto.

{
    "Users": [
      {
        "id": "1",
        "name": "Javier Aranda"
      },
      {
        "id": "2",
        "name": "John Doe"
      }
    ],
    "Posts": [
      {
        "id": "101",
        "title": "Cómo implementar GraphQL en un backend en Go",
        "content": "En el desarrollo actual de aplicaciones es muy común utilizar APIs...",
        "authorId": "1"
      },
      {
        "id": "102",
        "title": "Crea un chatbot con la API de OpenAI en 40 líneas de código",
        "content": "La inteligencia artificial está en boca de todo el mundo desde que OpenAI sorprendiera al mundo en noviembre de 2022 con el lanzamiento de ChatGPT...",
        "authorId": "1"
      },
      {
        "id": "103",
        "title": "¿Qué es LVM y cómo funciona en Linux?",
        "content": "Si gestionas almacenamiento en Linux, seguro has oído hablar de LVM (Logical Volume Manager)...",
        "authorId": "2"
      }
    ]
  }

Además de esto, necesitaremos especificar la lógica para parsear este fichero, para ello crearemos en el directorio graph un fichero llamado fixtures.go:

package graph


import (
    "encoding/json"
    "os"
    "profile-graphql-go/graph/model"
)


type seedData struct {
    Users []*model.User `json:"users"`
    Posts []*model.Post `json:"posts"`
}


func LoadData(path string) ([]*model.User, []*model.Post, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, nil, err
    }
    defer file.Close()


    var data seedData
    if err := json.NewDecoder(file).Decode(&data); err != nil {
        return nil, nil, err
    }


    // Mapeo de posts a usuarios
    for _, user := range data.Users {
        for _, post := range data.Posts {
            if post.AuthorID == user.ID {
                user.Posts = append(user.Posts, post)
            }
        }
    }


    return data.Users, data.Posts, nil

Modificando los resolvers

A continuación, vamos a escribir el resolver, que como mencionamos al principio es la función que se encarga de recuperar y mapear los datos. En el mismo directorio editamos el fichero resolver.go:

ackage graph


import "profile-graphql-go/graph/model"


type Resolver struct {
    Users []*model.User
    Posts []*model.Post
}

Tras esto, vamos a ejecutar el comando gqlgen generate para actualizar el código autogenerado por la biblioteca, y editamos el fichero schema.resolvers.go para definir la operativa.

package graph


// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.73


import (
    "context"
    models "profile-graphql-go/graph/model"
)


// Resolver para usuarios
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
    return r.Resolver.Users, nil
}


// Resolver para usuario único
func (r *queryResolver) User(ctx context.Context, id string) (*models.User, error) {
    for _, user := range r.Resolver.Users {
        if user.ID == id {
            return user, nil
        }
    }
    return nil, nil
}


// Resolver para obtener los posts por ID de autor
func (r *queryResolver) PostsByAuthor(ctx context.Context, userID string) ([]*models.Post, error) {
    var posts []*models.Post
    for _, post := range r.Posts {
        if post.AuthorID == userID {
            posts = append(posts, post)
        }
    }
    return posts, nil
}


func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }


type queryResolver struct{ *Resolver }

Integración en el servidor HTTP

Finalmente, vamos a actualizar el fichero server.go en la raíz del proyecto para que acople todos los mecanismos que hemos definido previamente.

package main


import (
    "log"
    "net/http"
    "os"


    "profile-graphql-go/graph"


    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/handler/extension"
    "github.com/99designs/gqlgen/graphql/handler/lru"
    "github.com/99designs/gqlgen/graphql/handler/transport"
    "github.com/99designs/gqlgen/graphql/playground"
    "github.com/vektah/gqlparser/v2/ast"
)


const defaultPort = "8080"


func main() {
    users, posts, err := graph.LoadData("data.json")
    if err != nil {
        log.Fatalf("error cargando data: %v", err)
    }


    resolver := &graph.Resolver{
        Users: users,
        Posts: posts,
    }


    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }


    srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: resolver}))


    srv.AddTransport(transport.Options{})
    srv.AddTransport(transport.GET{})
    srv.AddTransport(transport.POST{})


    srv.SetQueryCache(lru.New[*ast.QueryDocument](1000))


    srv.Use(extension.Introspection{})
    srv.Use(extension.AutomaticPersistedQuery{
        Cache: lru.New[string](100),
    })


    http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", srv)


    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))

Con todo esto, ya podemos ejecutar nuestro servidor, que nos abrirá un playground de GraphQL con el que podremos interactuar haciendo consultas.

playground de GraphQL

Conclusión

La implementación de GraphQL en un backend basado en Go, potenciado por glqgen, permite desarrollar APIs modernas de una forma sencilla. Aunque este tutorial cubre una puesta en marcha con lo mínimo indispensable, la escalabilidad y la eficiencia de un backend basado en esta tecnología, al igual que en REST, depende de tener un buen diseño y arquitectura.

Déjanos tu comentario en nuestras redes sociales y síguenos en nuestro canal de YouTube para mantenerte al día sobre lo último en programación.

Artículos ​ relacionados