GraphQL com gqlgen: Abordagem Schema-First, Resolvers e DataLoader
Como construir uma API GraphQL com gqlgen na abordagem schema-first, com resolvers e DataLoader para evitar o problema N+1.
Publicada em · 6 min de leitura
GraphQL com gqlgen: Abordagem Schema-First, Resolvers e DataLoader
Enquanto arquiteturas REST operam sobre endpoints rígidos focados em recursos (/pedidos, /usuarios), aplicações modernas frequentemente sofrem com Overfetching (baixar dezenas de campos desnecessários pela rede) ou Underfetching (precisar disparar 5 requisições HTTP sequenciais para renderizar uma única tela). O GraphQL resolve esse problema permitindo que o cliente defina exatamente a forma e os campos dos dados que deseja receber em uma única requisição.
No ecossistema Go, a biblioteca padrão absoluta para construção de servidores GraphQL é o gqlgen (github.com/99designs/gqlgen). Ao contrário de bibliotecas em outras linguagens que dependem de reflexão dinâmica e anotações pesadas em runtime, o gqlgen segue a filosofia Schema-First: você escreve o contrato estrito em um arquivo .graphqls, e o compilador gera código Go estático, com zero-reflection e 100% de type-safety.
1. O Pipeline do Compilador gqlgen
O gqlgen atua como um gerador de código baseado no schema da sua aplicação, desacoplando o parsing e validação do GraphQL da implementação das suas regras de negócio:
Componentes Gerados:
graph/schema.graphqls: O contrato universal escrito na Schema Definition Language (SDL) do GraphQL.gqlgen.yml: Arquivo de configuração que controla o mapeamento de pacotes, models e nomes de arquivos.graph/model/models_gen.go: Structs Go geradas automaticamente a partir dos tipos do schema (Inputs, Enums, Payloads).graph/generated/generated.go: Motor interno type-safe de parsing, validação e execução de queries.graph/schema.resolvers.go: O único arquivo onde você, desenvolvedor, escreve código Go com a lógica de negócio dos resolvers.
2. Definindo o Schema (schema.graphqls)
Vamos modelar um domínio de e-commerce contendo User e Order:
type User {
id: ID!
name: String!
email: String!
orders: [Order!]! # Relacionamento 1:N
createdAt: String!
}
type Order {
id: ID!
userId: ID!
totalAmount: Float!
status: OrderStatus!
createdAt: String!
}
enum OrderStatus {
PENDING
PAID
CANCELLED
}
input CreateUserInput {
name: String!
email: String!
}
input CreateOrderInput {
userId: ID!
totalAmount: Float!
}
type Query {
user(id: ID!): User
users: [User!]!
orders(userId: ID!): [Order!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
createOrder(input: CreateOrderInput!): Order!
}
3. Configurando o Mapeamento (gqlgen.yml)
O gqlgen.yml permite reaproveitar modelos de domínio existentes em vez de gerar novas structs para tudo:
schema:
- "graph/*.graphqls"
exec:
filename: graph/generated/generated.go
package: generated
model:
filename: graph/model/models_gen.go
package: model
resolver:
layout: follow-schema
dir: graph
package: graph
# Instruímos o gqlgen a NÃO resolver orders automaticamente na query pai,
# forçando a criação de um Field Resolver dedicado
models:
User:
fields:
orders:
resolver: true
Após configurar, execute no terminal:
go run github.com/99designs/gqlgen generate
4. Implementando Resolvers
O gqlgen gera as interfaces necessárias em schema.resolvers.go. Veja a implementação dos resolvers de Query, Mutation e do Field Resolver relacional:
package graph
import (
"context"
"fmt"
"time"
"meu-projeto/graph/model"
)
type Resolver struct {
// Injeção de dependências (repositórios, use cases, DB pools)
}
// CreateUser é o resolver da Mutation createUser
func (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.User, error) {
user := &model.User{
ID: fmt.Sprintf("usr_%d", time.Now().UnixNano()),
Name: input.Name,
Email: input.Email,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
// Persiste no banco de dados via repositório...
return user, nil
}
// User é o resolver da Query user(id)
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
// Busca usuário por ID no repositório...
return &model.User{
ID: id,
Name: "Alan Turing",
Email: "alan@turing.org",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}, nil
}
// Orders é o Field Resolver executado quando o cliente solicita o campo 'orders' dentro de User
func (r *userResolver) Orders(ctx context.Context, obj *model.User) ([]*model.Order, error) {
// AVISO: Se implementado com consulta direta ao banco, causa o Problema do N+1!
// Veja a seção 5 abaixo para a solução correta com DataLoader.
return GetOrdersLoader(ctx).Load(obj.ID)
}
5. O Problema do N+1 e a Solução com DataLoader
Quando um cliente executa a query:
query {
users {
id
name
orders {
id
totalAmount
}
}
}
Se a listagem inicial retornar 100 usuários, a engine do GraphQL executará o método Orders(ctx, obj) 100 vezes individualmente, disparando 100 queries separadas no banco de dados (SELECT * FROM orders WHERE user_id = ?).
Como o DataLoader Funciona:
- Cada chamada
Orders(ctx, user)não acessa a rede imediatamente; ela registra ouser.IDem uma fila em memória e aguarda o próximo tick de execução. - O DataLoader consolida todas as chaves solicitadas no mesmo micro-ciclo de I/O em um único slice de IDs (
[]string{"usr_1", "usr_2", "usr_3", ...}). - Uma única Batch Function é disparada no banco de dados: $$\text{SELECT * FROM orders WHERE user_id IN (‘usr_1’, ‘usr_2’, …);}$$
- O resultado é distribuído em memória de volta para as promises/canais correspondentes de cada resolver.
Implementação de um DataLoader em Go
Utilizando o pacote padrão github.com/vikstrous/dataloadgen:
package dataloader
import (
"context"
"time"
"meu-projeto/graph/model"
"github.com/vikstrous/dataloadgen"
)
type LoaderKey struct{}
type OrderBatcher struct {
// Injeção do repositório ou *sql.DB / pgxpool
}
// BatchGetOrders busca pedidos de múltiplos usuários em uma única query SQL IN
func (b *OrderBatcher) BatchGetOrders(ctx context.Context, userIDs []string) ([][]*model.Order, []error) {
// 1. Executa: SELECT * FROM orders WHERE user_id = ANY($1)
// 2. Mapeia as linhas retornadas para um mapa: map[string][]*model.Order
ordersByUserID := make(map[string][]*model.Order)
// 3. Monta o slice de resposta estritamente na mesma ordem dos userIDs recebidos
result := make([][]*model.Order, len(userIDs))
for i, id := range userIDs {
result[i] = ordersByUserID[id]
}
return result, nil
}
// Middleware para instanciar e anexar DataLoaders por requisição
func Middleware(batcher *OrderBatcher) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
loader := dataloadgen.NewLoader(
batcher.BatchGetOrders,
dataloadgen.WithWait(2*time.Millisecond), // Janela de espera para acumular batch
dataloadgen.WithBatchCapacity(100), // Tamanho máximo do lote
)
ctx := context.WithValue(r.Context(), LoaderKey{}, loader)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// For recupera o loader da requisição atual
func For(ctx context.Context) *dataloadgen.Loader[string, []*model.Order] {
return ctx.Value(LoaderKey{}).(*dataloadgen.Loader[string, []*model.Order])
}
6. Autenticação e Diretivas Customizadas
No GraphQL, diretivas customizadas permitem aplicar regras de autorização de forma declarativa diretamente no schema .graphqls:
directive @hasRole(role: Role!) on FIELD_DEFINITION
enum Role {
ADMIN
USER
}
type Mutation {
deleteUser(id: ID!): Boolean! @hasRole(role: ADMIN)
}
Configurando a diretiva no runtime do gqlgen:
config := generated.Config{Resolvers: &graph.Resolver{}}
config.Directives.HasRole = func(ctx context.Context, obj any, next graphql.Resolver, role model.Role) (any, error) {
// Lê o usuário autenticado injetado pelo Middleware de JWT
claims, ok := middleware.UserFromContext(ctx)
if !ok {
return nil, fmt.Errorf("acesso negado: não autenticado")
}
if !slices.Contains(claims.Roles, string(role)) {
return nil, fmt.Errorf("acesso proibido: exige permissão %s", role)
}
return next(ctx)
}
7. Servindo o GraphQL Server e o Playground
A inicialização do servidor integra o handler do GraphQL com a interface interativa do GraphQL Playground:
package main
import (
"log"
"net/http"
"meu-projeto/graph"
"meu-projeto/graph/generated"
"meu-projeto/internal/middleware"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
)
func main() {
srv := handler.NewDefaultServer(
generated.NewExecutableSchema(
generated.Config{Resolvers: &graph.Resolver{}},
),
)
mux := http.NewServeMux()
// 1. Interface Web do Playground para desenvolvimento
mux.Handle("/", playground.Handler("GraphQL Explorer", "/query"))
// 2. Endpoint de execução das queries GraphQL protegido por middlewares
pipeline := middleware.Chain(
middleware.RequestID(),
middleware.Logger(logger),
middleware.Authenticate(tokenManager), // Lê JWT do header Authorization
)
mux.Handle("POST /query", pipeline(srv))
log.Println("Servidor GraphQL rodando em http://localhost:8080/")
http.ListenAndServe(":8080", mux)
}
8. Boas Práticas e Segurança em Produção
- Proteção Contra DoS por Complexidade e Profundidade:
Um cliente malicioso pode enviar queries recursivas infinitas (
user { orders { user { orders { ... } } } }). Use plugins dogqlgenpara fixar limites estritos:srv.Use(extension.FixedComplexityLimit(200)) - Disable Introspection em Produção: Em ambientes de produção pública, desative a introspecção de schema para evitar que agentes externos façam engenharia reversa completa do seu grafo de dados.
- Escopo dos DataLoaders:
Nunca compartilhe uma instância de
DataLoaderglobalmente entre requisições concorrentes de usuários diferentes. Cada requisição HTTP deve instanciar seu próprio DataLoader isolado via Middleware para evitar vazamento de dados de cache entre clientes.
9. Resumo Conceitual
- O
gqlgenadota a filosofia Schema-First, gerando código estático tipado a partir de arquivos.graphqls. - Não utiliza reflexão em tempo de execução, garantindo desempenho de latência comparável a APIs REST puras em Go.
- Resolvers de campo relacionais executam lazy-loading e exigem DataLoaders para eliminar o problema do N+1.
- Cada requisição HTTP deve receber sua própria instância de DataLoader via
context.Context. - Proteja mutações e queries sensíveis usando diretivas customizadas (
@hasRole) combinadas com Autenticação e JWT e Middlewares em Go. - Monitore a complexidade do grafo com limites de profundidade para garantir a resiliência do microsserviço.