go-graphql — скилл для Claude Code: что делает и как установить
Скилл помогает строить GraphQL-серверы на Go с помощью gqlgen. Он предотвращает проблему N+1, задаёт лимиты сложности запросов и организует тонкие резолверы.
npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a claude-codeЧто делает go-graphql
Скилл направляет агента при разработке API на Go через библиотеку gqlgen. Вся работа строится по подходу schema-first: схема .graphql остаётся главным источником истины, а доменные типы привязываются в gqlgen.yml. Инструкция требует держать резолверы тонкими. Они передают выполнение в сервисы и не содержат SQL или бизнес-логику.
Для борьбы с проблемой N+1 запросов скилл настраивает пакетную загрузку через dataloadgen. Загрузчики создаются строго на каждый HTTP-запрос внутри middleware. Общий кэш ведёт к утечкам данных между пользователями. Защита от перегрузок опирается на лимиты сложности (FixedComplexityLimit) и обязательную пагинацию списков.
Для работы требуется Go toolchain и модуль gqlgen. Агенту открыт доступ к вызовам go, gofmt и gqlgen. Скилл не рассчитан на REST или gRPC.
Какие задачи решает
- Устранить проблему N+1 в резолверах списков с помощью dataloadgen
- Настроить генерацию кода и привязку доменных структур через gqlgen.yml
- Ограничить сложность и глубину входящих GraphQL-запросов
- Изолировать код резолверов от прямого выполнения SQL-запросов
- Скрыть внутренние ошибки сервера через SetErrorPresenter
Примеры запросов агенту
- Настрой gqlgen по схеме и свяжи структуры в gqlgen.yml
- Добавь dataloader через dataloadgen для поля Customer в orderResolver, чтобы убрать N+1
- Настрой FixedComplexityLimit и обработчик ошибок SetErrorPresenter для GraphQL-сервера
Как установить go-graphql
1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a claude-code2Вручную: положить файлы
~/.claude/skills/go-graphql/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/go-graphql/
1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a codex2Вручную: положить файлы
.agents/skills/go-graphql/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a cursor2Вручную: положить файлы
.cursor/skills/go-graphql/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install eduardo-sl/go-agent-skills go-graphql2Вручную: положить файлы
.github/skills/go-graphql/SKILL.md1Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a gemini-cli2Командой платформы
gemini skills install https://github.com/eduardo-sl/go-agent-skills --path 'skills/(architecture)/go-graphql'Вручную: положить файлы
.devin/skills/go-graphql/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о go-graphql
Как установить go-graphql в Claude Code?
Выполните команду npx skills add eduardo-sl/go-agent-skills --skill go-graphql -a claude-code. Файл навыка сохранится по пути ~/.claude/skills/go-graphql/SKILL.md.
Что делает скилл go-graphql?
Он инструктирует агента по созданию GraphQL-серверов на Go с библиотекой gqlgen. Скилл задаёт правила для тонких резолверов, пакетной загрузки через dataloader, ограничения сложности запросов и безопасной обработки ошибок.
Почему dataloader должен создаваться на каждый запрос?
Если объявить dataloader на весь процесс, кэш начнёт сохранять выборки между запросами разных пользователей. Это приводит к утечке данных между клиентами.
Исходный текст
SKILL.md · MIT
---
name: go-graphql
description: >
Build GraphQL servers in Go with gqlgen: schema-first generation, resolver
structure, the N+1 problem and dataloaders, complexity and depth limits,
error presentation, field-level authorization, and testing resolvers. Use
when implementing or reviewing a GraphQL API, when a query fans out into
hundreds of database calls, or when deciding what a resolver may expose.
Trigger examples: "GraphQL", "gqlgen", "resolver", "N+1 queries",
"dataloader", "query complexity limit", "GraphQL schema".
Not for: REST and OpenAPI (go-openapi), gRPC (go-grpc), general HTTP
middleware and shutdown (go-api-design).
user-invocable: true
license: MIT
compatibility: Designed for Claude Code or similar AI coding agents working on Go projects. Requires the Go toolchain. gqlgen is installed as a module tool.
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(gofmt:*) Bash(gqlgen:*)
metadata:
author: eduardo-sl
version: "1.0.3"
---
# Go GraphQL
GraphQL moves query planning to the client. That is the feature and the
danger: one innocuous query can become ten thousand database round trips, and
one over-permissive field can leak another tenant's data. Both are solved at
the server, not in the schema review.
Detailed reference material, loaded on demand:
- `references/errors-auth-testing.md` — error presenter, field
authorization directive, client-driven tests with query-count asserts.
Read a reference file only when the summary below is not enough.
## 1. Schema-First with gqlgen
The `.graphql` schema is the source of truth. gqlgen generates models,
resolver stubs, and the execution layer from it.
```bash
go get -tool github.com/99designs/gqlgen
go tool gqlgen init # once
go tool gqlgen generate # after every schema change
```
```yaml
# gqlgen.yml — bind generated types to your own models
models:
User:
model: github.com/myorg/app/internal/domain.User
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int64
```
Bind domain types explicitly. Left to itself gqlgen generates a parallel set
of anaemic structs, and every resolver becomes a mapping function.
Commit generated code, and fail CI when it is stale:
```bash
go tool gqlgen generate && git diff --exit-code
```
Never edit `generated.go` or `models_gen.go`. `resolver.go` and the
`*.resolvers.go` files are yours.
## 2. Resolvers Stay Thin
A resolver translates a GraphQL request into a service call. It contains no
business logic and no SQL.
```go
func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
u, err := r.users.Find(ctx, id)
if errors.Is(err, domain.ErrNotFound) {
return nil, nil // nullable field: absent, not an error
}
if err != nil {
return nil, fmt.Errorf("find user %s: %w", id, err)
}
return u, nil
}
```
Inject dependencies through the `Resolver` struct, never through package
globals:
```go
type Resolver struct {
users UserService
orders OrderService
loader *Loaders
}
```
Always propagate `ctx`. It carries the request deadline, the authenticated
principal, and the per-request dataloaders.
## 3. The N+1 Problem — the one that matters
A field resolver on a list type runs once per element.
```go
// ❌ 1 query for the orders, then N queries for the users
func (r *orderResolver) Customer(ctx context.Context, obj *domain.Order) (*domain.User, error) {
return r.users.Find(ctx, obj.CustomerID)
}
```
Batch with a dataloader. It collects the keys requested within a short window
and issues one query.
```go
import "github.com/vikstrous/dataloadgen"
type Loaders struct {
UserByID *dataloadgen.Loader[string, *domain.User]
}
func NewLoaders(s UserService) *Loaders {
return &Loaders{
UserByID: dataloadgen.NewLoader(func(ctx context.Context, ids []string) ([]*domain.User, []error) {
return s.FindMany(ctx, ids) // ONE query for all ids
}, dataloadgen.WithWait(time.Millisecond)),
}
}
// ✅ 1 query for the orders, 1 for all customers
func (r *orderResolver) Customer(ctx context.Context, obj *domain.Order) (*domain.User, error) {
return loadersFrom(ctx).UserByID.Load(ctx, obj.CustomerID)
}
```
Loaders are **per request**, installed by middleware. A process-wide loader
caches across users and leaks data between them.
```go
func withLoaders(svc UserService, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), loadersKey{}, NewLoaders(svc))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
The batch function must return results **in the order of the keys it was
given**, with a nil entry and an error per missing key. Returning a shorter
slice silently misaligns every result.
## 4. Bound Every Query
A public GraphQL endpoint without limits is a denial-of-service endpoint.
```go
srv := handler.New(generated.NewExecutableSchema(cfg))
srv.AddTransport(transport.POST{})
srv.SetQueryCache(lru.New[*ast.QueryDocument](1000))
srv.Use(extension.FixedComplexityLimit(300))
srv.Use(extension.AutomaticPersistedQuery{Cache: lru.New[string](100)})
```
- **Complexity limit** — assign a cost per field, higher for list fields with
a large `first`. Start at a number your slowest legitimate query fits under,
then measure.
- **Depth** — recursive types (`user { orders { customer { orders ... } } }`)
must be bounded. gqlgen has no built-in depth limit; enforce it in an
operation middleware.
- **Pagination is mandatory** on every list field. A field returning an
unbounded list is a schema bug.
- **Introspection** is only enabled if you install `extension.Introspection`.
Do not install it in production, or gate it behind an authenticated role.
- **Persisted queries** let a public client send a hash instead of a document,
so the server executes only queries you shipped.
Set `srv.AroundOperations` to enforce a per-operation timeout, and always run
behind an `http.Server` with `ReadTimeout` and `WriteTimeout` set.
## 5. Errors
GraphQL returns 200 with an `errors` array. Never leak internals into it.
Install `srv.SetErrorPresenter`: map known domain errors (validation,
not found) to their message plus a stable `extensions.code`, and log
everything else server-side while returning `internal server error` with
code `INTERNAL`. Example: `references/errors-auth-testing.md`.
Use `srv.SetRecoverFunc` to convert a resolver panic into an error instead of
killing the connection, and log it with the stack.
Remember the nullability rule: an error on a non-null field nulls out its
nearest nullable ancestor. Make a field non-null only when it can never
legitimately be absent.
## 6. Authorization Belongs on the Field
Object-level checks are not enough — a client can reach an object through
several paths.
Declare a `@hasRole` (or similar) directive on sensitive fields and
implement it as a check against the principal in the context; return
`access denied` without detail. Example: `references/errors-auth-testing.md`.
Authenticate in HTTP middleware, before the GraphQL handler. Authorize in the
directive or the resolver, using the principal from the context — never from
a query argument.
## 7. Testing
Drive the real schema through gqlgen's test `client` and assert on the
decoded response. For every resolver behind a dataloader, also assert the
query count — that is the only way an N+1 regression fails a build rather
than a dashboard. Examples: `references/errors-auth-testing.md`.
## Verification Checklist
1. Schema is the source of truth; generated files are committed and CI-checked
2. Generated types bind to domain models via `gqlgen.yml`
3. Resolvers contain no business logic and always propagate `ctx`
4. Every list-field resolver that fetches by ID goes through a dataloader
5. Dataloaders are constructed per request, never shared across requests
6. Batch functions return one result per key, in key order
7. A complexity limit and a depth bound are configured and tested
8. Every list field is paginated
9. Introspection is disabled or role-gated in production
10. An error presenter strips internal errors; a recover func is installed
11. Authorization is enforced per field, from the context principal
12. A test asserts the query count for at least one batched field
© eduardo-sl · MIT · eduardo-sl/go-agent-skills