Go

O que Todo Dev Deve Saber sobre GraphQL em Go com gqlgen: Schema-First e Resolvers Tipados

18 min de leitura

O que Todo Dev Deve Saber sobre GraphQL em Go com gqlgen: Schema-First e Resolvers Tipados

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.

<h2>Introdução: Por que GraphQL em Go com gqlgen?</h2>

<p>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 <strong>schema-first</strong>, 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.</p>

<p>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.</p>

<h2>Configuração do Projeto e Instalação</h2>

<h3>Preparando o Ambiente</h3>

<p>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.</p>

<pre><code class="language-bash">mkdir meu-graphql-api

cd meu-graphql-api

go mod init github.com/seu-usuario/meu-graphql-api</code></pre>

<p>Agora instale o gqlgen. A forma recomendada é adicioná-lo como uma dependência indireta através de um arquivo <code>tools.go</code>:</p>

<pre><code class="language-go">// tools.go

//go:build tools

// +build tools

package main

import (

_ &quot;github.com/99designs/gqlgen&quot;

)</code></pre>

<p>Execute <code>go mod tidy</code> para baixar a dependência. Depois, crie o arquivo de configuração:</p>

<pre><code class="language-bash">go run github.com/99designs/gqlgen init</code></pre>

<p>Este comando cria:</p>

<ul>

<li><code>gqlgen.yml</code> — configuração do projeto</li>

<li><code>graph/schema.graphqls</code> — seu schema GraphQL</li>

<li><code>graph/model/models_gen.go</code> — modelos gerados</li>

<li>Pastas de suporte como <code>graph/resolver.go</code></li>

</ul>

<h3>Estrutura do Projeto</h3>

<p>A estrutura final fica assim:</p>

<pre><code>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</code></pre>

<p>O arquivo <code>gqlgen.yml</code> é 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.</p>

<h2>Definindo o Schema GraphQL (Schema-First)</h2>

<h3>Conceitos Fundamentais do Schema</h3>

<p>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.</p>

<p>Abra o arquivo <code>graph/schema.graphqls</code> e defina um schema simples mas realista. Vou criar um exemplo de um sistema de blog:</p>

<pre><code class="language-graphql">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

}</code></pre>

<p>Este schema define dois tipos principais (<code>Post</code> e <code>User</code>), as operações de leitura (<code>Query</code>), as operações de escrita (<code>Mutation</code>), e dois tipos de entrada (<code>CreatePostInput</code> e <code>UpdatePostInput</code>). A exclamação <code>!</code> indica que o campo é obrigatório (não-nulo). Colchetes <code>[]</code> indicam listas.</p>

<h3>Gerando Código a partir do Schema</h3>

<p>Após definir o schema, execute:</p>

<pre><code class="language-bash">go run github.com/99designs/gqlgen generate</code></pre>

<p>O gqlgen analisa o schema, verifica as dependências e gera código tipo-seguro. Ele cria:</p>

<ul>

<li>Interfaces Go para todos os tipos (<code>Mutation</code>, <code>Query</code>, <code>Post</code>, etc.)</li>

<li>Modelos de dados estruturados</li>

<li>Um arquivo <code>schema.resolvers.go</code> onde você implementa os resolvers</li>

</ul>

<p>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.</p>

<h2>Implementando Resolvers Tipados</h2>

<h3>O que é um Resolver?</h3>

<p>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, <code>Query.post</code>, <code>Query.user</code>, <code>Mutation.createPost</code> e o campo <code>Post.author</code> são campos que precisam de resolvers porque não podem ser diretamente satisfeitos a partir de uma única fonte de dados.</p>

<p>Quando você executa <code>go run github.com/99designs/gqlgen generate</code> pela primeira vez, o gqlgen cria um arquivo <code>schema.resolvers.go</code> com stubs (esqueletos) de todos os resolvers. Você preencherá estes stubs com sua lógica de negócio.</p>

<h3>Implementando Resolvers de Query</h3>

<p>Abra <code>graph/schema.resolvers.go</code> e implemente os resolvers de query. Vou criar uma implementação simples com dados em memória:</p>

<pre><code class="language-go">package graph

import (

&quot;context&quot;

&quot;fmt&quot;

&quot;github.com/seu-usuario/meu-graphql-api/graph/model&quot;

)

// Dados em memória para este exemplo

var (

users = map[string]*model.User{

&quot;user1&quot;: {

ID: &quot;user1&quot;,

Name: &quot;João Silva&quot;,

Email: &quot;joao@example.com&quot;,

},

}

posts = map[string]*model.Post{

&quot;post1&quot;: {

ID: &quot;post1&quot;,

Title: &quot;Introdução a GraphQL&quot;,

Content: &quot;GraphQL é uma linguagem de query...&quot;,

AuthorID: &quot;user1&quot;,

CreatedAt: &quot;2024-01-15&quot;,

},

}

)

