GraphQL vs REST in Go: What to Choose in 2025

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

GraphQL vs REST in Go: A Clash of Titans, or How to Avoid a Wrong Choice

\n\n

If you've been writing Go for at least six months, you've likely encountered the eternal debate: REST or GraphQL? One is the good old reliable "workhorse," the other is a trendy "sports car" with a ton of features. But in the world of Go, where performance and simplicity are a religion, the choice isn't always obvious.

\n\n

In this article, we won't just list the pros and cons. We'll write real Go code, measure execution time, and analyze when GraphQL is a shot in the foot, and when it's a cheat code to efficient APIs. Let's go!

\n\n

REST in Go: Stability and Predictability

\n\n

REST is like an old, reliable hammer. It doesn't shine, but it drives nails perfectly. In Go, REST APIs are written using net/http or lightweight frameworks like gin and chi. Let's look at a simple example:

\n\n
package main\n\nimport (\n    "encoding/json"\n    "net/http"\n)\n\ntype User struct {\n    ID   int    `json:"id"`\n    Name string `json:"name"`\n}\n\nfunc getUserHandler(w http.ResponseWriter, r *http.Request) {\n    user := User{ID: 1, Name: "Alexey"}\n    w.Header().Set("Content-Type", "application/json")\n    json.NewEncoder(w).Encode(user)\n}\n\nfunc main() {\n    http.HandleFunc("/api/user", getUserHandler)\n    http.ListenAndServe(":8080", nil)\n}
\n\n

Nice? Yes. Simple? Very. But what if the frontend only needs the user's name, and we're sending the entire object? In REST, this is called over-fetching — an excess of data. And in Go, where we're used to controlling every byte, this can be annoying.

\n\n

The N+1 Problem in REST

\n\n

Let's say we have a list of orders, and for each one we need the user's name. In REST, we either make N requests (slow) or create a separate endpoint /api/orders-with-users (a ton of endpoints). Go handles this with join queries in the database, but at the API level, it still feels clunky.

\n\n

GraphQL in Go: Power and Flexibility

\n\n

GraphQL is like a Swiss Army knife: one endpoint, and the client decides what data it needs. In Go, there's the gqlgen framework (from 99designs) — it generates code from a schema, which is very much in the spirit of Go (less boilerplate, more action).

\n\n
// schema.graphql\ntype Query {\n    user(id: Int!): User\n}\n\ntype User {\n    id: Int!\n    name: String!\n    orders: [Order!]!\n}\n\ntype Order {\n    id: Int!\n    total: Float!\n}
\n\n

Now the Go implementation:

\n\n
package graph\n\nimport (\n    "context"\n    "fmt"\n)\n\ntype Resolver struct{}\n\nfunc (r *Resolver) User(ctx context.Context, args struct{ ID int }) (*User, error) {\n    // In reality — a database query\n    return &User{\n        ID:   args.ID,\n        Name: "Maria",\n        Orders: []*Order{\n            {ID: 1, Total: 250.50},\n        },\n    }, nil\n}
\n\n

The client requests only name — it gets only name. No over-fetching. But there's a catch: if the client requests orders for 10 users, and each has 50 orders — the database could be brought to its knees. This is called the N+1 problem in GraphQL, and in Go, it's solved with DataLoaders.

\n\n

DataLoader — The N+1 Savior

\n\n
import "github.com/graph-gophers/dataloader"\n\ntype UserLoader struct {\n    *dataloader.Loader\n}\n\nfunc NewUserLoader() *UserLoader {\n    return &UserLoader{\n        Loader: dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {\n            // Batch database queries\n            userIDs := make([]int, len(keys))\n            for i, key := range keys {\n                userIDs[i] = key.Int()\n            }\n            users := batchFetchUsers(userIDs) // One SQL query with WHERE IN\n            results := make([]*dataloader.Result, len(ke                

Blogs

Book Recommendations