GraphQL vs REST en Go: duelo de titanes, o cómo no equivocarse al elegir
\n\nSi llevas al menos seis meses escribiendo en Go, seguramente te has topado con el debate eterno: ¿REST o GraphQL? Uno es el viejo y confiable "caballo de batalla", el otro es un "auto deportivo" moderno con un montón de opciones. Pero en el mundo de Go, donde el rendimiento y la simplicidad son una religión, la elección no siempre es obvia.
\n\nEn este artículo no solo enumeraremos ventajas y desventajas. Escribiremos código real en Go, mediremos tiempos de ejecución y analizaremos cuándo GraphQL es un tiro en el pie y cuándo es un pase directo al mundo de las API eficientes. ¡Vamos allá!
\n\nREST en Go: estabilidad y previsibilidad
\n\nREST es como un martillo viejo y probado. No brilla, pero clava los clavos perfectamente. En Go, las API REST se escriben con net/http o frameworks ligeros como gin y chi. Veamos un ejemplo simple:
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¿Bonito? Sí. ¿Simple? Mucho. Pero, ¿qué pasa si el frontend solo necesita el nombre del usuario y nosotros enviamos todo el objeto? En REST esto se llama over-fetching — exceso de datos. Y en Go, donde estamos acostumbrados a controlar cada byte, esto puede ser frustrante.
\n\nEl problema N+1 en REST
\n\nSupongamos que tenemos una lista de pedidos y para cada uno necesitamos el nombre del usuario. En REST, hacemos N consultas (lento) o creamos un endpoint separado /api/orders-with-users (montón de endpoints). Go maneja esto con consultas JOIN en la base de datos, pero a nivel de API sigue siendo tosco.
GraphQL en Go: potencia y flexibilidad
\n\nGraphQL es como una navaja suiza: un solo endpoint, y el cliente decide qué datos necesita. En Go, para esto está el framework gqlgen (de 99designs) — genera código a partir del esquema, muy en el espíritu de Go (menos código repetitivo, más acció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\nAhora la implementación en Go:
\n\npackage 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 // En la vida real — consulta a la BD\n return &User{\n ID: args.ID,\n Name: "María",\n Orders: []*Order{\n {ID: 1, Total: 250.50},\n },\n }, nil\n}\n\nEl cliente solicita solo name — recibe solo name. Sin over-fetching. Pero hay un detalle: si el cliente solicita orders para 10 usuarios, y cada uno tiene 50 pedidos — la base de datos puede caer de rodillas. Esto se llama problema N+1 en GraphQL, y en Go se resuelve con DataLoaders.
DataLoader — salvador del N+1
\n\nimport "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 // Agrupamos consultas a la BD\n userIDs := make([]int, len(keys))\n for i, key := range keys {\n userIDs[i] = key.Int()\n }\n users := batchFetchUsers(userIDs) // Una sola consulta SQL con WHERE IN\n results := make([]*dataloader.Result, len(ke