{
  "slug": "Integrating-SQLC-and-GraphQL-in-Go--A-Practical-Example-with-Product-Filtering-4722e0a53179",
  "title": "Integrating SQLC and GraphQL in Go: A Practical Example with Product Filtering",
  "subtitle": "GraphQL provides a powerful query language for clients, while SQLC generates type‑safe Go code directly from SQL queries",
  "excerpt": "GraphQL provides a powerful query language for clients, while SQLC generates type‑safe Go code directly from SQL queries",
  "date": "2025-12-30",
  "tags": [
    "Go",
    "GraphQL"
  ],
  "readingTime": "3 min",
  "url": "https://medium.com/@mobinshaterian/integrating-sqlc-and-graphql-in-go-a-practical-example-with-product-filtering-4722e0a53179",
  "hero": "https://cdn-images-1.medium.com/max/800/1*HyxyW6v8mW4b53KiQyosWQ.png",
  "content": [
    {
      "type": "heading",
      "level": 2,
      "text": "Introduction"
    },
    {
      "type": "paragraph",
      "html": "Modern backend systems often need to expose flexible APIs while maintaining strong type safety and performance. GraphQL provides a powerful query language for clients, while SQLC generates type‑safe Go code directly from SQL queries. By combining these two tools, we can build APIs that are both expressive and reliable."
    },
    {
      "type": "paragraph",
      "html": "In this article, we’ll walk through a recent change to a Go project that integrates SQLC with GraphQL, focusing on a <code>Product</code> domain. We’ll see how filters, pagination, and integration tests come together to form a robust pipeline."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*HyxyW6v8mW4b53KiQyosWQ.png",
      "alt": "Integrating SQLC and GraphQL in Go: A Practical Example with Product Filtering",
      "caption": "",
      "width": 1024,
      "height": 1024
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Github Go-GraphQL Project"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "The Change: Adding Product Filters and Pagination"
    },
    {
      "type": "paragraph",
      "html": "We extended the GraphQL schema to support filtering and pagination:"
    },
    {
      "type": "code",
      "lang": "graphql",
      "code": "scalar Time\nscalar Int64\ninput ProductFilter {\n  id: Int  name: String  description: String  minPrice: Int64  maxPrice: Int64  isActive: Boolean\n}\n\ninput PaginationInput {\n  limit: Int  offset: Int\n}\n\ntype Product {\n  id: Int!  name: String!  description: String!  price: Int64!  isActive: Boolean!\n}\n\ntype ProductConnection {\n  products: [Product!]!  total: Int!\n}\n\ntype Query {\n  product(id: Int!): Product  products(filter: ProductFilter, pagination: PaginationInput):\n  ProductConnection!\n}"
    },
    {
      "type": "paragraph",
      "html": "This schema allows clients to query products with flexible filters (by ID, name, description, price range, and active status) and control pagination."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Mapping GraphQL Inputs to SQLC Parameters"
    },
    {
      "type": "paragraph",
      "html": "To bridge GraphQL inputs with SQLC queries, we introduced utility functions that convert GraphQL pointers into <code>sql.Null*</code> types:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func ToNullInt64(val *int64) sql.NullInt64 {\n    if val == nil {\n        return sql.NullInt64{\n            Valid: false\n        }\n    }\n    return sql.NullInt64{\n        Int64: *val, Valid: true\n    }\n}"
    },
    {
      "type": "paragraph",
      "html": "These helpers ensure that optional GraphQL fields map cleanly into SQLC’s nullable parameters."
    },
    {
      "type": "paragraph",
      "html": "The resolver then translates GraphQL filters into SQLC query parameters:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func (s *Product) graphqlFilterToSQLCParams(\nfilter *model.ProductFilter,\npagination *model.PaginationInput,) sqlc.ListProductsWithFiltersParams {\n    params := sqlc.ListProductsWithFiltersParams{\n        Limit: sql.NullInt64{\n            Int64: 10, Valid: true\n        }, Offset: sql.NullInt64{\n            Int64: 0, Valid: true\n        },\n    }\n    if filter != nil {\n        params.ID = utils.ToNullInt32(filter.ID) params.ProductName =\n        utils.ToNullString(filter.Name) params.ProductDescription =\n        utils.ToNullString(filter.Description) params.MinPrice = utils.ToNullInt64(filter.MinPrice)\n        params.MaxPrice = utils.ToNullInt64(filter.MaxPrice) params.IsActive =\n        utils.ToNullBool(filter.IsActive)\n    }\n    if pagination != nil {\n        if pagination.Limit != nil {\n            params.Limit = sql.NullInt64{\n                Int64: int64(*pagination.Limit), Valid: true\n            }\n        }\n        if pagination.Offset != nil {\n            params.Offset = sql.NullInt64{\n                Int64: int64(*pagination.Offset), Valid: true\n            }\n        }\n    }\n    return params\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "SQLC Queries"
    },
    {
      "type": "paragraph",
      "html": "The SQLC queries define the filtering logic directly in SQL:"
    },
    {
      "type": "code",
      "lang": "sql",
      "code": "-- name: ListProductsWithFilters :many\nSELECT\n    id,\n    product_name,\n    product_description,\n    price,\n    is_active,\n    created_at\nFROM products\nWHERE (sqlc.narg('id')::int IS NULL OR id = sqlc.narg('id'))\n  AND (\n      sqlc.narg('product_name')::text IS NULL\n      OR product_name ILIKE '%' || sqlc.narg('product_name') || '%'\n  )\n  AND (\n      sqlc.narg('min_price')::bigint IS NULL\n      OR price >= sqlc.narg('min_price')\n  )\n  AND (\n      sqlc.narg('max_price')::bigint IS NULL\n      OR price <= sqlc.narg('max_price')\n  )\n  AND (\n      sqlc.narg('is_active')::bool IS NULL\n      OR is_active = sqlc.narg('is_active')\n  )\n  AND (\n      sqlc.narg('product_description')::text IS NULL\n      OR product_description ILIKE '%' || sqlc.narg('product_description') || '%'\n  )\nORDER BY id\nLIMIT sqlc.narg('limit')\nOFFSET sqlc.narg('offset');\n\n-- name: CountProductsWithFilters :one\nSELECT COUNT(*) AS count\nFROM products\nWHERE (sqlc.narg('id')::int IS NULL OR id = sqlc.narg('id'))\n  AND (\n      sqlc.narg('product_name')::text IS NULL\n      OR product_name ILIKE '%' || sqlc.narg('product_name') || '%'\n  )\n  AND (\n      sqlc.narg('min_price')::bigint IS NULL\n      OR price >= sqlc.narg('min_price')\n  )\n  AND (\n      sqlc.narg('max_price')::bigint IS NULL\n      OR price <= sqlc.narg('max_price')\n  )\n  AND (\n      sqlc.narg('is_active')::bool IS NULL\n      OR is_active = sqlc.narg('is_active')\n  )\n  AND (\n      sqlc.narg('product_description')::text IS NULL\n      OR product_description ILIKE '%' || sqlc.narg('product_description') || '%'\n  );"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Integration Tests"
    },
    {
      "type": "paragraph",
      "html": "To validate the integration, we wrote end‑to‑end tests that:"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "Create a product via the admin API.",
        "Query the product by ID using GraphQL.",
        "Query the product list with filters and pagination.",
        "Clean up by deleting the product."
      ]
    },
    {
      "type": "paragraph",
      "html": "Example test for filters:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func queryProductsByFilter(t *testing.T, addr, filter string, validate func(map[string]interface{\n}) bool) {\n    query :=\n    fmt.Sprintf(`{\"query\":\"query { products(filter: { %s }) { products { id name description price isActive } total } }\"}`, filter)\n    resp, err := http.Post(addr+\"/query\", \"application/json\", bytes.NewBufferString(query))\n    //...decode response and validate results...\n}"
    },
    {
      "type": "paragraph",
      "html": "This ensures that each filter (<code>id</code>, <code>name</code>, <code>description</code>, <code>minPrice</code>, <code>maxPrice</code>, <code>isActive</code>) behaves correctly against the database."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Benefits of SQLC + GraphQL Integration"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Type Safety:</strong> SQLC generates Go structs and query methods directly from SQL, reducing runtime errors.",
        "<strong>Flexibility:</strong> GraphQL allows clients to specify exactly what they need, with filters and pagination.",
        "<strong>Maintainability:</strong> Utility functions centralize nullability handling, making code cleaner.",
        "<strong>Testability:</strong> Integration tests validate the full pipeline, from GraphQL input to SQL execution."
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Git commit"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Conclusion"
    },
    {
      "type": "paragraph",
      "html": "By integrating SQLC with GraphQL, we’ve built a backend that combines the strengths of both worlds: SQLC’s type‑safe database access and GraphQL’s flexible query language. This change makes the API more powerful, maintainable, and reliable."
    },
    {
      "type": "paragraph",
      "html": "It’s a strong foundation for scaling product queries, and the same pattern can be extended to other domains like orders, users, or analytics."
    }
  ]
}