Field note / patterns

publishedupdated 2026-05-04#graphql#api#schema#resolvers#federation

GraphQL in Production

How I think about GraphQL beyond the pitch: schema boundaries, resolver cost, caching, authorization, and where it wins over REST.

TL;DR

  • GraphQL is strongest when clients need flexible composition across multiple data domains and I want one coherent query surface.
  • The real production work is not writing type Query; it is schema design, resolver efficiency, authorization, and usage governance.
  • I like GraphQL when the frontend surface is rich and evolving, but I only trust it when cost and ownership are explicit.
  • GraphQL improves contract flexibility, but it shifts complexity into the graph layer and into operational discipline.

Context

GraphQL looks easy in demos because the basic promise is compelling:

  • ask for exactly what you need
  • get one predictable response shape
  • evolve one schema instead of many route versions

In real systems, the pressure points show up later:

  • slow resolvers
  • N+1 queries
  • unclear field ownership
  • authorization scattered across resolvers
  • schema growth without discipline

That is why production GraphQL is a design problem as much as a transport choice.

The Approach

1. Start from the schema contract, not resolver convenience

GraphQL’s type system is one of its biggest strengths.

type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
  title: String!
  price: Money!
  inventoryStatus: String!
  reviews(first: Int!, after: ID): ReviewConnection!
}

The schema should reflect business language and client usefulness, not whatever the storage model happened to be.

2. Use it where composition pressure is real

Good fits:

  • product detail pages pulling from pricing, inventory, content, and reviews
  • mobile/web clients with different data needs
  • organizations with multiple services that still want a single client-facing graph

This was one of the reasons GraphQL became attractive in large frontend-heavy systems: it gave the UI one contract surface instead of many coordinated REST fetches.

3. Treat resolver cost as a first-class production concern

The fastest way to make GraphQL feel bad is to let each field trigger its own naive fetch.

const resolvers = {
  Query: {
    product: async (_parent: unknown, args: { id: string }, ctx: Context) => {
      return ctx.productService.findById(args.id)
    },
  },
  Product: {
    reviews: async (product: Product, args: { first: number; after?: string }, ctx: Context) => {
      return ctx.reviewService.findByProductId(product.id, args)
    },
  },
}

That shape is fine until nested access patterns make the backend fan out too much. Then I want batching, caching, and clear per-field ownership.

4. Keep authorization close to domain boundaries

A GraphQL server should not confuse “field exists” with “field is allowed.”

I want auth decisions to be deliberate for:

  • tenant scope
  • role-based fields
  • internal-only or privileged fields
  • mutation permissions

The graph can be elegant and still leak data if auth is bolted on too late.

5. Real-world trade: GraphQL vs REST

I choose GraphQL over REST when:

  • the client surface is rich and compositional
  • the number of coordinated reads is high
  • the schema gives more leverage than many resource endpoints

I stay with REST when:

  • simple cacheable resources dominate
  • the contract needs to stay tool-agnostic and obvious
  • the client does not benefit enough from graph flexibility

6. Watch schema governance as the graph grows

At scale, the biggest GraphQL problems are often human:

  • naming inconsistency
  • weak deprecation discipline
  • fields nobody owns
  • no telemetry for which queries are hot or expensive

That is where platform-level governance matters more than syntax.

Trade-offs

  • GraphQL gives clients flexible reads, but it demands stronger discipline in schema design and resolver performance.
  • One graph surface can simplify frontend development, but it can also centralize complexity in a new layer.
  • Schema evolution is smoother than route-version churn, but only if deprecation and field ownership are managed well.
  • It is powerful in frontend-heavy systems, but it is not automatically the right answer for every API boundary.

References