Getting Started
This guide takes you from an empty Rails app to a working, directive-driven GraphQL endpoint.
Install
# Gemfile
gem 'lighthouse-graphql'
bundle install
Runtime dependencies (graphql ~> 2.5, activesupport) come with the gem. If you want federation, add apollo-federation to your app — it is intentionally not a hard dependency of Lighthouse so non-federated apps stay lean.
The install generator
bin/rails generate lighthouse:install
does steps 1–5 below for you. It builds on graphql-ruby's own install, rails g graphql:install, with whichever of its options the app chose (--relay, --batch, --api, ...). When the app has no graphql-ruby schema class yet, it runs graphql:install first, with graphql-ruby's defaults. Then it adapts what graphql-ruby wrote, and adds Lighthouse's own:
| File | What it is |
|---|---|
app/graphql/app_schema.rb | graphql-ruby's schema class, now built by Lighthouse from the SDL on every class graphql-ruby wrote (base_types). The rest of the class is graphql-ruby's |
app/graphql/types/query_type.rb, mutation_type.rb | graphql-ruby's root types, which the SDL's Query and Mutation are built on. Their example test_field goes; Relay's node / nodes stay |
app/graphql/types/base_*.rb, resolvers/base_resolver.rb, mutations/base_mutation.rb | graphql-ruby's base classes. BaseObject, BaseInterface, BaseUnion and BaseScalar include Lighthouse::GraphQL::Type |
app/controllers/graphql_controller.rb | graphql-ruby's controller, replaced by one including Lighthouse::GraphQL::Controller |
config/initializers/lighthouse.rb | Every configuration option, commented out at its default |
graphql/schema.graphql | The schema's entrypoint: a starter schema (user via @find, users via @paginate) that assumes a User model. With graphql-ruby's Relay, also node(id:) and nodes(ids:) (see relay.md). Skipped when the file exists |
graphql.config.yml | IDE configuration — see IDE support |
--schema=ApiSchema, --schema-path=app/schema/schema.graphql, --subscriptions, --skip-controller and --skip-ide change what it does. Rerunning it changes nothing, and a controller the app changed is kept. generators.md has the details, every generator Lighthouse has, and which of graphql-ruby's graphql:* generators fit a Lighthouse app.
1. Write your schema (SDL)
Lighthouse reads the schema from one entrypoint, RAILS_ROOT/graphql/schema.graphql (config.schema_path), as PHP Lighthouse does:
# graphql/schema.graphql
type Query {
users: [User!]! @all
user(id: ID! @eq): User @find
}
type User {
id: ID!
name: String
email: String
}
@all resolves to User.all; @find resolves a single User by the id argument (the @eq makes id a filter). Lighthouse infers the model from the field's return type name (User), or you can be explicit with @all(model: "User").
To split the schema, the entrypoint imports the other files. A type is defined once, and the other files add to it with extend type:
# graphql/schema.graphql
type Query
#import user.graphql
#import blog/*.graphql
# graphql/user.graphql
type User {
id: ID!
name: String
}
extend type Query {
users: [User!]! @all
}
A file nothing imports is not part of the schema, and a type defined twice is an error. See schema-organisation.md.
2. Provide your base classes
Lighthouse builds on graphql-ruby, and follows its convention: each kind of class inherits from a base class in its own namespace, where what they all share goes once. graphql-ruby's graphql:install writes them, and lighthouse:install builds on them. By hand, run graphql-ruby's install with its defaults:
bin/rails generate graphql:install
Then include Lighthouse::GraphQL::Type in the base object, interface, union and scalar, so every type class that inherits from them lends its methods to the SDL type of its name (see type classes):
# app/graphql/types/base_object.rb
module Types
class BaseObject < GraphQL::Schema::Object
include Lighthouse::GraphQL::Type
edge_type_class(Types::BaseEdge)
connection_type_class(Types::BaseConnection)
field_class Types::BaseField
# include ApolloFederation::Object # if you use federation
end
end
Add the subscription base, which graphql-ruby has none of, if the schema has a Subscription type (lighthouse:install --subscriptions writes it):
# app/graphql/subscriptions/base_subscription.rb — Subscription fields
module Subscriptions
class BaseSubscription < GraphQL::Schema::Subscription
include Lighthouse::GraphQL::Subscription
end
end
graphql-ruby's Mutations::BaseMutation stays a RelayClassicMutation. A Mutation field that declares its arguments one by one calls it as a plain mutation, with those arguments as keywords; one with a single input: argument goes through RelayClassicMutation's own input: handling.
3. Build the schema
graphql-ruby's schema class becomes Lighthouse's: it inherits from the schema get_schema builds from the SDL, instead of GraphQL::Schema.
# app/graphql/app_schema.rb
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
options: {
base_types: {
query_type: Types::QueryType,
mutation_type: Types::MutationType,
node: Types::NodeType,
resolver: Resolvers::BaseResolver,
mutation: Mutations::BaseMutation,
subscription: Subscriptions::BaseSubscription,
object: Types::BaseObject,
interface: Types::BaseInterface,
union: Types::BaseUnion,
scalar: Types::BaseScalar,
enum: Types::BaseEnum,
input_object: Types::BaseInputObject
}
}
)
# Lighthouse batches relationship loads through graphql-ruby's Dataloader.
use GraphQL::Dataloader
max_depth(15)
max_query_string_tokens(5000)
validate_max_errors(100)
# Relay-style Object Identification, as graphql-ruby's install writes it:
def self.id_from_object(object, type_definition, query_ctx)
object.to_gid_param
end
def self.object_from_id(global_id, query_ctx)
GlobalID.find(global_id)
end
end
The SDL is read from config.schema_path and the files it imports (get_schema(schema_path: ...) overrides it). The class body is graphql-ruby's, so plugins and settings go there as in any graphql-ruby schema (use ..., max_complexity(...)), but for query(...) and mutation(...), which the SDL's root types replace, and resolve_type, which Lighthouse answers (see types.md).
object: and the other SDL kinds go to graphql-ruby, which builds the SDL's types from them. query_type: and mutation_type: are the classes the SDL's Query and Mutation are built on, so their own fields (Relay's node / nodes) are the schema's too; node: is the SDL's Node interface (see relay.md). resolver:, mutation: and subscription: are Lighthouse's: every class a Query, Mutation or Subscription field resolves to must inherit from its kind's base, and a Subscription field with no class of its own resolves with the subscription base. See resolvers.md.
A constant works as well, with the settings passed as options or set on it:
AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
options: { base_types: { object: Types::BaseObject }, using: { GraphQL::Dataloader => {} } }
)
Lighthouse uses graphql-ruby's
Dataloaderto batch relationship loads. Keepuse GraphQL::Dataloaderin your schema to get N+1-free relationships.
4. Serve it
rails g lighthouse:install replaces graphql-ruby's controller (when the app has not changed it) with one including Lighthouse::GraphQL::Controller, a concern that serves the schema:
class GraphqlController < ApplicationController
include Lighthouse::GraphQL::Controller
lighthouse_schema AppSchema # or 'AppSchema', or a block: lighthouse_schema { AppSchema }
private
# What @guard and the @can* directives read. Defaults to
# { current_user: current_user } when the controller has a current_user.
def graphql_context = { current_user: current_user }
end
Route to it (post '/graphql', to: 'graphql#execute', plus a get for persisted queries if you use them). Its execute action:
- dispatches the
StartRequestandEndRequestevents; - reads
variables(a JSON string, as a GET carries it, or parameters); - runs the operation through
PersistedQueries.execute, so Automatic Persisted Queries work onceconfig.persisted_queriesis on, and a JSON array body runs as a batch; - sets the
Cache-Controlheader the selected fields'@cacheControlhints allow; - renders the JSON; in development an error renders as JSON with its backtrace.
The hooks graphql_context, graphql_schema and graphql_variables(param) are private methods you can override. A controller with an action of its own wraps it in lighthouse_request { ... } to keep the request events:
def search
lighthouse_request do
render json: Lighthouse::GraphQL::PersistedQueries.execute(AppSchema, query: params[:q], context: graphql_context)
end
end
Not on the concern? Plain graphql-ruby works too: render json: AppSchema.execute(params[:query], variables: params[:variables], context: { current_user: current_user }).
5. Configure (optional)
Create an initializer to wire app-specific settings. This is also the single place to register custom directives and federation reference resolvers, which keeps Lighthouse itself free of any app constants.
# config/initializers/lighthouse.rb
require 'lighthouse-graphql'
Lighthouse.configure do |config|
# Namespaces tried when resolving a class referenced by a directive argument,
# e.g. @field(resolver: "Users::Profile") or @all(model: "User").
config.resolver_namespaces = ['App::Graphql', 'Resolvers']
# How swallowed directive errors are surfaced (raise in test, log otherwise).
# config.on_error = ->(error, _ctx) { Sentry.capture_exception(error) }
end
See configuration.md for all options.
IDE support
Your IDE validates and autocompletes .graphql files against the schema it can see. Lighthouse's directive definitions and the types its manipulation phase generates live in Ruby, so without help the IDE flags @paginate as unknown and can't resolve UserPaginator. PHP Lighthouse solves this with an IDE helper command, and so does Lighthouse for Rails. In development, an app with a graphql.config.yml gets the files rewritten on boot and on every reload (config.ide_helper), so you never run it by hand there. Elsewhere:
bin/rails lighthouse:ide_helper # writes to Rails.root
OUTPUT=tmp/ide bin/rails lighthouse:ide_helper
It writes two files:
schema-directives.graphql— a definition for every registered directive, built-in or your own, except those your SDL already declares (so the IDE never sees a duplicate).programmatic-types.graphql— the types Lighthouse adds to the executable schema:<Type>Paginator,<Type>SimplePaginator,<Type>Connection/Edge,PaginatorInfo,SimplePaginatorInfo,PageInfo, the@whereConditionsand@orderByinputs and enums, and the_placeholder scalar (without it, the IDE reportsUnknown type "_"). It leaves out every type the SDL orschema-directives.graphqldefines, because graphql-js, which the IDE plugins use, rejects a type defined twice.
Point your IDE's graphql-config at them next to your SDL — the install generator writes this graphql.config.yml:
schema:
- graphql/**/*.graphql
- schema-directives.graphql
- programmatic-types.graphql
It works with the GraphQL plugin for JetBrains IDEs and the GraphQL extension for VS Code. The files are derived from your schema, so the generator adds them to .gitignore. Outside development, rerun the task after changing the schema or registering a directive. The IDE reads every .graphql file the glob matches, as one schema: it does not follow #import.
The IDE reads fields as you wrote them:
users: [User!]! @paginatestill looks like a list to it, although clients queryusers { data { … } }. Field rewrites are not something an extra file can express.
Casing: camelCase ↔ snake_case
GraphQL conventionally uses camelCase field names; Rails uses snake_case attributes. Lighthouse bridges them with one rule: a field with no directive is read off the parent object by its snake_cased name.
type User {
customAttributes: String # reads user.custom_attributes
}
When the mapping isn't a simple underscore, use @rename:
type User {
displayName: String @rename(attribute: "full_name")
}
Custom resolvers and mutations
When a field needs Ruby, write a graphql-ruby resolver or mutation class named after it — Query.latestPosts resolves to Resolvers::LatestPosts (app/graphql/resolvers/latest_posts.rb, a Resolvers::BaseResolver), Mutation.publishPost to Mutations::PublishPost (a Mutations::BaseMutation). A method of the root type class comes first, as in graphql-ruby: Query.latestPosts calls Types::QueryType#latest_posts when it defines one. bin/rails generate graphql:mutation PublishPost writes a code-first mutation. See resolvers.md.
Next steps
- Directives reference — the full vocabulary.
- Resolvers, mutations & builders, Mutations, Validation, Caching.
- Relationships — wiring associations.
- Filtering & ordering — query constraints.
- Apollo Federation — become a subgraph.