// 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(&quot;post não encontrado&quot;)

}

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(&quot;usuário não encontrado&quot;)

}

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

}</code></pre>

<p>Note que cada resolver recebe <code>context.Context</code> 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.</p>

<h3>Resolvendo Campos Aninhados</h3>

<p>GraphQL é especial porque permite campos aninhados. Se um cliente solicitar:</p>

<pre><code class="language-graphql">{

post(id: &quot;post1&quot;) {

title

author {

name

}

}

}</code></pre>

<p>O campo <code>author</code> dentro de <code>Post</code> também precisa de um resolver. Embora nosso modelo <code>Post</code> tenha apenas <code>AuthorID</code>, o schema exige um campo <code>Author</code> de tipo <code>User</code>. Vamos implementar:</p>

<pre><code class="language-go">// 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(&quot;autor não encontrado&quot;)

}

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

}</code></pre>

<p>Este é um ponto crucial: resolvers de campos aninhados recebem o objeto pai como parâmetro (<code>obj <em>model.Post</code>, <code>obj </em>model.User</code>). Isso permite resolver o campo usando dados do pai, mantendo a grafo de dados coeso.</p>

<h3>Implementando Mutations</h3>

<p>Mutations alteram dados. Implementamos seguindo o mesmo padrão:</p>

<pre><code class="language-go">// 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(&quot;post%d&quot;, len(posts)+1)

newPost := &amp;model.Post{

ID: newID,

Title: input.Title,

Content: input.Content,

AuthorID: input.AuthorID,

CreatedAt: &quot;2024-01-15&quot;, // 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(&quot;post não encontrado&quot;)

}

if input.Title != nil {

post.Title = *input.Title

}

if input.Content != nil {

post.Content = *input.Content

}

return post, nil

}</code></pre>

<p>Note que <code>UpdatePostInput</code> utiliza ponteiros (<code>*string</code>) porque os campos são opcionais. Um ponteiro <code>nil</code> significa que o campo não foi fornecido, então não deve ser atualizado.</p>

<h2>Executando e Testando sua API GraphQL</h2>

<h3>Configurando o Servidor HTTP</h3>

<p>Crie um arquivo <code>server.go</code> na raiz do projeto que inicia o servidor HTTP:</p>

<pre><code class="language-go">package main

import (

&quot;log&quot;

&quot;net/http&quot;

&quot;os&quot;

&quot;github.com/99designs/gqlgen/graphql/handler&quot;

&quot;github.com/99designs/gqlgen/graphql/playground&quot;

&quot;github.com/seu-usuario/meu-graphql-api/graph&quot;

&quot;github.com/seu-usuario/meu-graphql-api/graph/generated&quot;

)

const defaultPort = &quot;8080&quot;

func main() {

port := os.Getenv(&quot;PORT&quot;)

if port == &quot;&quot; {

port = defaultPort

}

srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{

Resolvers: &amp;graph.Resolver{},

}))

http.Handle(&quot;/query&quot;, srv)

http.Handle(&quot;/&quot;, playground.Handler(&quot;GraphQL playground&quot;, &quot;/query&quot;))

log.Printf(&quot;connect to http://localhost:%s/ for GraphQL playground&quot;, port)

log.Fatal(http.ListenAndServe(&quot;:&quot;+port, nil))

}</code></pre>

<p>Este código:</p>

<ol>

<li>Cria um <code>ExecutableSchema</code> — a representação interna do gqlgen de seu schema e resolvers</li>

<li>Monta o handler de GraphQL em <code>/query</code></li>

<li>Monta o GraphQL Playground (ferramenta visual para testar queries) em <code>/</code></li>

<li>Inicia o servidor na porta 8080</li>

</ol>

<h3>Testando com GraphQL Playground</h3>

<p>Execute o servidor:</p>

<pre><code class="language-bash">go run server.go</code></pre>

<p>Abra http://localhost:8080 em seu navegador. Você verá o GraphQL Playground. Teste uma query simples:</p>

<pre><code class="language-graphql">query {

allPosts {

id

title

author {

name

email

}

}

}</code></pre>

<p>Teste uma mutation:</p>

<pre><code class="language-graphql">mutation {

createPost(input: {

title: &quot;Novo Post&quot;

content: &quot;Conteúdo do novo post&quot;

authorID: &quot;user1&quot;

}) {

id

title

createdAt

}

}</code></pre>

<p>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.</p>

<h3>Adicionando Validação e Tratamento de Erros</h3>

<p>Em produção, adicione validação robusta. Modifique seu <code>CreatePost</code>:</p>

