DEV Community

Cover image for GraphQL Clicked When I Stopped Thinking About Endpoints
Aditya Chauhan
Aditya Chauhan

Posted on

GraphQL Clicked When I Stopped Thinking About Endpoints

If you have built more than a couple of APIs, you have probably felt the friction of REST at scale. You ship an endpoint, the frontend team asks for one more field, you version the route, the mobile team needs a different shape of the same data, and six months later you are maintaining /v3/users/:id/full next to /v2/users/:id/summary and nobody remembers which one the Android app actually calls.

GraphQL was built to kill that exact pain. It is a query language and runtime that lets clients ask for precisely the data they need — no more, no less — from a single endpoint, against a strongly typed schema that doubles as living documentation.

GraphQL started making sense to me when I stopped treating it as "one endpoint instead of many" and started treating it as a backend-owned contract.

The client still asks for data. The response still comes back as JSON. But the important design work lives behind that request: the schema, resolvers, authorization rules, nullability choices, pagination model, and the cost of each field the backend exposes.

I recently spent time understanding GraphQL from this backend angle in a ruby production codebase. This is the mental model I wish I had before reading real GraphQL APIs.

.
.

GraphQL solves a data-shape problem

Most product screens do not naturally think in endpoints. They think in workflows.

A product screen may need viewer context, one primary resource, access state, display metadata, and the first page of related records. With fixed REST-style endpoints, teams often end up choosing between a few imperfect options:

  • Return too much data from a broad endpoint.
  • Make the frontend call several endpoints for one screen.
  • Add screen-specific endpoints that become hard to reuse.

GraphQL approaches this differently. The backend publishes a typed schema, and the client selects a permitted shape from that schema.

request_execution

.
.
.

The schema is the public API surface

The schema answers questions like:

  • Which root fields exist?
  • Which types can clients query?
  • Which fields are available on each type?
  • Which arguments does a field accept?
  • Which fields require authorization?
  • Which fields need custom resolver logic?

Once a field is exposed, clients can start depending on it. That makes every schema field a product decision, not just an implementation detail.

.
.
.

Resolvers: Where the Work Happens

Resolvers are the part of the backend that turn fields into real values.

For a root field, a resolver may load a record using an external identifier, read request context, check access, and return an object that GraphQL can continue resolving.

For nested fields, the work can vary a lot. Some fields are simple attributes. Some are calculated methods. Some call another resolver. Some need stricter authorization than the parent object. Some load data from a join table or another service.

That is why GraphQL does not remove backend complexity. It organizes that complexity around fields.

This also explains a common production risk: a small-looking nested field can become expensive. If a resolver runs once for every item in a list and performs its own database query each time, the API can hit an N+1 problem quickly.

.
.
.

Mutations are write boundaries

Queries read data. Mutations change data.

The backend design question is not just "which method updates the record?" A mutation should define the full write contract:

  • What input is required?
  • Which object is being changed?
  • Who can perform the change?
  • What does success return?
  • Which validation errors can the user fix?
  • Which failures should be treated as system or authorization errors?
  • Which side effects run after success?

.
.
.

Connections make pagination explicit

Pagination was the part that initially felt the most noisy to me.

Instead of returning a plain list, GraphQL APIs often return a connection. A connection is a wrapper around one page of results.

The basic vocabulary is:

  • Connection: the paginated list wrapper.
  • Edge: one result in that list, plus metadata about the relationship.
  • Node: the actual object.
  • Cursor: the bookmark for this result.
  • PageInfo: metadata that tells the client whether another page exists and where to continue.

connection_model

The part that made edges click for me was this:

Node is the thing. Edge is the relationship to that thing.

Imagine the same object appearing through two different relationships. The object itself is unchanged, but the metadata about each relationship may be different.

That metadata belongs to the relationship. It should not necessarily live on the object itself.

That is why edges exist. They give the API a place to put relationship-specific data, while nodes stay focused on the actual object.

.
.
.

Generated types make contract drift visible

In many modern GraphQL frontends, operation files are not just raw strings. Tooling reads the backend schema and the client operations, then generates typed variables, typed response shapes, and query or mutation hooks.

This creates useful feedback.

If the backend removes a field that an operation uses, code generation can fail. If a backend field changes from always-present to maybe-null, the generated frontend type changes. If an enum value changes, the compiler may catch places that need to be updated.

This is one of GraphQL's strongest collaboration benefits. The contract can be checked before the bug reaches a browser.

But it only works if the schema, operations, and generated files stay in sync.

For backend engineers, that means a schema change is not done just because the server boots. You also need to think about generated client impact:

  • Which operations select this field?
  • Are enum values stable?
  • Is this a removal or should it be a deprecation?
  • Will generated types change in a meaningful way?

.
.
.

GraphQL is a tradeoff, not an automatic upgrade

After this learning cycle, I do not think of GraphQL as better REST.

It is a different API design tradeoff.

GraphQL is a strong fit when product screens need precise nested data, different clients need different slices of the same domain object, and typed contracts improve frontend/backend collaboration.

It may be the wrong tool when the workflow is a simple file upload, a command with no meaningful selected response shape, a heavy async process, or an API where caching and strict cost control matter more than selection flexibility.

The main tradeoff is visibility.

With a REST endpoint, request cost is often easier to reason about from the route. With GraphQL, request cost depends on the operation shape. One endpoint can represent many different execution paths.

Top comments (0)