Introdução: Por que GraphQL em Go com gqlgen?
GraphQL é uma linguagem de query moderna que permite aos clientes solicitar exatamente os dados que precisam, nada mais, nada menos. Quando comparado a REST APIs tradicionais, GraphQL reduz significativamente o over-fetching e under-fetching de dados. Go é uma linguagem compilada, rápida e eficiente, ideal para construir APIs de alta performance. O gqlgen é um gerador de código GraphQL para Go que segue a abordagem schema-first, significa que você define primeiro o contrato da sua API no schema GraphQL, e então o gqlgen gera código tipo-seguro para você implementar.
A abordagem schema-first é poderosa porque estabelece um contrato claro entre cliente e servidor antes de qualquer implementação. Você não fica preso a estruturas Go específicas; em vez disso, define o que sua API deve fazer, e o gqlgen cria as interfaces e tipos necessários. Isso torna o desenvolvimento mais organizado, previsível e menos propenso a erros de tipo em tempo de execução. Vou guiá-lo através do processo completo: desde a instalação, passando pela definição do schema, até a implementação dos resolvers com tipagem completa.
Configuração do Projeto e Instalação
Preparando o Ambiente
Comece criando um novo diretório para seu projeto e inicialize um módulo Go. Você precisará do Go 1.16 ou superior instalado em sua máquina. O gqlgen foi projetado para funcionar com a estrutura padrão de projetos Go, então mantenemos uma organização simples e clara.
mkdir meu-graphql-api
cd meu-graphql-api
go mod init github.com/seu-usuario/meu-graphql-api
Agora instale o gqlgen. A forma recomendada é adicioná-lo como uma dependência indireta através de um arquivo tools.go:
// tools.go
//go:build tools
// +build tools
package main
import (
_ "github.com/99designs/gqlgen"
)
Execute go mod tidy para baixar a dependência. Depois, crie o arquivo de configuração:
go run github.com/99designs/gqlgen init
Este comando cria:
- gqlgen.yml — configuração do projeto
- graph/schema.graphqls — seu schema GraphQL
- graph/model/models_gen.go — modelos gerados
- Pastas de suporte como graph/resolver.go
Estrutura do Projeto
A estrutura final fica assim:
meu-graphql-api/
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph/
│ ├── schema.graphqls
│ ├── model/
│ │ └── models_gen.go
│ ├── resolver.go
│ └── schema.resolvers.go
├── server.go
└── tools.go
O arquivo gqlgen.yml é o coração da configuração. Ele mapeia seus tipos GraphQL para tipos Go e define como o código é gerado. Por padrão, ele está bem configurado, mas você pode customizá-lo conforme necessário para controlar namespaces de pacotes, caminhos de saída e comportamentos de geração.
Definindo o Schema GraphQL (Schema-First)
Conceitos Fundamentais do Schema
Um schema GraphQL define todos os tipos, queries, mutations e subscriptions disponíveis em sua API. Ele é um contrato explícito entre cliente e servidor. Quando você trabalha com schema-first, este é o ponto de partida: você escreve o schema antes do código Go, e o gqlgen infere quais tipos e funções você precisa implementar.
Abra o arquivo graph/schema.graphqls e defina um schema simples mas realista. Vou criar um exemplo de um sistema de blog:
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: String!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Query {
post(id: ID!): Post
user(id: ID!): User
allPosts: [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post
}
input CreatePostInput {
title: String!
content: String!
authorID: ID!
}
input UpdatePostInput {
title: String
content: String
}
Este schema define dois tipos principais (Post e User), as operações de leitura (Query), as operações de escrita (Mutation), e dois tipos de entrada (CreatePostInput e UpdatePostInput). A exclamação ! indica que o campo é obrigatório (não-nulo). Colchetes [] indicam listas.
Gerando Código a partir do Schema
Após definir o schema, execute:
go run github.com/99designs/gqlgen generate
O gqlgen analisa o schema, verifica as dependências e gera código tipo-seguro. Ele cria:
- Interfaces Go para todos os tipos (Mutation, Query, Post, etc.)
- Modelos de dados estruturados
- Um arquivo schema.resolvers.go onde você implementa os resolvers
Este processo elimina a necessidade de você definir manualmente as estruturas Go correspondentes a cada tipo GraphQL. O código gerado é consistente, bem-tipado e segue as melhores práticas de Go.
Implementando Resolvers Tipados
O que é um Resolver?
Um resolver é uma função que implementa a lógica de negócio para um campo específico em seu schema GraphQL. Para cada campo que não é um tipo primitivo de uma única fonte de dados, você precisa de um resolver. No schema anterior, Query.post, Query.user, Mutation.createPost e o campo Post.author são campos que precisam de resolvers porque não podem ser diretamente satisfeitos a partir de uma única fonte de dados.
Quando você executa go run github.com/99designs/gqlgen generate pela primeira vez, o gqlgen cria um arquivo schema.resolvers.go com stubs (esqueletos) de todos os resolvers. Você preencherá estes stubs com sua lógica de negócio.
Implementando Resolvers de Query
Abra graph/schema.resolvers.go e implemente os resolvers de query. Vou criar uma implementação simples com dados em memória:
package graph
import (
"context"
"fmt"
"github.com/seu-usuario/meu-graphql-api/graph/model"
)
// Dados em memória para este exemplo
var (
users = map[string]*model.User{
"user1": {
ID: "user1",
Name: "João Silva",
Email: "joao@example.com",
},
}
posts = map[string]*model.Post{
"post1": {
ID: "post1",
Title: "Introdução a GraphQL",
Content: "GraphQL é uma linguagem de query...",
AuthorID: "user1",
CreatedAt: "2024-01-15",
},
}
)
// Post resolve o campo Query.post
func (r *queryResolver) Post(ctx context.Context, id string) (*model.Post, error) {
post, exists := posts[id]
if !exists {
return nil, fmt.Errorf("post não encontrado")
}
return post, nil
}
// User resolve o campo Query.user
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
user, exists := users[id]
if !exists {
return nil, fmt.Errorf("usuário não encontrado")
}
return user, nil
}
// AllPosts resolve o campo Query.allPosts
func (r *queryResolver) AllPosts(ctx context.Context) ([]*model.Post, error) {
var result []*model.Post
for _, post := range posts {
result = append(result, post)
}
return result, nil
}
Note que cada resolver recebe context.Context como primeiro parâmetro. Isso permite rastreamento, cancelamento de operações de longa duração e passagem de valores entre middlewares. Os retornos seguem o padrão Go: resultado, erro.
Resolvendo Campos Aninhados
GraphQL é especial porque permite campos aninhados. Se um cliente solicitar:
{
post(id: "post1") {
title
author {
name
}
}
}
O campo author dentro de Post também precisa de um resolver. Embora nosso modelo Post tenha apenas AuthorID, o schema exige um campo Author de tipo User. Vamos implementar:
// Author resolve o campo Post.author
func (r *postResolver) Author(ctx context.Context, obj *model.Post) (*model.User, error) {
user, exists := users[obj.AuthorID]
if !exists {
return nil, fmt.Errorf("autor não encontrado")
}
return user, nil
}
// Posts resolve o campo User.posts (usuário possui múltiplos posts)
func (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) {
var result []*model.Post
for _, post := range posts {
if post.AuthorID == obj.ID {
result = append(result, post)
}
}
return result, nil
}
Este é um ponto crucial: resolvers de campos aninhados recebem o objeto pai como parâmetro (obj *model.Post, obj *model.User). Isso permite resolver o campo usando dados do pai, mantendo a grafo de dados coeso.
Implementando Mutations
Mutations alteram dados. Implementamos seguindo o mesmo padrão:
// CreatePost resolve o campo Mutation.createPost
func (r *mutationResolver) CreatePost(
ctx context.Context,
input model.CreatePostInput,
) (*model.Post, error) {
// Gerar um novo ID (em produção, use UUID ou banco de dados)
newID := fmt.Sprintf("post%d", len(posts)+1)
newPost := &model.Post{
ID: newID,
Title: input.Title,
Content: input.Content,
AuthorID: input.AuthorID,
CreatedAt: "2024-01-15", // Em produção, use time.Now()
}
posts[newID] = newPost
return newPost, nil
}
// UpdatePost resolve o campo Mutation.updatePost
func (r *mutationResolver) UpdatePost(
ctx context.Context,
id string,
input model.UpdatePostInput,
) (*model.Post, error) {
post, exists := posts[id]
if !exists {
return nil, fmt.Errorf("post não encontrado")
}
if input.Title != nil {
post.Title = *input.Title
}
if input.Content != nil {
post.Content = *input.Content
}
return post, nil
}
Note que UpdatePostInput utiliza ponteiros (*string) porque os campos são opcionais. Um ponteiro nil significa que o campo não foi fornecido, então não deve ser atualizado.
Executando e Testando sua API GraphQL
Configurando o Servidor HTTP
Crie um arquivo server.go na raiz do projeto que inicia o servidor HTTP:
package main
import (
"log"
"net/http"
"os"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/seu-usuario/meu-graphql-api/graph"
"github.com/seu-usuario/meu-graphql-api/graph/generated"
)
const defaultPort = "8080"
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{
Resolvers: &graph.Resolver{},
}))
http.Handle("/query", srv)
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Este código:
1. Cria um ExecutableSchema — a representação interna do gqlgen de seu schema e resolvers
2. Monta o handler de GraphQL em /query
3. Monta o GraphQL Playground (ferramenta visual para testar queries) em /
4. Inicia o servidor na porta 8080
Testando com GraphQL Playground
Execute o servidor:
go run server.go
Abra http://localhost:8080 em seu navegador. Você verá o GraphQL Playground. Teste uma query simples:
query {
allPosts {
id
title
author {
name
email
}
}
}
Teste uma mutation:
mutation {
createPost(input: {
title: "Novo Post"
content: "Conteúdo do novo post"
authorID: "user1"
}) {
id
title
createdAt
}
}
O GraphQL Playground fornece autocompletar (Ctrl+Space), validação em tempo real e documentação integrada do seu schema. Esta é uma ferramenta poderosa para desenvolvimento.
Adicionando Validação e Tratamento de Erros
Em produção, adicione validação robusta. Modifique seu CreatePost:
func (r *mutationResolver) CreatePost(
ctx context.Context,
input model.CreatePostInput,
) (*model.Post, error) {
// Validar entrada
if input.Title == "" {
return nil, fmt.Errorf("título não pode estar vazio")
}
if len(input.Title) > 200 {
return nil, fmt.Errorf("título não pode exceder 200 caracteres")
}
if input.Content == "" {
return nil, fmt.Errorf("conteúdo não pode estar vazio")
}
// Verificar se o autor existe
if _, exists := users[input.AuthorID]; !exists {
return nil, fmt.Errorf("autor com ID %s não encontrado", input.AuthorID)
}
newID := fmt.Sprintf("post%d", len(posts)+1)
newPost := &model.Post{
ID: newID,
Title: input.Title,
Content: input.Content,
AuthorID: input.AuthorID,
CreatedAt: "2024-01-15",
}
posts[newID] = newPost
return newPost, nil
}
Erros retornados dos resolvers são automaticamente formatados como erros GraphQL e retornados ao cliente com status HTTP 200 e um campo errors na resposta JSON.
Padrões Avançados e Boas Práticas
Injeção de Dependência
Em aplicações reais, seus resolvers precisam acessar banco de dados, cache, APIs externas, etc. Use injeção de dependência através do Resolver raiz:
// graph/resolver.go
package graph
import (
"database/sql"
"log"
)
type Resolver struct {
db *sql.DB
log *log.Logger
}
func NewResolver(db *sql.DB, logger *log.Logger) *Resolver {
return &Resolver{
db: db,
log: logger,
}
}
Agora seus resolvers podem acessar r.db e r.log. No server.go:
func main() {
// ... setup db
resolver := graph.NewResolver(db, log.New(os.Stdout, "", log.LstdFlags))
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{
Resolvers: resolver,
}))
// ...
}
DataLoader para Evitar N+1 Queries
Um problema comum: quando você resolve User.posts para múltiplos usuários, executa uma query por usuário (problema N+1). DataLoader agrupa requisições:
Instale: go get github.com/graph-gophers/dataloader/v7
import "github.com/graph-gophers/dataloader/v7"
type loaders struct {
postsByAuthorID *dataloader.Loader
}
// Criar loader na inicialização
func NewLoaders(db *sql.DB) *loaders {
return &loaders{
postsByAuthorID: dataloader.NewBatchedLoader(
func(ctx context.Context, ids []string) []*dataloader.Result {
// Buscar todos os posts destes autores em uma única query
results := make([]*dataloader.Result, len(ids))
// ... implementar lógica batch
return results
},
),
}
}
Use no resolver:
func (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) {
// Usar loader em vez de query individual
return r.postsByAuthorIDLoader.Load(ctx, obj.ID)
}
Middleware para Logging, Autenticação e Rate Limiting
O gqlgen permite adicionar middleware no handler:
srv := handler.NewDefaultServer(schema)
// Middleware de logging
srv.Use(&logging.Middleware{})
// Middleware de autenticação
srv.Use(&auth.Middleware{})
// Middleware de rate limiting
srv.Use(&ratelimit.Middleware{})
Você pode implementar estes middlewares usando a interface graphql.OperationMiddleware ou graphql.ResponseMiddleware fornecida pelo gqlgen.
Conclusão
Você aprendeu como utilizar o gqlgen para construir APIs GraphQL tipadas e robustas em Go seguindo a abordagem schema-first. Primeiro, você define o contrato da API no schema GraphQL, o que fornece clareza e documentação automática. Segundo, o gqlgen gera código tipo-seguro baseado no schema, eliminando classes inteiras de erros de tipo e mantendo cliente e servidor sincronizados. Terceiro, implementar resolvers é direto porque cada função tem uma assinatura bem-definida, parâmetros explícitos e retornos seguindo padrões Go idiomáticos — você sabe exatamente o que implementar e o resultado é código limpo, testável e manutenível.