<pre><code class="language-go">func (r *mutationResolver) CreatePost(

ctx context.Context,

input model.CreatePostInput,

) (*model.Post, error) {

// Validar entrada

if input.Title == &quot;&quot; {

return nil, fmt.Errorf(&quot;título não pode estar vazio&quot;)

}

if len(input.Title) &gt; 200 {

return nil, fmt.Errorf(&quot;título não pode exceder 200 caracteres&quot;)

}

if input.Content == &quot;&quot; {

return nil, fmt.Errorf(&quot;conteúdo não pode estar vazio&quot;)

}

// Verificar se o autor existe

if _, exists := users[input.AuthorID]; !exists {

return nil, fmt.Errorf(&quot;autor com ID %s não encontrado&quot;, input.AuthorID)

}

newID := fmt.Sprintf(&quot;post%d&quot;, len(posts)+1)

newPost := &amp;model.Post{

ID: newID,

Title: input.Title,

Content: input.Content,

AuthorID: input.AuthorID,

CreatedAt: &quot;2024-01-15&quot;,

}

posts[newID] = newPost

return newPost, nil

}</code></pre>

<p>Erros retornados dos resolvers são automaticamente formatados como erros GraphQL e retornados ao cliente com status HTTP 200 e um campo <code>errors</code> na resposta JSON.</p>

<h2>Padrões Avançados e Boas Práticas</h2>

<h3>Injeção de Dependência</h3>

<p>Em aplicações reais, seus resolvers precisam acessar banco de dados, cache, APIs externas, etc. Use injeção de dependência através do <code>Resolver</code> raiz:</p>

<pre><code class="language-go">// graph/resolver.go

package graph

import (

&quot;database/sql&quot;

&quot;log&quot;

)

type Resolver struct {

db *sql.DB

log *log.Logger

}

func NewResolver(db sql.DB, logger log.Logger) *Resolver {

return &amp;Resolver{

db: db,

log: logger,

}

}</code></pre>

<p>Agora seus resolvers podem acessar <code>r.db</code> e <code>r.log</code>. No <code>server.go</code>:</p>

<pre><code class="language-go">func main() {

// ... setup db

resolver := graph.NewResolver(db, log.New(os.Stdout, &quot;&quot;, log.LstdFlags))

srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{

Resolvers: resolver,

}))

// ...

}</code></pre>

<h3>DataLoader para Evitar N+1 Queries</h3>

<p>Um problema comum: quando você resolve <code>User.posts</code> para múltiplos usuários, executa uma query por usuário (problema N+1). DataLoader agrupa requisições:</p>

<p>Instale: <code>go get github.com/graph-gophers/dataloader/v7</code></p>

<pre><code class="language-go">import &quot;github.com/graph-gophers/dataloader/v7&quot;

type loaders struct {

postsByAuthorID *dataloader.Loader

}

// Criar loader na inicialização

func NewLoaders(db sql.DB) loaders {

return &amp;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

},

),

}

}</code></pre>

<p>Use no resolver:</p>

<pre><code class="language-go">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)

}</code></pre>

<h3>Middleware para Logging, Autenticação e Rate Limiting</h3>

<p>O gqlgen permite adicionar middleware no handler:</p>

<pre><code class="language-go">srv := handler.NewDefaultServer(schema)

// Middleware de logging

srv.Use(&amp;logging.Middleware{})

// Middleware de autenticação

srv.Use(&amp;auth.Middleware{})

// Middleware de rate limiting

srv.Use(&amp;ratelimit.Middleware{})</code></pre>

<p>Você pode implementar estes middlewares usando a interface <code>graphql.OperationMiddleware</code> ou <code>graphql.ResponseMiddleware</code> fornecida pelo gqlgen.</p>

<h2>Conclusão</h2>

<p>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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://graphql.org/" target="_blank" rel="noopener noreferrer">GraphQL Official Documentation</a></li>

<li><a href="https://gqlgen.com/" target="_blank" rel="noopener noreferrer">gqlgen Official Documentation</a></li>

<li><a href="https://gqlgen.com/docs/getting-started/" target="_blank" rel="noopener noreferrer">Go GraphQL Best Practices</a></li>

<li><a href="https://github.com/graph-gophers/dataloader" target="_blank" rel="noopener noreferrer">Graph-Gophers DataLoader</a></li>

<li><a href="https://golang.org/ref/spec" target="_blank" rel="noopener noreferrer">The Go Programming Language Specification</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Middleware de Autenticação, Logging e Rate Limiting em Go na Prática
Middleware de Autenticação, Logging e Rate Limiting em Go na Prática

Introdução: Por que Middleware Importa em Go Middleware é um padrão fundament...

Guia Completo de sync.Mutex e sync.RWMutex em Go: Exclusão Mútua Explícita
Guia Completo de sync.Mutex e sync.RWMutex em Go: Exclusão Mútua Explícita

Entendendo Concorrência e a Necessidade de Sincronização A programação concor...

Dominando Channels em Go: Comunicação entre Goroutines com Segurança em Projetos Reais
Dominando Channels em Go: Comunicação entre Goroutines com Segurança em Projetos Reais

Entendendo Channels: A Espinha Dorsal da Concorrência em Go Channels são estr...