{
  "slug": "Complete-Guide--Migrating-from-REST---gRPC-to-GraphQL-in-Go-96c53025d9dc",
  "title": "Complete Guide: Migrating from REST & gRPC to GraphQL in Go",
  "subtitle": "we’ll walk through the complete process of migrating a Go application from a REST API with gRPC to a modern GraphQL Go base",
  "excerpt": "we’ll walk through the complete process of migrating a Go application from a REST API with gRPC to a modern GraphQL Go base",
  "date": "2025-12-14",
  "tags": [
    "Go",
    "API",
    "gRPC",
    "GraphQL"
  ],
  "readingTime": "6 min",
  "url": "https://medium.com/@mobinshaterian/complete-guide-migrating-from-rest-grpc-to-graphql-in-go-96c53025d9dc",
  "hero": "https://cdn-images-1.medium.com/max/800/1*f7CUhDU_ZXYRTPkXzWbDBg.png",
  "content": [
    {
      "type": "heading",
      "level": 2,
      "text": "Complete Guide: Migrating from REST & gRPC to GraphQL in Go"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Introduction"
    },
    {
      "type": "paragraph",
      "html": "In this comprehensive guide, we’ll walk through the complete process of migrating a Go application from a REST API with gRPC and JWT authentication to a modern GraphQL-based architecture. This article documents the actual migration of the <code>go-simple</code> project to <code>go-graphql</code>, covering every step, decision, and implementation detail."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*f7CUhDU_ZXYRTPkXzWbDBg.png",
      "alt": "Complete Guide: Migrating from REST & gRPC to GraphQL in Go",
      "caption": "",
      "width": 1024,
      "height": 1024
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Why Migrate to GraphQL?"
    },
    {
      "type": "paragraph",
      "html": "Before diving into the technical details, let’s understand the motivation:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Single Endpoint</strong>: Instead of managing multiple REST routes, GraphQL provides one <code>/query</code> endpoint for all operations",
        "<strong>Precise Data Fetching</strong>: Clients request exactly the fields they need, reducing over-fetching",
        "<strong>Strong Type System</strong>: GraphQL schema provides built-in documentation and validation",
        "<strong>Better Developer Experience</strong>: Interactive playground for testing queries in real-time",
        "<strong>Real-time Capabilities</strong>: Native support for subscriptions without complex polling logic",
        "<strong>Reduced Complexity</strong>: No need to maintain both REST and gRPC layers"
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Project Overview"
    },
    {
      "type": "paragraph",
      "html": "The original <code>go-simple</code> project was built with:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Gin Framework</strong> for HTTP routing",
        "<strong>gRPC</strong> for service-to-service communication",
        "<strong>JWT</strong> for authentication",
        "<strong>SQLC</strong> for type-safe database queries",
        "<strong>Redis</strong> for caching",
        "<strong>Uber Fx</strong> for dependency injection",
        "<strong>PostgreSQL</strong> for data persistence"
      ]
    },
    {
      "type": "paragraph",
      "html": "The goal was to consolidate all operations into a single GraphQL layer while maintaining the existing clean architecture."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 1: Dependency Management"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 1.1: Add GraphQL Dependencies"
    },
    {
      "type": "paragraph",
      "html": "The first major change was adding gqlgen and its ecosystem:"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "go get github.com/99designs/gqlgen@latest\ngo get github.com/vektah/gqlparser/v2\ngo get\ngithub.com/gorilla/websocket\n# For subscriptions"
    },
    {
      "type": "paragraph",
      "html": "Key additions in <code>go.mod</code>:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<code>github.com/99designs/gqlgen v0.17.84</code> - Code generation",
        "<code>github.com/vektah/gqlparser/v2 v2.5.31</code> - GraphQL parsing",
        "<code>github.com/gorilla/websocket v1.5.0</code> - WebSocket support",
        "<code>github.com/hashicorp/golang-lru/v2 v2.0.7</code> - Response caching",
        "<code>github.com/google/uuid v1.6.0</code> - ID generation"
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 1.2: Remove Legacy Dependencies"
    },
    {
      "type": "paragraph",
      "html": "Since we’re eliminating gRPC and JWT, several dependencies became obsolete:"
    },
    {
      "type": "paragraph",
      "html": "Removed:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<code>github.com/golang-jwt/jwt/v5</code> - No longer needed for authentication",
        "<code>github.com/gin-contrib/timeout</code> - Timeout handling moved to gqlgen",
        "<code>github.com/goccy/go-yaml</code> - Was used for config parsing, now handled differently",
        "<code>github.com/urfave/cli/v3</code> - CLI management not needed"
      ]
    },
    {
      "type": "paragraph",
      "html": "This cleanup was critical because some of these packages conflicted with gqlgen’s dependencies, causing parser errors like:"
    },
    {
      "type": "code",
      "lang": "solidity",
      "code": "unable to parse config: [17:3] mapping key \"filename_template\" already defined"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 1.3: Update Go Modules"
    },
    {
      "type": "paragraph",
      "html": "After adding and removing dependencies, update your module:"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "go mod tidy\ngo mod download"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 2: Project Structure Refactoring"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 2.1: New Directory Layout"
    },
    {
      "type": "paragraph",
      "html": "Transform your project structure from REST/gRPC-focused to GraphQL-focused:"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "go-graphql/├── cmd/│   └── server/│       └── main.go├── internal/│   ├── app/│   │   └── app.go\n# Fx app setup│   ├── config/│   │   └── config.go                 # Configuration│   ├── graph/│\n│   ├── schema.graphqls           # GraphQL schema definition│   │   ├── generated.go              #\nAuto-generated by gqlgen│   │   ├── model.go                  # Auto-generated models│   │   ├──\nresolver.go               # Root resolver│   │   └── resolvers/│   │       ├── resolver.go\n# Resolver setup│   │       └── product.go            # Product resolvers│   ├── product/│   │   ├──\nservice/│   │   │   └── service.go            # Business logic (unchanged)│   │   └── repository/│\n│       └── repository.go         # Data access (unchanged)│   ├── storage/│   │   └── sql/│   │\n├── db.go                 # Database connection│   │       ├── migrate/              # Migrations│\n│       └── sqlc/                 # Generated queries│   └── server/│       └── http.go\n# HTTP server setup└── schema.graphqls                   # Root schema file"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 2.2: Keep Business Logic Layers"
    },
    {
      "type": "paragraph",
      "html": "The beauty of this migration is that your existing service and repository layers remain largely unchanged. GraphQL becomes purely an API layer replacement:"
    },
    {
      "type": "paragraph",
      "html": "go"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "// Service layer stays the sametype ProductService struct {    repo *ProductRepository    cache\nCacheStore}"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func (s *ProductService) GetByID(ctx context.Context, id string) (*Product, error) {\n    // Existing implementation\n}"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "// Repository layer stays the sametype ProductRepository struct {    db *sql.DB}"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func (r *ProductRepository) FindByID(ctx context.Context, id string) (*Product, error) {\n    // Existing queries\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 3: GraphQL Schema Definition"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 3.1: Create schema.graphqls"
    },
    {
      "type": "paragraph",
      "html": "Create your GraphQL schema in the project root:"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "scalar Time"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "type Query {\n  getProduct(id: ID!): Product  listProducts(limit: Int, offset: Int): ProductList!\n}"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "type Mutation {\n  createProduct(input: CreateProductInput!): Product!  updateProduct(id: ID!, input:\n  UpdateProductInput!): Product!  deleteProduct(id: ID!): Boolean!\n}"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "type Product {\n  id: ID!\n  name: String!\n  description: String\n  price: Float!\n  createdAt: Time!\n  updatedAt: Time!\n}"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "type ProductList {\n  items: [Product!]!\n  total: Int!\n  limit: Int!\n  offset: Int!\n}"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "input CreateProductInput {\n  name: String!  description: String  price: Float!\n}"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "input UpdateProductInput {\n  name: String  description: String  price: Float\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 3.2: Map Schema to Your Domain"
    },
    {
      "type": "paragraph",
      "html": "The schema should map directly to your existing domain models:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "// Domain model (existing\n)\ntype Product struct {\n    ID string\n    Name string\n    Description string\n    Price float64\n    CreatedAt time.Time\n    UpdatedAt time.Time\n}"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "// GraphQL model (generated\n)\ntype Product struct {\n    ID string\n    `json:\"id\"`\n    Name string\n    `json:\"name\"`\n    Description *string `json:\"description\"`\n    Price float64 `json:\"price\"`\n    CreatedAt time.Time `json:\"createdAt\"`\n    UpdatedAt time.Time `json:\"updatedAt\"`\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 4: GQLGen Configuration"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 4.1: Create gqlgen.yml"
    },
    {
      "type": "paragraph",
      "html": "Create a minimal but functional configuration:"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "schema:  - schema.graphqlexec:  filename: internal/graph/generated/generated.go  package:\ngeneratedmodel:  filename: internal/graph/model/models_gen.go  package: modelresolver:  layout:\nfollow-schema  dir: internal/graph/resolvers  package: resolversmodels:  Time:    model:      -\ngo-graphql/internal/graph/model.Time"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 4.2: Generate Code"
    },
    {
      "type": "paragraph",
      "html": "Once the configuration is correct:"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "gqlgen generate"
    },
    {
      "type": "paragraph",
      "html": "This generates:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<code>internal/graph/generated/generated.go</code> - GraphQL executable schema",
        "<code>internal/graph/model/model.go</code> - GraphQL type models"
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 5: Resolver Implementation"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 5.1: Root Resolver Structure"
    },
    {
      "type": "paragraph",
      "html": "Create the root resolver that holds all dependencies:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "package resolvers// This file will not be regenerated automatically.//// It serves as dependency\ninjection for your app, add any dependencies you require// here.import product\n\"go-graphql/internal/product/service\"\ntype Resolver struct {\n    Product *product.Product\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 5.2: Resolver Adapters"
    },
    {
      "type": "paragraph",
      "html": "Create adapter resolvers that embed the root resolver:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "package resolvers// This file will be automatically regenerated based on the schema, any resolver//\nimplementations// will be copied through when generating and any unknown code will be moved to the\nend.// Code generated by github.com/99designs/gqlgen version v0.17.84import (\"context\"\n\"fmt\"\n\"go-graphql/internal/graph/generated\"\n\"go-graphql/internal/graph/model\")// Product is the resolver for the product field.\nfunc (r *queryResolver) Product(ctx context.Context, id string) (*model.Product, error) {\n    panic(fmt.Errorf(\"not implemented: Product - product\"))\n}\n// Products is the resolver for the products field.\nfunc (r *queryResolver) Products(ctx context.Context, limit *int, offset *int) ([]*model.Product,\nerror) {\n    panic(fmt.Errorf(\"not implemented: Products - products\"))\n}\n// Query returns generated.QueryResolver implementation.\nfunc (r *Resolver) Query() generated.QueryResolver {\n    return &queryResolver{\n        r\n    }\n}\ntype queryResolver struct{\n    *Resolver\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 6: HTTP Server Setup"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 6.1: Replace Gin with Standard HTTP"
    },
    {
      "type": "paragraph",
      "html": "Create a new HTTP server handler:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "package server\nimport (\n\"context\"\n\"fmt\"\n\"net/http\"\n\"go-graphql/internal/config\"\n\"go-graphql/internal/graph/generated\"\n\"go-graphql/internal/graph/resolvers\" // gqlgen generated package product\n\"go-graphql/internal/product/service\"\n\"github.com/99designs/gqlgen/graphql/handler\"\n\"github.com/99designs/gqlgen/graphql/playground\"\n\"go.uber.org/fx\"\n\"go.uber.org/zap\"\n)\ntype HTTPServer struct {\n    server *http.Server\n    config *config.Config\n    log *zap.Logger\n}\nfunc NewHTTPServer(cfg *config.Config, log *zap.Logger) *HTTPServer {\n    return &HTTPServer{\n        config: cfg, log:\n        log,\n    }\n}\n// NewGraphQLResolver wires your services into the gqlgen resolvers.\nfunc NewGraphQLResolver(productSvc *product.Product) *resolvers.Resolver {\n    return &resolvers.Resolver{\n        Product: productSvc,\n    }\n}\nfunc RegisterGraphQLRoutes(hs *HTTPServer, resolver *resolvers.Resolver, // Assuming your resolver\npackage is named 'resolvers') {\n    mux := http.NewServeMux() // Create the executable schema using the injected resolver schema :=\n    generated.NewExecutableSchema(generated.Config{\n        Resolvers: resolver\n    }) // GraphQL handler srv := handler.NewDefaultServer(schema) mux.Handle(\"/query\", srv) //\n    GraphQL Playground mux.Handle(\"/playground\", playground.Handler(\"GraphQL Playground\", \"/query\"))\n    // Health check mux.HandleFunc(\"/health\",\n    func(w http.ResponseWriter, r *http.Request) {\n        w.Header().Set(\"Content-Type\", \"application/json\") w.WriteHeader(http.StatusOK)\n        fmt.Fprint(w, `{\"status\":\"ok\"}`)\n    }) hs.server = &http.Server{\n        Addr:\n        fmt.Sprintf(\"%s:%d\", hs.config.HTTPAddress, hs.config.HTTPPort), Handler: mux,\n    }\n}\nfunc StartHTTPServer(lc fx.Lifecycle, hs *HTTPServer) {\n    lc.Append(fx.Hook{\n        OnStart: func(ctx context.Context) error {\n            go func() {\n                hs.log.Info(\"Server running\", zap.String(\"url\",\n                fmt.Sprintf(\"http://%s:%d/playground\", hs.config.HTTPAddress, hs.config.HTTPPort)),\n                )\n                if err := hs.server.ListenAndServe();\n                err != nil && err != http.ErrServerClosed {\n                    hs.log.Error(\"Server error\", zap.Error(err))\n                }\n            }\n            () return nil\n        }, OnStop: func(ctx context.Context) error {\n            hs.log.Info(\"Stopping server...\") return hs.server.Shutdown(ctx)\n        },\n    })\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 6.2: Update Uber Fx Configuration"
    },
    {
      "type": "paragraph",
      "html": "Update your app initialization:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "package app\nimport (\n\"go-graphql/internal/config\" // gqlgen generated package // your resolvers\n\"go-graphql/internal/health\"\n\"go-graphql/internal/pkg/logger\" productService \"go-graphql/internal/product/service\"\n\"go-graphql/internal/server\"\n\"go-graphql/internal/storage/cache\"\n\"go-graphql/internal/storage/sql\"\n\"go-graphql/internal/storage/sql/migrate\"\n\"go-graphql/internal/storage/sql/sqlc\"\n\"go.uber.org/fx\"\n)\nfunc NewApp() *fx.App {\n    return fx.New(fx.Provide(logger.NewLogger, config.NewConfig, sql.InitialDB, // health check\n    health.New, // server server.NewHTTPServer, // db migrate.NewRunner, sqlc.New, // cache\n    cache.NewClient, cache.NewCacheStore, // services productService.New, // GraphQL\n    server.NewGraphQLResolver,), fx.Invoke(server.RegisterGraphQLRoutes, server.StartHTTPServer, //\n    migration migrate.RunMigrations, // life cycle logger.RegisterLoggerLifecycle,),)\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phase 8: Testing & Validation"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 8.1: Query Examples"
    },
    {
      "type": "paragraph",
      "html": "Test your GraphQL API using curl:"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "# Simple query\ncurl -X POST http://localhost:4000/query \\  -H \"Content-Type: application/json\" \\  -d\n'{    \"query\": \"{ getProduct(id: \\\"1\\\") { id name price } }\"  }'# Query with variables\ncurl -X POST\nhttp://localhost:4000/query \\  -H \"Content-Type: application/json\" \\  -d\n'{    \"query\": \"query GetProduct($id: ID!) { getProduct(id: $id) { id name } }\",    \"variables\": { \"id\": \"1\" }  }'# Mutation\ncurl -X POST http://localhost:4000/query \\  -H \"Content-Type: application/json\" \\  -d '{    \"query\": \"mutation { createProduct(input: {name: \\\"Laptop\\\", price: 999.99}) { id name } }\"  }'"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Step 8.2: Using GraphQL Playground"
    },
    {
      "type": "paragraph",
      "html": "GraphQL Playground provides an interactive interface:"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "Navigate to <code><a href=\"http://localhost:4000/playground\" target=\"_blank\" rel=\"noreferrer noopener\">http://localhost:4000/playground</a></code>",
        "Write queries in the left panel",
        "View results in the right panel",
        "Use the documentation panel to explore your schema"
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Git Commits"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Git Repository"
    },
    {
      "type": "paragraph",
      "html": "<a href=\"https://github.com/mobintmu/go-graphql\" target=\"_blank\" rel=\"noreferrer noopener\">https://github.com/mobintmu/go-graphql</a>"
    }
  ]
}
