Lighthouse GraphQL RailsLighthouse GraphQL Rails
Docs
Directives
Example app
Changelog
RubyGems
GitHub
Docs
Directives
Example app
Changelog
RubyGems
GitHub
  • Getting Started

    • Motivation
    • Getting Started
    • Configuration
  • The Basics

    • Resolvers, Mutations and Builders
    • Generators
    • Unions, Interfaces and Scalars
    • Relay: Global Object Identification
    • Relationships
    • Filtering & Ordering
    • Mutations
    • File Uploads
    • Validation
    • Subscriptions
  • Security

    • Authentication & Authorization
    • Query Complexity & Depth
  • Performance

    • Performance at scale
    • Caching
  • Digging Deeper

    • Schema Organisation
    • Custom Directives
    • Events
    • Apollo Federation
    • Best Practices
  • API Reference

    • Directives Reference

Best Practices

A few conventions that keep a Lighthouse schema clean and predictable. They build on the GraphQL best practices and PHP Lighthouse's guidance, adapted to Rails.

Name your primary key id

Clients and tooling (and Apollo Federation) assume an id field. Keep it ID!.

Let the schema be the source of truth

Reach for a directive before writing a resolver class. Most fields are an @all, @find, @paginate, a relationship, or a plain attribute read. Hand-write Ruby (@field) only for genuinely custom logic. Your .graphql files should read like documentation of your API.

Keep the camelCase ↔ snake_case mapping implicit

Expose camelCase fields and let the default reader map them to snake_case attributes. Only use @rename(attribute:) when the mapping isn't a plain underscore — don't sprinkle it everywhere.

Restrict filterable columns

When using @whereConditions, always pass columns: (or columnsEnum:) to constrain what clients can filter on. Open-ended column filtering is a performance and security risk:

people(where: _ @whereConditions(columns: ["age", "type"])): [Person!]! @all

The same applies to @orderBy — expose a bounded set of sortable columns.

Enable the Dataloader

Add use GraphQL::Dataloader to your schema so relationship directives batch. Without it, nested relationships re-introduce N+1 queries.

Authorize at the field, early

Use @guard and the @can* directives on fields that require them. They wrap the resolver, so the check runs before any data is loaded (only @canResolved, which needs the result, runs after — keep it to fields that don't write).

Keep resolvers, mutations and builders apart

A custom Query field is a Resolvers::BaseResolver in app/graphql/resolvers, a Mutation field a Mutations::BaseMutation in app/graphql/mutations, and a query refinement a builder in app/graphql/builders. They have different jobs and contracts; keep each kind in its own folder. See resolvers.md.

Share through the base classes

What every resolver, mutation or subscription needs (a concern, a helper, graphql-ruby's argument_class) goes in Resolvers::BaseResolver, Mutations::BaseMutation or Subscriptions::BaseSubscription, once, as in any graphql-ruby app. Name them in the schema's base_types (resolver:, mutation:, subscription:), and a class that forgets to inherit from its base is refused instead of quietly missing what it shares. See resolvers.md.

Validate in the model first

Model validations already cover @create / @update. Reach for @validator only for rules that aren't the model's. See validation.md.

Mutations: prefer throwable errors

For mutations, raise GraphQL::ExecutionError for business failures ("Not found", validation messages) rather than returning an errors field in the payload. Thrown errors surface in the response's top-level errors array, which is where GraphQL clients' error handling already looks — onError callbacks fire, and you don't have to thread an errors field through every payload and caller.

def resolve(id:)
  record = Model.find_by(id: id)
  raise GraphQL::ExecutionError, 'Not found.' unless record
  # ...
  { record: record }
end

One configuration seam

Keep all Lighthouse wiring — namespaces, resolver_namespaces, custom directive registration, federation reference resolvers, on_error — in initializers, not scattered through the app. This keeps the library a clean dependency and makes the app's GraphQL setup easy to find.

Split your SDL

Lighthouse reads graphql/schema.graphql and the files it #imports (schema-organisation.md). Organize by domain (users.graphql, orders.graphql, …), each with its types and the fields it adds with extend type Query / extend type Mutation. Keep a small directives.graphql, imported too, only for directives you declare yourself (built-ins are injected automatically).

Edit this page on GitHub
Prev
Apollo Federation