What is GraphQL & How It Compares to REST
๐ง Detailed Explanation
GraphQL is a way to get exactly the data you need from an API โ no more, no less.
It was created by Facebook to solve common problems with traditional REST APIs.
In REST:
- You usually make multiple API calls to get all your data.
- Sometimes you get too much information you donโt need (over-fetching).
- Or you donโt get enough (under-fetching), so you have to call another API.
In GraphQL:
- You ask for exactly what you need โ and get it in a single response.
- Thereโs only one endpoint (usually
/graphql
). - You write a query that matches your data needs.
Think of it like: Instead of ordering a fixed meal (REST), you build your own plate and pick exactly what you want (GraphQL).
Simple Example:
If you want a user’s name
and email
in REST, you might get back everything โ name, email, posts, phone, etc.
But in GraphQL, you can write:
{
user(id: 1) {
name
email
}
}
And the response will be only what you asked for:
{
"user": {
"name": "Ali",
"email": "[email protected]"
}
}
This makes GraphQL very efficient for frontend apps, especially when working with mobile or slow networks.
๐ก Examples
REST Example:
GET /users/1
Response:
{
"id": 1,
"name": "Ali",
"email": "[email protected]",
"posts": [...]
}
GraphQL Equivalent:
{
user(id: 1) {
name
email
}
}
GraphQL gives exactly the data needed โ no more, no less.
๐ Alternative Concepts
- REST APIs (Representational State Transfer)
- gRPC (for binary data exchange)
- OData (Open Data Protocol)
โ General Questions & Answers
Q1: Is GraphQL better than REST?
A: It depends on your needs. GraphQL is flexible and powerful for frontends but may add complexity on the backend.
Q2: Can I use GraphQL with Rails?
A: Yes! Use the graphql-ruby
gem to add GraphQL support in Rails.
๐ ๏ธ Technical Questions & Answers
Q1: How is data fetched in GraphQL?
A: Data is fetched by writing a GraphQL query. You define what fields you want, and the server returns exactly those fields.
Example:
# GraphQL Query
{
user(id: 1) {
name
email
posts {
title
}
}
}
Response:
{
"user": {
"name": "Ali",
"email": "[email protected]",
"posts": [
{ "title": "My first post" },
{ "title": "Another update" }
]
}
}
Why itโs good: One query gives you nested related data in a single request.
Q2: How do I handle errors in GraphQL?
A: Errors are returned in a standard errors
array in the response โ even if partial data is available.
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"path": ["user"]
}
]
}
Solution: Always check both data
and errors
when handling responses.
Q3: How do I protect against expensive or malicious queries?
A: Use query depth limits, complexity analysis, and timeouts.
Solution: In Rails with graphql-ruby
, use:
GraphQL::Analysis::AST::MaxQueryDepth.new(max_depth: 10)
GraphQL::Analysis::AST::MaxQueryComplexity.new(max_complexity: 100)
This prevents users from asking for deeply nested or overly complex data.
Q4: How does GraphQL handle authentication?
A: It doesn’t define authentication itself โ you add it in your resolvers.
# Example in Rails (graphql-ruby)
def resolve(id:)
raise GraphQL::ExecutionError, "Not authorized" unless context[:current_user]
User.find(id)
end
Tip: Pass the current user from your Rails controller into the GraphQL context.
Q5: How do I make a mutation in GraphQL (like POST in REST)?
A: GraphQL uses mutation
blocks for creating/updating/deleting data.
mutation {
createUser(name: "Ali", email: "[email protected]") {
id
name
}
}
Response:
{
"data": {
"createUser": {
"id": "1",
"name": "Ali"
}
}
}
Just like POST/PUT in REST, but more flexible and readable.
โ Best Practices
- Use GraphQL introspection carefully โ disable in production.
- Validate and sanitize incoming queries.
- Limit query depth to avoid performance hits.
- Use persisted queries to improve speed and security.
- Use tools like GraphiQL or Apollo Studio to explore and test.
๐ Real-World Use Cases
- โก React apps fetching nested data with a single request
- ๐ฑ Mobile apps where bandwidth and efficiency are critical
- ๐ฎ Game dashboards querying user + score + ranking in one call
- ๐๏ธ E-commerce product pages with multiple data dependencies (price, reviews, inventory)
- ๐ Analytics platforms combining multiple datasets
Benefits of GraphQL in Rails (Over-fetching vs Under-fetching)
๐ง Detailed Explanation
GraphQL helps you get exactly the data you need from your server โ not more, not less.
In a normal REST API, you might get:
- โ Too much data (called over-fetching)
- โ Or not enough data (called under-fetching)
Letโs say your app needs just a userโs name and email.
With REST, the response might include:
- โ๏ธ name
- โ๏ธ email
- โ bio
- โ avatar
- โ posts, comments, followers, etc.
This is over-fetching โ getting a lot you donโt need.
Now imagine you want user + posts + comments in one screen.
With REST, youโll need to make 3 API calls:
- ๐ /users
- ๐ /users/:id/posts
- ๐ /posts/:id/comments
This is under-fetching โ making extra requests to get the full picture.
With GraphQL:
- โ
You use ONE endpoint:
/graphql
- โ You write a query to get exactly what you need
- โ Everything comes back in one go
Think of it like: a buffet where you only take what you want on your plate (GraphQL) vs a fixed meal that gives you too much or too little (REST).
This makes apps faster, especially when:
- You’re on mobile networks
- Your frontend changes often
- You need related data (like posts + comments)
Summary: GraphQL is smart, flexible, and efficient. It helps you build better APIs in Rails, especially when you donโt want to waste time or data.
๐ก Examples
REST Example: Getting user details might return the entire profile including address, bio, photo, etc., even if you only needed the name.
GET /api/users/1
{
"id": 1,
"name": "Ali",
"email": "[email protected]",
"bio": "Full stack dev",
"avatar": "...",
"posts": [...]
}
GraphQL Example: Ask only for what you need:
{
user(id: 1) {
name
email
}
}
Output:
{
"user": {
"name": "Ali",
"email": "[email protected]"
}
}
๐ Alternative Concepts
- REST with custom endpoints
- Rails JBuilder for JSON customization
- ActiveModel::Serializers with includes/excludes
โ General Questions & Answers
Q1: Can GraphQL reduce the number of API calls?
A: Yes. With GraphQL, you can get deeply nested related data (e.g. user โ posts โ comments) in one request.
Q2: Is GraphQL only useful for large apps?
A: No. Even small apps benefit from flexible queries, especially when you have multiple clients like web and mobile.
๐ ๏ธ Technical Questions & Answers
Q1: What is over-fetching and under-fetching in REST APIs?
A: Over-fetching means getting more data than needed. Under-fetching means not getting enough data and needing more API calls.
Example:
# REST Example (Over-fetching)
GET /users/1
Response:
{
"id": 1,
"name": "Ali",
"email": "[email protected]",
"bio": "...",
"avatar": "...",
"posts": [...],
"followers": [...]
}
You only needed name
and email
, but got everything. Thatโs over-fetching.
Q2: How does GraphQL solve this problem?
A: GraphQL lets the client request only the fields it needs. The server returns exactly that โ nothing more.
# GraphQL Query
{
user(id: 1) {
name
email
}
}
Response:
{
"user": {
"name": "Ali",
"email": "[email protected]"
}
}
โ No extra fields like bio or posts. Just what you asked for.
Q3: How do I install GraphQL in a Rails project?
A: Use the graphql
gem.
# Step 1: Add to Gemfile
gem 'graphql'
# Step 2: Install it
bundle install
# Step 3: Run the generator
rails generate graphql:install
This creates a schema file, base query/mutation files, and mounts the API at /graphql
.
Q4: How do I create a basic query in GraphQL with Rails?
A: Define a type and a field in Types::QueryType
.
# app/graphql/types/query_type.rb
field :user, Types::UserType, null: false do
argument :id, ID, required: true
end
def user(id:)
User.find(id)
end
Now you can run:
{
user(id: 1) {
name
email
}
}
And get exactly what the frontend needs.
Q5: How can I prevent expensive or deep queries?
A: Use built-in guards for query depth and complexity in `graphql-ruby`.
GraphQL::Analysis::AST::MaxQueryDepth.new(10)
GraphQL::Analysis::AST::MaxQueryComplexity.new(100)
This prevents users from writing queries that slow down your server.
โ Best Practices
1. Only request the data you need
GraphQL lets you choose specific fields โ donโt ask for everything unless needed.
# Good (small payload)
{
user(id: 1) {
name
email
}
}
# Bad (fetching unnecessary data)
{
user(id: 1) {
name
email
posts {
comments {
user {
profile {
bio
}
}
}
}
}
}
2. Use pagination for large lists
Never return 1000+ records at once. Use first
, after
, or offset
patterns.
{
posts(first: 10) {
edges {
node {
title
content
}
}
}
}
3. Set limits for query depth and complexity
To prevent malicious or deeply nested queries from slowing down your app:
# config/initializers/graphql.rb
GraphQL::Analysis::AST::MaxQueryDepth.new(10)
GraphQL::Analysis::AST::MaxQueryComplexity.new(200)
4. Use descriptive errors
Throw clear and helpful errors in your resolvers.
def user(id:)
user = User.find_by(id: id)
return GraphQL::ExecutionError.new("User not found") unless user
user
end
5. Protect private data with authentication
Use the context
object to check current user.
def current_user
context[:current_user] || raise GraphQL::ExecutionError, "Unauthorized"
end
6. Organize your schema
Group types, mutations, and resolvers by feature (like users, posts).
Types::UserType
Mutations::CreateUser
Inputs::UserInputType
7. Use GraphiQL or Apollo Studio for testing
These tools help you explore your schema, run live queries, and test edge cases easily.
8. Avoid N+1 problems
Use tools like batch-loader
or graphql-batch
to avoid repeated DB queries.
# Gemfile
gem 'batch-loader'
# In resolver
field :posts, [PostType], null: false
def posts
BatchLoader::GraphQL.for(object.id).batch do |user_ids, loader|
Post.where(user_id: user_ids).group_by(&:user_id).each do |user_id, posts|
loader.call(user_id, posts)
end
end
end
๐ Real-World Use Cases
- Mobile Apps: Reduce payload for slow networks.
- React/Vue Frontends: Fetch exactly what’s needed per page.
- CMS Dashboards: Show user, posts, and settings in one request.
- Analytics Dashboards: Customize metrics and charts with dynamic queries.
GraphQL Queries in Rails
๐ง Detailed Explanation
GraphQL queries are how you ask for data from your Rails server.
They work like questions. You write what you want, and the server gives you exactly that โ nothing more, nothing less.
For example:
- Do you want just a userโs
name
andemail
? โ Ask for those only. - Want user plus their
posts
? โ You can ask for both โ in one request.
REST vs GraphQL:
- ๐ With REST: You may need 2โ3 API calls to get related data (like
/users
+/posts
). - โก With GraphQL: You write one smart query and get everything in one go.
How it works in Rails:
- Install the
graphql
gem - Define a type (like
UserType
) - Add fields to your
QueryType
- Now your frontend can ask for that data
โ Itโs fast, clean, and flexible โ great for modern web and mobile apps.
Tip: Think of GraphQL like ordering a custom burger. You choose each ingredient. REST gives you a fixed meal โ sometimes with stuff you donโt want.
โ Best Implementation (Step-by-Step)
Hereโs how to implement a GraphQL query in a Rails application from scratch, using best practices.
๐ฆ Step 1: Add the GraphQL Gem
Add the official GraphQL gem to your Gemfile
and install it:
# Gemfile
gem 'graphql'
# Terminal
bundle install
rails generate graphql:install
This creates the base schema, query type, mutation type, and mounts GraphQL at /graphql
.
๐งฑ Step 2: Define Your Type (e.g. User)
Create a GraphQL type for your model (User in this example):
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
field :posts, [PostType], null: true
end
end
This defines which fields can be requested on a user.
๐ Step 3: Add the Query Field
Now add a user
query inside your main QueryType
:
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :user, UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
user = User.find_by(id: id)
return GraphQL::ExecutionError.new("User not found") unless user
user
end
end
end
This allows clients to run user(id: 1)
queries.
๐งช Step 4: Test in GraphiQL
Visit http://localhost:3000/graphiql
in your browser and try this query:
{
user(id: 1) {
name
email
posts {
title
}
}
}
You should see a clean, structured JSON response.
๐ Step 5: Secure It with Context (Auth)
Pass current user from controller:
# app/controllers/graphql_controller.rb
def execute
context = {
current_user: current_user
}
...
end
Then restrict queries in the resolver:
def user(id:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
User.find(id)
end
โ๏ธ Step 6: Add Query Depth & Complexity Limits
Protect your server from deeply nested or expensive queries:
# config/initializers/graphql.rb
GraphQL::Analysis::AST::MaxQueryDepth.new(10)
GraphQL::Analysis::AST::MaxQueryComplexity.new(200)
๐ Summary
- โ Install and set up GraphQL
- โ Define clean, reusable types
- โ Add meaningful and secure resolvers
- โ Always validate and limit queries
- โ Use GraphiQL for testing
This approach makes your GraphQL API in Rails fast, clean, and production-ready.
๐ก Examples
Query to fetch a user and their posts:
{
user(id: 1) {
name
email
posts {
title
published
}
}
}
Response:
{
"user": {
"name": "Ali",
"email": "[email protected]",
"posts": [
{ "title": "GraphQL Basics", "published": true }
]
}
}
๐ Alternative Concepts
- REST GET request to
/users
,/users/:id/posts
- GraphQL Fragments (to reuse parts of queries)
- Stored Procedures (in SQL world)
โ General Questions & Answers
Q1: Do GraphQL queries replace REST?
A: Not exactly. GraphQL is more flexible, but REST is still good for simple services or caching-heavy apps.
Q2: Is one query better than multiple REST calls?
A: Yes! One query can get nested data, which is hard with REST unless you make many requests.
๐ ๏ธ Technical Questions & Answers
Q1: How do I create a GraphQL query in a Rails app?
A: First, install the GraphQL gem and generate the schema using:
bundle add graphql
rails generate graphql:install
This sets up everything you need: schema, types, query entry point.
Q2: How do I define a query to fetch a user?
A: You add it in Types::QueryType
like this:
# app/graphql/types/query_type.rb
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
Now you can run this query from the frontend or GraphiQL:
{
user(id: 1) {
name
email
}
}
Response:
{
"user": {
"name": "Ali",
"email": "[email protected]"
}
}
Q3: What happens if the user doesnโt exist?
A: You should return a custom error using GraphQL::ExecutionError
.
def user(id:)
user = User.find_by(id: id)
return GraphQL::ExecutionError.new("User not found") unless user
user
end
This sends a clear error to the client while keeping the query response valid.
Q4: Can I reuse the same query structure?
A: Yes! Use fragments in the frontend to avoid repeating query code.
fragment userFields on User {
name
email
}
{
user(id: 1) {
...userFields
}
}
Q5: How do I test a query manually?
A: Open /graphiql
in your Rails app (after install), and run this query:
{
user(id: 1) {
name
email
posts {
title
}
}
}
This helps you explore and debug queries in real-time.
โ Best Practices (with Examples)
1. Only ask for the data you need
GraphQL lets you choose fields โ donโt fetch everything by default.
# โ
Good
{
user(id: 1) {
name
email
}
}
# โ Bad (too much data)
{
user(id: 1) {
id
name
email
bio
avatar
posts {
id
title
comments {
id
content
}
}
}
}
2. Use pagination for large lists
Donโt fetch thousands of records in one query. Use first
or limit
for safety.
# โ
Paginated posts
{
posts(first: 10) {
edges {
node {
title
published
}
}
}
}
3. Validate arguments in resolvers
Always check for nil or invalid inputs to prevent errors.
def user(id:)
user = User.find_by(id: id)
return GraphQL::ExecutionError.new("User not found") unless user
user
end
4. Use context to handle authentication
Pass the current user using context and restrict access to queries.
def user(id:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
User.find(id)
end
5. Use query complexity limits
This prevents people from running super-heavy queries that slow down your app.
# config/initializers/graphql.rb
GraphQL::Analysis::AST::MaxQueryDepth.new(10)
GraphQL::Analysis::AST::MaxQueryComplexity.new(200)
6. Use tools like GraphiQL for testing
/graphiql
is included with the gem and lets you test your queries in the browser.
7. Organize query files by feature
Keep your schema clean by grouping types, queries, and resolvers into folders like:
types/user_type.rb
queries/user_query.rb
resolvers/user_resolver.rb
๐ Real-World Use Cases
- Mobile app: Get user, notifications, and settings in one request
- Dashboard: Fetch metrics, user data, and recent activity together
- Profile page: Load user info, posts, and comments in a single query
GraphQL Mutations in Rails
๐ง Detailed Explanation
Mutations in GraphQL are used when you want to change data.
Think of them like:
- ๐ Creating a new user
- โ๏ธ Updating a blog post
- โ Deleting a comment
In REST, you use POST
, PUT
, or DELETE
to do these actions. In GraphQL, you use mutations.
Example of a mutation:
mutation {
createUser(input: { name: "Ali", email: "[email protected]" }) {
user {
id
name
}
errors
}
}
What happens here:
- ๐ A user is created
- โ You get the new user info back
- โ If something goes wrong, you get a list of errors
Why itโs useful in Rails:
- You can handle form submissions easily (like sign-up)
- You can use ActiveRecord validations
- You get clean, structured responses โ including success or error
Summary: GraphQL mutations help your frontend and backend work together to save or change data โ all in one flexible request.
โ Best Implementation (with Detail)
This step-by-step guide shows how to implement a GraphQL mutation in Rails to create a user, using clean and secure practices.
๐ฆ Step 1: Install the GraphQL gem
Add GraphQL to your Rails app:
# In your terminal
bundle add graphql
rails generate graphql:install
This creates files like schema.rb
, query_type.rb
, and mutation_type.rb
.
๐งฑ Step 2: Create the Mutation File
Create a mutation for creating users:
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :name, String, required: true
argument :email, String, required: true
field :user, Types::UserType, null: true
field :errors, [String], null: false
def resolve(name:, email:)
user = User.new(name: name, email: email)
if user.save
{ user: user, errors: [] }
else
{ user: nil, errors: user.errors.full_messages }
end
end
end
end
โก๏ธ This handles both success and error states cleanly.
๐ Step 3: Register the Mutation
Now hook this mutation into the main mutation type:
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser
end
end
โ
This exposes the mutation to clients via /graphql
.
๐ Step 4: Add Authentication (optional but best practice)
Pass the current user from your controller into GraphQL context:
# app/controllers/graphql_controller.rb
def execute
context = {
current_user: current_user
}
...
end
Then use it in your mutation:
def resolve(name:, email:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
user = User.new(name: name, email: email)
...
end
๐งช Step 5: Test the Mutation in GraphiQL
Visit /graphiql
in your browser and run:
mutation {
createUser(input: { name: "Ali", email: "[email protected]" }) {
user {
id
name
email
}
errors
}
}
โ You should see a new user created, or a clear error message.
๐งผ Step 6: (Optional) Use Input Object for Clean Arguments
If your mutation needs many arguments, group them into an input object:
# app/graphql/types/inputs/user_input_type.rb
module Types
module Inputs
class UserInputType < Types::BaseInputObject
argument :name, String, required: true
argument :email, String, required: true
end
end
end
Then use:
argument :input, Types::Inputs::UserInputType, required: true
def resolve(input:)
...
end
๐ Summary
- โ Create a mutation file and register it
- โ Validate and return errors properly
- โ Use context for auth if needed
- โ Test it using GraphiQL or your frontend
- โ Use input objects for clean arguments
๐ This gives you a production-ready, testable, and clean GraphQL mutation in Rails!
๐ก Example
Mutation to create a user:
mutation {
createUser(input: { name: "Ali", email: "[email protected]" }) {
user {
id
name
email
}
errors
}
}
Response:
{
"data": {
"createUser": {
"user": {
"id": "1",
"name": "Ali",
"email": "[email protected]"
},
"errors": []
}
}
}
๐ Alternative Methods
- RESTful POST, PATCH, DELETE endpoints
- Forms with ActiveModel validations in Rails
โ General Q&A
Q1: Can a mutation return multiple fields?
A: Yes! You can return updated data and custom messages in one response.
Q2: Is it safe to update records this way?
A: Yes, if you validate input and use authorization in the resolver.
๐ ๏ธ Technical Questions & Answers
Q1: What is a mutation in GraphQL?
A: A mutation is how you change data โ like creating, updating, or deleting something (like users, posts, or comments).
Think of it like: a form submission that saves something to the database.
Q2: How do I create a mutation in Rails?
A: You create a file in app/graphql/mutations/
and define your logic there.
Example: Create a new user:
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :name, String, required: true
argument :email, String, required: true
field :user, Types::UserType, null: true
field :errors, [String], null: false
def resolve(name:, email:)
user = User.new(name: name, email: email)
if user.save
{ user: user, errors: [] }
else
{ user: nil, errors: user.errors.full_messages }
end
end
end
end
Q3: How do I connect this mutation to my schema?
A: Add it to your mutation type like this:
# app/graphql/types/mutation_type.rb
field :create_user, mutation: Mutations::CreateUser
Now you can call it from the frontend or GraphiQL!
Q4: What does the frontend mutation look like?
mutation {
createUser(input: { name: "Ali", email: "[email protected]" }) {
user {
id
name
}
errors
}
}
Expected Output:
{
"data": {
"createUser": {
"user": {
"id": "1",
"name": "Ali"
},
"errors": []
}
}
}
Q5: How do I show an error if something goes wrong?
A: Use GraphQL::ExecutionError
or return an error array:
return GraphQL::ExecutionError.new("Something went wrong")
OR inside mutation response:
{ user: nil, errors: ["Email can't be blank"] }
โ Best Practices (with Examples)
1. Always validate input before saving
Never save data directly. Use ActiveRecord validations and return clean errors.
def resolve(name:, email:)
user = User.new(name: name, email: email)
if user.save
{ user: user, errors: [] }
else
{ user: nil, errors: user.errors.full_messages }
end
end
2. Return both data and errors
This gives the frontend full control to show results or messages.
field :user, Types::UserType, null: true
field :errors, [String], null: false
3. Use descriptive field names
Instead of create
, use createUser
or updatePost
so itโs clear what each mutation does.
field :create_user, mutation: Mutations::CreateUser
4. Handle authentication using context
Check if the user is logged in before allowing a mutation.
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
5. Use service objects for complex logic
If the mutation does a lot (e.g., saving records + sending emails), move logic to a service.
def resolve(input)
result = UserCreatorService.call(input)
if result.success?
{ user: result.user, errors: [] }
else
{ user: nil, errors: result.errors }
end
end
6. Use input objects for clean argument handling
When a mutation needs many fields, use an input type.
# Inside your mutation
argument :input, Types::UserInputType, required: true
It keeps things clean and organized.
7. Donโt expose sensitive fields
In your UserType
, avoid fields like password, token, etc.
field :email, String, null: false
# โ Don't add :password or :token
8. Keep response structure consistent
All your mutations should return a similar structure: data + errors.
This helps the frontend handle responses easily.
๐ Real-World Use Cases
- โ Creating accounts in a sign-up form
- โ Submitting a blog post or comment
- โ Updating a user profile or password
- โ Deleting a product from an inventory system
GraphQL Subscriptions in Rails
๐ง Detailed Explanation
GraphQL Subscriptions let your app get live updates from the server when something changes โ without refreshing the page.
๐ฆ Imagine a chat app: when someone sends a message, you want other users to see it right away. Thatโs what subscriptions are for.
In simple words: Subscriptions = โTell me when this thing happens.โ
โ Example use cases:
- New chat messages
- Live notifications
- Real-time updates on stock prices or delivery status
โ๏ธ In Rails, subscriptions are powered by ActionCable, which uses WebSockets (a live connection between the browser and the server).
How it works:
- Frontend subscribes to something (like โnewMessageโ)
- Server keeps listening through a WebSocket
- When data changes, the server pushes the update to the frontend
Difference from normal queries:
- Queries = โGive me data nowโ (one-time request)
- Subscriptions = โKeep me updated whenever this changesโ (live connection)
Summary: GraphQL Subscriptions help your app stay live and updated in real-time โ perfect for chat, alerts, dashboards, and anything that changes frequently.
โ Best Implementation (with Detail)
This guide will help you set up GraphQL subscriptions in a Rails app using graphql-ruby and ActionCable.
๐ฆ Step 1: Install GraphQL and ActionCable support
Make sure your app has graphql
and WebSocket support:
# In your Gemfile
gem 'graphql'
# Then install:
bundle install
rails generate graphql:install
This sets up your GraphQL schema and mounts it at /graphql
.
๐ง Step 2: Enable ActionCable for WebSockets
Mount the ActionCable server in your app:
# config/routes.rb
mount ActionCable.server => '/cable'
And configure your app to use ActionCable:
# config/application.rb
config.action_cable.mount_path = '/cable'
โ๏ธ Step 3: Enable subscriptions in your schema
Edit your main schema file:
# app/graphql/your_app_schema.rb
class YourAppSchema < GraphQL::Schema
use GraphQL::Subscriptions::ActionCableSubscriptions
subscription(Types::SubscriptionType)
end
๐ This tells GraphQL to use ActionCable to send real-time updates.
๐ก Step 4: Define your subscription type
# app/graphql/types/subscription_type.rb
module Types
class SubscriptionType < Types::BaseObject
field :message_added, subscription: Subscriptions::MessageAdded
end
end
This registers your live field: messageAdded
.
โ๏ธ Step 5: Create the subscription class
# app/graphql/subscriptions/message_added.rb
module Subscriptions
class MessageAdded < GraphQL::Schema::Subscription
field :message, Types::MessageType, null: false
argument :room_id, ID, required: true
def subscribe(room_id:)
# Client starts listening to this room
{}
end
def update(room_id:)
# Server pushes this when something changes
{ message: object }
end
end
end
Note: object
comes from your appโs logic (e.g., a created message).
๐ Step 6: Trigger the subscription when data changes
# In a mutation or controller (e.g., create_message)
YourAppSchema.subscriptions.trigger(
:message_added,
{ room_id: message.room_id },
message
)
๐ This line sends the update to all users subscribed to that room.
๐งช Step 7: Test it in GraphiQL
Go to /graphiql
and try this:
subscription {
messageAdded(roomId: 1) {
message {
id
content
sender {
name
}
}
}
}
โ
When a message is created for roomId: 1
, this subscription receives it automatically.
๐ Summary
- โ Subscriptions let clients receive live updates
- โ Powered by ActionCable + WebSockets
- โ Defined just like GraphQL types
- โ Triggered manually when data changes
- โ Useful for real-time apps (chat, notifications, dashboards)
๐ You now have real-time GraphQL subscriptions working inside a Rails app!
๐ก Example
Subscribe to a new message:
subscription {
messageAdded(roomId: 1) {
id
content
sender {
name
}
}
}
Every time a new message is posted in that room, the client receives the update automatically.
๐ Alternative Concepts
- Polling API every few seconds
- Server-Sent Events (SSE)
- Manual WebSocket implementation
โ General Q&A
Q1: Do subscriptions replace WebSockets?
A: No. GraphQL subscriptions are built on WebSockets, but offer a structured, query-like interface for events.
Q2: Is it required for every app?
A: No. Use subscriptions when real-time updates are necessary (chat, live dashboard, notifications).
๐ ๏ธ Technical Questions & Answers
Q1: What is a GraphQL Subscription?
A: A subscription is a way for the client to โstay connectedโ and get real-time updates from the server whenever something changes.
Itโs like saying: โHey server, let me know when a new message is sent.โ
Q2: How does it work in Rails?
A: Subscriptions use ActionCable under the hood, which runs on WebSockets.
Hereโs how it works:
- ๐ฅ๏ธ The client sends a
subscription
request (likemessageAdded
). - ๐ Rails opens a WebSocket connection using ActionCable.
- ๐ข When a new message is created, Rails sends that data to all subscribed clients.
Q3: How do I define a subscription in Rails?
A: Create a new subscription class in app/graphql/subscriptions/
like this:
# app/graphql/subscriptions/message_added.rb
module Subscriptions
class MessageAdded < GraphQL::Schema::Subscription
field :message, Types::MessageType, null: false
argument :room_id, ID, required: true
def subscribe(room_id:)
# Called when client starts listening
{}
end
def update(room_id:)
# Called when you trigger this subscription
{ message: object }
end
end
end
Q4: How do I trigger a subscription when new data is created?
A: Call the following method in your mutation or service:
YourAppSchema.subscriptions.trigger(
:message_added, # subscription name
{ room_id: message.room_id }, # subscription arguments
message # data to send
)
This notifies everyone subscribed to room_id
.
Q5: How do I test my subscription?
A: Use /graphiql
in the browser and run:
subscription {
messageAdded(roomId: 1) {
message {
id
content
}
}
}
Then in another tab, run a mutation to create a message. You’ll see this subscription auto-update!
Q6: Do subscriptions work with HTTP?
A: No. Subscriptions require a WebSocket connection (like ActionCable), not regular HTTP requests.
Q7: What happens if the user disconnects?
A: The subscription stops. They need to reconnect and resubscribe to start getting updates again.
โ Best Practices (with Examples)
1. Use subscriptions only when needed
Subscriptions use WebSockets and server resources, so only use them when you need real-time updates.
- โ Good: Chat apps, notifications, live dashboards
- โ Bad: Static content like blog posts or FAQ pages
2. Scope subscriptions by context (e.g., room or user)
This ensures that each client only receives relevant updates.
# Trigger only users subscribed to a specific room
YourAppSchema.subscriptions.trigger(
:message_added,
{ room_id: message.room_id },
message
)
3. Authenticate users before allowing subscriptions
Pass current_user
into the GraphQL context and use it to authorize the subscription.
# app/controllers/graphql_controller.rb
def execute
context = { current_user: current_user }
...
end
def subscribe(room_id:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
{}
end
4. Return a consistent structure
Make sure your subscription always returns the same fields. This helps the frontend know what to expect.
field :message, Types::MessageType, null: false
def update(room_id:)
{ message: object }
end
5. Use background jobs or services to broadcast
If your subscription trigger is part of a larger process (like saving a message), use a service or job to keep things clean.
class MessageBroadcaster
def self.broadcast(message)
YourAppSchema.subscriptions.trigger(
:message_added,
{ room_id: message.room_id },
message
)
end
end
6. Monitor and limit active subscriptions
Subscriptions keep connections open, so monitor how many users are connected and limit them if needed.
- โ Use Redis for pub/sub scale
- โ Add logging or metrics to track connections
7. Clean up resources when users disconnect
ActionCable automatically closes WebSocket connections, but make sure your app handles disconnects properly and unsubscribes the user.
Tip: Add cleanup hooks if you’re storing subscription state manually (e.g., in Redis).
๐ Real-World Use Cases
- ๐ฌ Live chat (e.g., new messages in real time)
- ๐ Dashboard with live metrics
- ๐ Real-time notifications and alerts
- ๐ฅ User status updates (e.g., online/offline)
- ๐ฆ Live order tracking in e-commerce
GraphQL Schema in Rails
๐ง Detailed Explanation
The GraphQL Schema is like the blueprint of your API.
It tells your app and the frontend:
- ๐ What data can be asked for (like users, posts, comments)
- ๐งพ What actions can be performed (queries, mutations, subscriptions)
- ๐ How different types are connected
In a GraphQL + Rails app, your schema is usually defined in this file:
app/graphql/your_app_schema.rb
Hereโs how a simple schema looks in Rails:
class MyAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
subscription(Types::SubscriptionType)
end
That means:
- ๐ฅ
QueryType
handles getting data (likeuser
orpost
) - โ๏ธ
MutationType
handles actions likecreateUser
orupdatePost
- ๐
SubscriptionType
sends real-time updates (optional)
Every time you add a new field or type, youโre telling the schema: โHey, this is now part of the API!โ
Why itโs important:
- โ It defines what clients can request or do
- โ It auto-generates docs (in tools like GraphiQL or Apollo)
- โ It helps prevent invalid requests by validating everything first
Think of it like: a menu in a restaurant โ it tells you exactly whatโs available and how you can order it.
Summary: The schema is the brain of your GraphQL API โ it connects types, queries, mutations, and rules all in one place.
โ Best Implementation (with Detail)
Hereโs how to set up and organize your GraphQL schema in a Rails app using graphql-ruby
.
๐ฆ Step 1: Install the GraphQL gem
# In your terminal
bundle add graphql
rails generate graphql:install
This command sets up the initial schema file at:
app/graphql/your_app_schema.rb
๐ Step 2: Understand the Schema File
This is the main file that connects your queries, mutations, and subscriptions:
class YourAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
subscription(Types::SubscriptionType) # Optional
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
end
- query: for read operations
- mutation: for create/update/delete
- subscription: for real-time updates
๐งฑ Step 3: Add Query & Mutation Types
Create a new file to define what can be queried:
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
end
end
And for mutations:
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser
end
end
๐ Step 4: Define Custom Types
Create a UserType
to describe user fields:
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
end
end
This controls what fields are returned to the frontend.
โ Step 5: Organize Your Schema Cleanly
- Put all types in
app/graphql/types/
- Put all mutations in
app/graphql/mutations/
- Put subscriptions in
app/graphql/subscriptions/
(if needed) - Split large types into folders (e.g.,
Types::Admin::UserType
)
๐ก This keeps your code easy to navigate as the app grows.
๐งช Step 6: Test with GraphiQL
Visit /graphiql
in your browser and test this:
{
user(id: 1) {
id
name
email
}
}
โ
Youโll get exactly the fields you defined in UserType
.
๐ Step 7: Use Context for Auth in Schema Logic
Pass the current user in your controller:
# app/controllers/graphql_controller.rb
def execute
context = { current_user: current_user }
...
end
Then use it in your schema queries:
def user(id:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
User.find_by(id: id)
end
๐ Summary
- โ The schema file connects query, mutation, and subscription types
- โ Each type is defined in its own file for clarity
- โ Only expose the fields your frontend needs
- โ Use context to handle auth and access control
- โ Test schema interactions using GraphiQL
๐ฏ This is the cleanest and most scalable way to manage your GraphQL schema in Rails.
๐ก Example
Basic Schema Definition in Rails:
class MyAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
subscription(Types::SubscriptionType)
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
use GraphQL::Subscriptions::ActionCableSubscriptions
end
This connects your QueryType
, MutationType
, and SubscriptionType
.
๐ Alternative Concepts
- REST APIs use controllers and routes
- OpenAPI or Swagger schemas for REST docs
- gRPC IDL files (Interface Definition Language)
โ General Questions & Answers
Q1: Do I need a schema file in Rails?
A: Yes! It defines the whole structure of your GraphQL API โ what clients can ask for and how.
Q2: Can I split schema parts into smaller files?
A: Yes. You can organize it with Types::QueryType
, Types::UserType
, and custom resolvers.
๐ ๏ธ Technical Questions & Answers
Q1: What is a GraphQL Schema in Rails?
A: Itโs the main file that connects all parts of your GraphQL API: queries, mutations, and subscriptions. It defines what data the frontend can ask for and how.
Example schema:
class MyAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
subscription(Types::SubscriptionType) # optional
end
Q2: Where is the schema file located in a Rails project?
A: After running rails generate graphql:install
, the file is located at:
app/graphql/your_app_schema.rb
This file is automatically connected to your GraphQL controller.
Q3: How do I add a new query to the schema?
A: You add it inside Types::QueryType
.
# app/graphql/types/query_type.rb
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
And your main schema file must reference this type:
query(Types::QueryType)
Q4: How do I organize a large schema?
A: Split each part into its own file:
UserType
โapp/graphql/types/user_type.rb
CreateUser
โapp/graphql/mutations/create_user.rb
PostType
โapp/graphql/types/post_type.rb
Use folders for large domains, e.g.:
app/graphql/types/admin/user_type.rb
Q5: Can I secure queries using the schema?
A: Yes. Use the context
object to check for authentication or roles.
def user(id:)
raise GraphQL::ExecutionError, "Not authorized" unless context[:current_user]
User.find_by(id: id)
end
Q6: What if a requested field doesnโt exist in the schema?
A: GraphQL returns an error like:
{
"errors": [
{
"message": "Field 'emailAddress' doesn't exist"
}
]
}
๐ก Tip: Only expose the fields you want the client to use by defining them in your types.
Q7: How do I test my schema manually?
A: Visit /graphiql
in your browser and run a query:
{
user(id: 1) {
name
email
}
}
If the schema is set up properly, youโll get a real-time response with exactly those fields.
โ Best Practices (with Examples)
1. Organize your schema into types, queries, and mutations
Keep each responsibility in its own file for clarity and scalability.
- โ
QueryType
โ for all read operations - โ
MutationType
โ for create/update/delete actions - โ
UserType
,PostType
, etc. โ for reusable field definitions
Example:
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
end
end
2. Keep schema logic thin โ use resolvers or service objects
Donโt write heavy logic directly in the schema. Use service classes to handle business logic.
Example:
def user(id:)
FindUserService.call(id)
end
3. Use descriptive field and argument names
Make your API self-explanatory to frontend developers.
# Good
field :recent_posts, [PostType], null: false
# Avoid vague names
field :data, String, null: false
4. Use input types for complex arguments
Instead of many flat arguments, group them into a custom input object.
Example:
# Types::Inputs::UserInputType
class UserInputType < Types::BaseInputObject
argument :name, String, required: true
argument :email, String, required: true
end
# In mutation
argument :input, Types::Inputs::UserInputType, required: true
5. Only expose fields you want clients to access
Donโt add sensitive fields like password
or token
in your type definitions.
# Good
field :email, String, null: false
# โ Avoid this
field :encrypted_password, String, null: false
6. Add field descriptions for automatic documentation
Use the description
keyword to help frontend developers understand your API.
field :email, String, null: false, description: "User's email address"
7. Use nullability wisely
Set null: false
if a field must always be present. Use null: true
only when it’s optional.
field :id, ID, null: false # always required
field :bio, String, null: true # optional
8. Add query complexity limits (for performance)
Prevent overly deep or expensive queries using built-in tools.
# config/initializers/graphql.rb
GraphQL::Analysis::AST::MaxQueryDepth.new(10)
GraphQL::Analysis::AST::MaxQueryComplexity.new(200)
9. Test your schema using GraphiQL
Use the in-browser GraphiQL IDE at /graphiql
to test all your types and fields.
{
user(id: 1) {
name
email
}
}
10. Document your schema and keep it clean
Maintain a readable schema layout so future devs (or you) can easily understand and extend it.
๐ Real-World Use Cases
- ๐ Building a single API for both web and mobile
- ๐ ๏ธ Auto-generating documentation tools like GraphiQL
- ๐ฆ Structuring data flow across large teams in microservices
- ๐ฌ Creating a strongly-typed API for chats, posts, or analytics dashboards
GraphQL Resolvers in Rails
๐ง Detailed Explanation
Resolvers are the part of GraphQL that actually โgetsโ the data you ask for in a query.
When someone writes a query like:
{
user(id: 1) {
name
email
}
}
๐ The resolver is the Ruby method that finds and returns that user from the database.
Where do resolvers live?
- In
Types::QueryType
โ for โreadโ queries - In
Types::MutationType
โ for โwriteโ actions - Or in separate resolver classes (optional for larger apps)
Example: A simple resolver that returns a user by ID:
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
๐ก This tells GraphQL: โIf someone asks for a user by ID, hereโs how to find it.โ
What makes resolvers powerful?
- ๐ฏ You can filter, sort, or customize what gets returned
- ๐ You can check if a user is logged in before returning data
- โ๏ธ You can connect to databases, APIs, or services
Think of it like: A waiter (resolver) who listens to your order (query) and brings back exactly what you asked for (data).
Summary: Resolvers are the methods behind your GraphQL API โ they do the real work of finding and returning the data for every field.
โ Best Implementation (with Detail)
Resolvers are the methods that tell GraphQL how to fetch the data for each field.
Letโs go step by step to implement a clean, secure, and organized resolver in a Rails + GraphQL app.
๐ฆ Step 1: Install GraphQL in your Rails app
# In your terminal
bundle add graphql
rails generate graphql:install
This generates your schema and base files like:
app/graphql/types/query_type.rb
app/graphql/your_app_schema.rb
๐งฑ Step 2: Add a field with arguments to your Query type
Letโs say we want to fetch a user by ID. Add this to QueryType
:
# app/graphql/types/query_type.rb
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
This declares that your API supports user(id: ID)
queries.
โ๏ธ Step 3: Write the resolver method
Right below the field, add the resolver method:
def user(id:)
User.find_by(id: id)
end
โ
Now when someone queries user(id: 1)
, this method runs and returns the matching user.
๐ Step 4: Use context for authentication
Pass the current user in your controller:
# app/controllers/graphql_controller.rb
def execute
context = {
current_user: current_user
}
...
end
Then check it in your resolver:
def user(id:)
raise GraphQL::ExecutionError, "Not authorized" unless context[:current_user]
User.find_by(id: id)
end
๐งผ Step 5: Move complex logic to a service
Keep resolvers clean by moving logic to a service object:
def user(id:)
UserFetcherService.call(id)
end
# app/services/user_fetcher_service.rb
class UserFetcherService
def self.call(id)
User.find_by(id: id)
end
end
โ This makes your GraphQL code easy to read and test.
๐งช Step 6: Test the resolver in GraphiQL
Visit /graphiql
and try:
{
user(id: 1) {
id
name
email
}
}
You should get a clean JSON response if everything works.
โ Step 7: Reuse logic for other resolvers
For example, reuse the current_user
check in multiple places:
def current_user!
context[:current_user] || raise GraphQL::ExecutionError, "Please log in"
end
Now you can call current_user!
in any resolver.
๐ Summary
- โ Define your GraphQL fields with arguments
- โ Write resolvers that return exactly whatโs asked
- โ Use context for authentication
- โ Extract business logic into service objects
- โ Keep resolvers short and reusable
๐ฏ Following this structure keeps your GraphQL API clean, scalable, and easy to debug.
๐ก Example
Example resolver inside QueryType
:
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
Here, the resolver is the def user
method that returns the user with the given ID.
๐ Alternative Concepts
- REST controller actions (like
UsersController#show
) - Service objects (recommended for complex logic)
โ General Questions & Answers
Q1: Do all GraphQL fields need a resolver?
A: Yes โ either explicitly (you write the method), or implicitly (it matches a model field automatically).
Q2: Can I reuse resolvers?
A: Yes. You can extract them into classes or service objects for reuse across types.
๐ ๏ธ Technical Questions & Answers
Q1: What is a resolver in GraphQL?
A: A resolver is the Ruby method that tells GraphQL how to get the data for a specific field.
Example: If you query user(id: 1)
, the resolver will find and return that user.
Q2: Where do I write resolvers in a Rails app?
A: Inside your Types::QueryType
(for fetching data) or Types::MutationType
(for changing data).
# app/graphql/types/query_type.rb
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
Q3: Can I use arguments in resolvers?
A: Yes. You can define them like this:
field :posts_by_author, [Types::PostType], null: false do
argument :author_id, ID, required: true
end
def posts_by_author(author_id:)
Post.where(author_id: author_id)
end
Q4: How do I access the current user in a resolver?
A: You use the context
hash. First, pass it in your controller:
# app/controllers/graphql_controller.rb
context = {
current_user: current_user
}
Then in your resolver:
def user(id:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
User.find_by(id: id)
end
Q5: Can I reuse logic across resolvers?
A: Yes. Use service classes or helper methods.
# Reusable method in ApplicationResolver
def current_user!
context[:current_user] || raise GraphQL::ExecutionError, "Please log in"
end
Then use it in any resolver:
def user(id:)
current_user!
User.find_by(id: id)
end
Q6: What happens if a resolver raises an error?
A: GraphQL catches it and sends a structured error to the client.
def user(id:)
raise GraphQL::ExecutionError, "User not found" unless User.exists?(id: id)
User.find(id)
end
Frontend will receive:
{
"errors": [{ "message": "User not found" }]
}
Q7: Can resolvers return nested data?
A: Yes. If you query a user and their posts, GraphQL will call the user resolver, then call the posts
method on the returned user object.
# Query
{
user(id: 1) {
name
posts {
title
}
}
}
Each part is resolved step by step, automatically.
โ Best Practices (with Examples)
1. Keep resolvers short and focused
Donโt put complex logic inside the resolver method. Use service objects or plain Ruby classes to keep it clean.
# โ Avoid
def user(id:)
user = User.find(id)
log_access(user)
notify_admin(user)
user
end
# โ
Better
def user(id:)
UserFetcher.call(id)
end
2. Use service objects for business logic
# app/services/user_fetcher.rb
class UserFetcher
def self.call(id)
User.find_by(id: id)
end
end
3. Always handle missing or invalid data
Raise user-friendly errors with GraphQL::ExecutionError
.
def user(id:)
user = User.find_by(id: id)
raise GraphQL::ExecutionError, "User not found" unless user
user
end
4. Use context for authentication and authorization
Never allow sensitive data without checking context[:current_user]
.
def user(id:)
raise GraphQL::ExecutionError, "Unauthorized" unless context[:current_user]
User.find_by(id: id)
end
5. Use descriptive field and argument names
This improves clarity in the frontend and auto-generated docs.
field :posts_by_author, [Types::PostType], null: false do
argument :author_id, ID, required: true
end
6. Return only whatโs defined in the schema
The resolver should match the shape and fields expected by the GraphQL type.
# If your PostType has title and body, don't return unrelated data
def post(id:)
Post.select(:id, :title, :body).find_by(id: id)
end
7. Avoid N+1 queries with eager loading
Use .includes
or .preload
to reduce DB hits when resolving nested associations.
def posts_by_author(author_id:)
Post.includes(:comments).where(author_id: author_id)
end
8. Use input objects for complex arguments
This makes mutations and filters cleaner and reusable.
# Input
argument :filter, Types::Inputs::PostFilterInput, required: false
# Usage
def posts(filter:)
PostFilterService.call(filter)
end
9. Avoid returning nil silently
When something is missing or fails, return a meaningful message.
raise GraphQL::ExecutionError, "Post not found"
10. Test your resolvers independently
Write unit tests for your resolver methods or service objects to ensure logic works as expected.
๐ Real-World Use Cases
- ๐ Returning the current userโs data securely
- ๐ Fetching all blog posts by a specific author
- ๐ Filtering dashboard data based on role
- ๐ฏ Displaying search results based on filters and keywords
Install GraphQL Gem in Rails
๐ง Detailed Explanation
To use GraphQL in a Rails app, the first thing you need to do is install the graphql
gem.
This gem gives your app the power to:
- โ Build flexible APIs where the client chooses what data to receive
- โ Write queries instead of multiple REST endpoints
What happens when you install it?
- ๐ง It creates a
schema
file to define your API structure - ๐ It adds a
QueryType
andMutationType
file for reading and writing data - ๐งช It sets up a browser tool called
GraphiQL
to test your queries
Steps to install:
1. Add the gem:
bundle add graphql
2. Run the installer:
rails generate graphql:install
After that, your app is ready to handle GraphQL queries at the /graphql
endpoint!
Summary: Installing the graphql
gem is the first and easiest step to turning your Rails app into a powerful, flexible API system โ ready for modern web and mobile development.
๐ก Examples
# Step 1: Add the gem
bundle add graphql
# Step 2: Run the installer
rails generate graphql:install
๐ This sets up:
app/graphql/your_app_schema.rb
app/graphql/types/query_type.rb
/graphql
and/graphiql
routes
๐ Alternative Concepts
- REST API setup with
jbuilder
oractive_model_serializers
- Using GraphQL via Apollo Server in a separate Node.js service
- GraphQL client libraries like Apollo Client or Relay for frontend
โ General Q&A
Q1: Is the graphql gem maintained?
A: Yes! Itโs maintained by the community and works well with modern Rails apps.
Q2: Is it beginner-friendly?
A: Absolutely. The graphql:install
command sets up everything you need to get started.
๐ ๏ธ Technical Q&A
Q: Do I need to restart my server after installation?
A: Yes. After adding a gem and generating files, restart your Rails server to load everything.
Q: What versions are supported?
A: The gem works well with Rails 6+ and Ruby 2.7+. It also supports Rails 7 and Ruby 3.x.
Q: Can I remove it later?
A: Yes. You can remove the gem and delete the graphql
folder if you ever switch back to REST.
โ Best Practices
- ๐ฆ Install the gem in development first, test it, then move to production
- ๐งฑ Commit the generated GraphQL files right after installation
- ๐งช Open
/graphiql
after setup to confirm installation is successful - ๐๏ธ Use version constraints in
Gemfile
to avoid unexpected upgrades
๐ Real-World Use Cases
- ๐ E-commerce apps using a single GraphQL API for both mobile and web clients
- ๐ Dashboards and admin panels needing custom filtered data from multiple sources
- ๐งฉ Headless CMS or frontend-backend split systems using GraphQL for API integration
Setup GraphQL in Rails using rails generate graphql:install
๐ง Detailed Explanation
After you run rails generate graphql:install
, your Rails app becomes ready to handle GraphQL requests at the /graphql
endpoint.
You can test if everything works by sending a special query called an โintrospection query.โ It checks if the GraphQL schema exists and is working.
โ Sample Query to Test Setup
This is the simplest query to check if GraphQL is installed correctly:
{
"query": "query { __schema { queryType { name } } }"
}
If everything is working, the response will look like this:
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
}
}
}
}
๐งช Test with cURL in Terminal
Run this command in your terminal (replace the port if needed):
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query { __schema { queryType { name } } }"}'
โ
You should see a JSON response with "name": "Query"
.
๐งช Test with Postman
- ๐ง Open Postman
- Set method to
POST
- Set URL to:
http://localhost:3000/graphql
- Go to the Body tab, select
raw
+JSON
- Paste this JSON:
{
"query": "query { __schema { queryType { name } } }"
}
โ Click โSendโ and you should get a JSON response confirming your GraphQL setup is working.
๐ Optional: Add Authentication Headers
If your GraphQL API requires a user to be logged in, you may need to pass an Authorization
token:
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"query":"query { __schema { queryType { name } } }"}'
You can also add this header in Postman under the โHeadersโ tab.
๐ Summary
- โ
rails generate graphql:install
creates all needed files - โ
You can test GraphQL immediately at
/graphql
- โ
Use
curl
or Postman to send queries - โ Use the introspection query to make sure your schema is active
๐ If the query returns the schema name, your GraphQL setup is complete!
๐ก Example
# Step 1: Install the gem
bundle add graphql
# Step 2: Run the installer
rails generate graphql:install
๐ This command creates:
app/graphql/your_app_schema.rb
โ main schema fileTypes::QueryType
โ for GET-like queriesTypes::MutationType
โ for POST/update/deleteapp/controllers/graphql_controller.rb
โ handles GraphQL requests/graphiql
โ web interface to test queries
๐ Alternative Concepts
- Manual schema creation without using the generator
- Running GraphQL as a separate service (e.g., Apollo Server)
- Using REST with
active_model_serializers
instead of GraphQL
โ General Questions & Answers
Q1: What does this command actually do?
A: It scaffolds the GraphQL folder, creates a schema, sets up query/mutation structure, and mounts the GraphQL API at /graphql
.
Q2: Do I need to configure anything manually?
A: Not at first! You can use the generated files right away and add more types as needed.
๐ ๏ธ Technical Questions & Answers
Q: Where are queries handled?
A: In app/graphql/types/query_type.rb
. You define fields and resolvers there.
Q: Can I test queries immediately?
A: Yes! Visit http://localhost:3000/graphiql
and use the test UI.
Q: How do I enable authentication?
A: You can pass the current user in context
inside the controller:
context = { current_user: current_user }
โ Best Practices
- โ Run the install command after gem installation, not before
- โ Check the generated files into version control
- โ Use GraphiQL to explore and test before frontend is ready
- โ Add version constraints to the gem to avoid breaking changes
๐ Real-World Use Cases
- ๐ Building one API that serves both web and mobile clients
- ๐ฆ Backend for a React or Vue frontend (Headless architecture)
- ๐ง Prototyping APIs quickly without writing multiple controllers
GraphQL Playground Interface in Rails
๐ง Detailed Explanation
After setting up GraphQL in Rails using rails generate graphql:install
, you donโt automatically get the GraphiQL UI (the browser playground).
To enable this, you need to manually install the graphiql-rails
gem.
โ Steps to Install and Enable GraphQL Playground
1. Add the gem to your Gemfile
(development only):
group :development do
gem 'graphiql-rails', '~> 1.8'
end
2. Run:
bundle install
3. Mount it in your config/routes.rb
:
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
4. Start your server and visit:
http://localhost:3000/graphiql
๐ Now you have a full-featured GraphQL browser playground to:
- ๐จ Write and send GraphQL queries
- ๐ Explore your schema
- ๐งช Debug and preview data live
๐ Summary
- ๐ซ
rails generate graphql:install
does NOT install GraphiQL UI - โ
You must install
graphiql-rails
gem manually - โ
Only mount it in
development
to keep production safe
๐ฏ Once set up, GraphiQL becomes a powerful tool for building and testing your GraphQL API inside Rails!
๐ก Example Usage
Try this inside the browser playground:
{
__schema {
types {
name
}
}
}
It will return a list of all types in your schema.
๐ Alternative Concepts
- Using standalone
Apollo Studio
orAltair
GraphQL tools - Postman for sending raw GraphQL queries
- GraphQL Voyager for visualizing schemas
โ General Questions & Answers
Q1: Is this playground safe for production?
A: No. You should only enable GraphiQL in development mode. It’s disabled automatically in production for security.
Q2: Do I need to install anything extra?
A: No. GraphiQL is included by default when you install the graphql
gem and run the install generator.
๐ ๏ธ Technical Questions & Answers
Q: How is the GraphQL endpoint defined?
A: In your routes:
# config/routes.rb
post "/graphql", to: "graphql#execute"
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
Q: Can I use headers (like auth tokens) in GraphiQL?
A: Yes! Click the “Headers” tab and add something like:
{
"Authorization": "Bearer YOUR_TOKEN"
}
Q: Can I test mutations too?
A: Absolutely. Just paste your mutation block into the editor.
โ Best Practices
- โ
Use GraphiQL only in
development
mode - โ Use it to explore your schema as you build types and fields
- โ Test complex queries before connecting the frontend
- โ Use the “Docs” sidebar to understand available types
๐ Real-World Use Cases
- ๐ Quickly testing queries before implementing frontend logic
- ๐ Using Docs to onboard new team members or frontend devs
- ๐ Exploring a large schema without opening multiple files
- ๐ Debugging failing GraphQL queries with real-time results
Define Schema & Root Types in GraphQL (Rails)
๐ง Detailed Explanation
In GraphQL, everything starts with a Schema. It’s like the brain of your API.
Your schema tells Rails what your app can ask for (queries) and do (mutations).
There are two main parts:
- QueryType: Think of it as reading data (like โget all photosโ)
- MutationType: This is for changing data (like โcreate a new userโ)
๐ง Example Breakdown
1. Schema: Connects the two main types
class MyAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
end
2. QueryType: Defines what you can fetch
module Types
class QueryType < Types::BaseObject
field :photos, [Types::PhotoType], null: false
def photos
Photo.all
end
end
end
3. MutationType: Defines what you can change
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser
end
end
๐ฏ Summary:
- โ Schema connects QueryType and MutationType
- โ QueryType is for reading/fetching
- โ MutationType is for writing/updating
- โ Each field runs a method or mutation to get the job done
This structure makes your API clear, fast, and customizable.
๐๏ธ Best Implementation (Step-by-Step)
Letโs build a simple photo API where users can fetch photos and create a photo using GraphQL in a Rails app.
1. โ Schema File
This connects your appโs entry points: QueryType and MutationType.
# app/graphql/photo_app_schema.rb
class PhotoAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
end
2. โ Define QueryType
Here you add fields that can be used to fetch data.
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :photos, [Types::PhotoType], null: false
def photos
Photo.all
end
end
end
Explanation: This defines a photos
query that returns all photos from the database.
3. โ Create a PhotoType
This defines what fields are exposed for a photo.
# app/graphql/types/photo_type.rb
module Types
class PhotoType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :image_url, String, null: true
end
end
4. โ Define MutationType
Connect your mutation classes here.
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_photo, mutation: Mutations::CreatePhoto
end
end
5. โ Create the CreatePhoto Mutation
This mutation creates a new photo in the database.
# app/graphql/mutations/create_photo.rb
module Mutations
class CreatePhoto < BaseMutation
argument :title, String, required: true
argument :image_url, String, required: false
field :photo, Types::PhotoType, null: true
field :errors, [String], null: false
def resolve(title:, image_url: nil)
photo = Photo.new(title: title, image_url: image_url)
if photo.save
{ photo: photo, errors: [] }
else
{ photo: nil, errors: photo.errors.full_messages }
end
end
end
end
6. โ Add a GraphQL Controller
GraphQL needs a single endpoint to work.
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
def execute
result = PhotoAppSchema.execute(
params[:query],
variables: params[:variables],
context: {},
operation_name: params[:operationName]
)
render json: result
end
end
And in routes.rb
:
post "/graphql", to: "graphql#execute"
๐ Done!
You now have a fully functional schema with Query and Mutation types.
Use the GraphiQL interface (or Postman/cURL) to test:
query {
photos {
id
title
}
}
mutation {
createPhoto(input: { title: "Sunset", imageUrl: "http://..." }) {
photo {
id
title
}
errors
}
}
๐ก Examples
Example: QueryType
module Types
class QueryType < Types::BaseObject
field :photos, [Types::PhotoType], null: false
def photos
Photo.all
end
end
end
Example: MutationType
module Types
class MutationType < Types::BaseObject
field :create_photo, mutation: Mutations::CreatePhoto
end
end
๐ Alternative Concepts
- Using multiple mutation classes for complex apps
- Combining queries and mutations in a single schema file (not recommended)
- Schema stitching (for microservices or modular APIs)
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: Where is the GraphQL schema defined in a Rails app?
A: The schema is typically defined in app/graphql/your_app_schema.rb
. It connects the root QueryType
and MutationType
.
class MyAppSchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
end
Q2: Whatโs the purpose of Types::QueryType
?
A: QueryType
defines how to fetch/read data. It maps query fields to methods that return data.
field :users, [Types::UserType], null: false
def users
User.all
end
Q3: How do I pass arguments to a query or mutation?
A: Use the argument
keyword and reference it in the resolver method.
# For Mutation
argument :title, String, required: true
def resolve(title:)
Photo.create(title: title)
end
Q4: How do I handle errors in GraphQL mutations?
A: Return both the result and a list of errors.
if user.save
{ user: user, errors: [] }
else
{ user: nil, errors: user.errors.full_messages }
end
Q5: Whatโs the difference between field :createUser, mutation:
and field :createUser do ... end
?
A: The first uses a predefined class (cleaner for reusability). The second defines inline logic (useful for quick or tiny mutations).
Q6: Can I separate large queries into smaller files?
A: Yes! You can organize logic using resolver
classes and group queries under namespaces/modules.
Q7: How do I test my schema?
A: You can use Postman, cURL, or GraphiQL UI with a sample query like:
{
users {
id
email
}
}
โ Best Practices (with Examples)
1. Keep Queries and Mutations Focused
Define one responsibility per query or mutation. Avoid bloated fields with multiple unrelated tasks.
# Good
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
# Avoid
field :userAndOrders, Types::UserWithOrdersType, null: true
2. Always Return Meaningful Errors
Mutations should return custom error messages that clients can use.
if photo.save
{ photo: photo, errors: [] }
else
{ photo: nil, errors: photo.errors.full_messages }
end
3. Use Strong Types and Arguments
Declare argument types and required fields clearly to avoid unexpected nulls or malformed input.
argument :title, String, required: true
argument :image_url, String, required: false
4. Split Complex Mutations into Service Objects
Donโt put heavy logic inside mutation classes. Delegate to service classes for clean code.
def resolve(args)
result = CreatePhotoService.call(args)
...
end
5. Namespace Types by Domain
Organize your types to avoid clutter and improve readability in large apps.
# Good
Types::User::ProfileType
Types::Admin::UserType
6. Use Null Safety Thoughtfully
Explicitly define which fields can be null
to make the contract between frontend and backend strong.
field :email, String, null: false
7. Test Each Query & Mutation Independently
Write tests for GraphQL operations as you would for controllers to prevent regression.
๐ Real-World Use Cases
- ๐ A photo-sharing app where users can query public galleries (QueryType) and upload new photos (MutationType)
- ๐ An e-commerce store where QueryType fetches products and MutationType creates orders
Generate Types using rails g graphql:object
๐ง Detailed Explanation
In GraphQL for Rails, a Type is like a blueprint of a model โ it defines which fields can be fetched by clients.
We generate types using:
rails generate graphql:object ModelName
This creates a file like:
app/graphql/types/model_name_type.rb
Inside, we list all the fields that we want to expose in the API. These fields match the modelโs attributes or custom computed fields.
Why itโs important: It ensures your frontend can only access the data you explicitly allow โ adding security and flexibility.
๐ง Example Breakdown
Letโs say we have a Photo
model with id
, title
, and image_url
.
Run the command:
rails generate graphql:object Photo
This generates:
module Types
class PhotoType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :image_url, String, null: true
end
end
Explanation:
ID
,String
โ These are GraphQL typesnull: false
โ means this field is required and wonโt return nullnull: true
โ means itโs optional
This type can now be used in queries and mutations to define the shape of data returned.
๐๏ธ Best Implementation (Step-by-Step)
Let’s say we want to build a GraphQL API to expose a Photo
model that includes fields like id
, title
, and image_url
.
1. โ Generate the Type
Use the Rails generator to scaffold a GraphQL type object:
rails generate graphql:object Photo
This creates a file at app/graphql/types/photo_type.rb
2. โ Define Fields in PhotoType
Edit the generated file to define the fields you want the API to expose:
# app/graphql/types/photo_type.rb
module Types
class PhotoType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :image_url, String, null: true
end
end
Explanation: This tells GraphQL what a Photo
looks like when queried.
3. โ Use PhotoType in QueryType
Add a query to return a list of photos using the newly created PhotoType
:
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :photos, [Types::PhotoType], null: false
def photos
Photo.all
end
end
end
This exposes a photos
field that fetches all photo records.
4. โ Add Data and Test
Make sure you have sample data in your database (e.g., via rails console
or seed file).
Then test it using GraphiQL or Postman:
query {
photos {
id
title
imageUrl
}
}
๐ฏ Summary
- Generate type using
rails g graphql:object
- Define fields inside the generated type class
- Use the type in your root
QueryType
- Test using GraphiQL, Postman, or curl
This structure ensures your API is modular, secure, and easy to maintain.
๐ง Examples
Example command:
rails g graphql:object Photo
Generated file:
module Types
class PhotoType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :image_url, String, null: true
end
end
๐ Alternative Concepts
- Manually creating type files for finer control
- Using namespaced types (e.g.,
User::ProfileType
) for large apps
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: What does rails g graphql:object
do?
A: It generates a new GraphQL object type file inside app/graphql/types/
. This file defines fields for a specific model or concept.
rails generate graphql:object Photo
# Output: creates types/photo_type.rb
Q2: Where are the GraphQL types stored in a Rails app?
A: They are stored inside the app/graphql/types
directory. Each file defines a type class like PhotoType
, UserType
, etc.
Q3: How do I expose a model via GraphQL?
A: Define a corresponding type (e.g. `PhotoType`) and reference it in `QueryType`.
# app/graphql/types/query_type.rb
field :photos, [Types::PhotoType], null: false
def photos
Photo.all
end
Q4: Whatโs the difference between a GraphQL type and a model?
A: The **model** (e.g., `Photo`) handles database logic, while the **GraphQL type** (`PhotoType`) defines how that data is exposed to the client via the API.
Q5: Can GraphQL types return associations?
A: Yes! You can define associated fields like this:
# app/graphql/types/photo_type.rb
field :user, Types::UserType, null: false
def user
object.user
end
Q6: How do I rename a field in GraphQL but still map to a model field?
A: You can rename the field and write a custom resolver:
field :photo_title, String, null: false
def photo_title
object.title
end
Q7: How do I validate data in a GraphQL object?
A: Validation should be done in mutations or models. Object types only define fields; they do not include validation logic.
Q8: What if a field returns `null` even when data exists?
A: Check if the field is marked `null: false` in the type and whether the resolver method is implemented or returning correct data.
Q9: How can I hide a field from GraphQL response?
A: Simply donโt declare that field in the type class. Only declared fields are accessible in API queries.
Q10: Can I reuse a type for multiple queries?
A: Yes, GraphQL types in Rails are reusable across queries and mutations as long as they logically represent that model or structure.
โ Best Practices (with Examples)
1. Use Strong Typing for Clarity
Always define the type and nullability for each field clearly to help clients understand the API schema.
field :title, String, null: false
field :description, String, null: true
2. Avoid Exposing Internal Model Fields
Only expose necessary fields in your GraphQL types. Do not expose sensitive or internal attributes.
# Avoid exposing internal logic
# Do NOT do this unless intentional
field :admin_notes, String, null: true
3. Group Fields by Logical Sections
Structure your fields in a logical, user-friendly order so clients understand the data context.
field :id, ID, null: false
field :title, String, null: false
field :image_url, String, null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
4. Handle Associations Explicitly
When exposing associations, use custom resolvers if needed and be cautious of N+1 queries.
field :user, Types::UserType, null: false
def user
object.user
end
5. Use Custom Resolvers for Custom Logic
Donโt write complex logic inside your GraphQL type. Delegate to methods or service objects if needed.
field :display_title, String, null: false
def display_title
object.title.upcase
end
6. Use `graphql:object` Generator Consistently
Always use the generator to maintain structure and avoid manual mistakes. It follows naming conventions automatically.
rails generate graphql:object Product
# โ
Creates: app/graphql/types/product_type.rb
7. Avoid Circular References
Be careful when types reference each other, which can lead to infinite loops or performance issues.
# ProductType
field :category, Types::CategoryType, null: false
# CategoryType
field :products, [Types::ProductType], null: false
โ Use pagination or connection types when needed.
๐ Real-World Use Case
A photo-sharing app uses PhotoType
to expose user-generated photos through GraphQL queries. Users fetch a list of photos with associated comments and likes defined using object types.
Define Fields, Arguments & Return Types in GraphQL (Rails)
๐ง Detailed Explanation
In GraphQL, every Type (like UserType
or PostType
) is built with a set of fields.
Each field represents a piece of data that can be queried. It works like a method that returns some value.
๐ Fields can have:
- Return Types โ what kind of data is returned (e.g.,
String
,ID
, custom types likeUserType
) - Arguments โ optional parameters you can pass to the field (like a filter or limit)
๐ Why it’s important: This structure makes your API precise and self-documented. Clients can see what they can ask for and what they’ll get.
๐ฌ Example field with argument:
field :posts, [Types::PostType], null: true do
argument :limit, Integer, required: false, default_value: 5
end
This says: “You can fetch multiple posts, and optionally limit how many you get.”
๐งฉ Return Type Meaning:
ID
โ unique identifierString
โ text field[Types::PostType]
โ array of custom PostType
โ These definitions keep your API consistent and safe from unexpected results.
๐๏ธ Best Implementation (Step-by-Step)
Letโs walk through how to define fields, arguments, and return types in a GraphQL type in a real Rails app.
โ Step 1: Generate a GraphQL Type
rails g graphql:object User
This creates a file at app/graphql/types/user_type.rb
.
โ Step 2: Define Fields
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
end
end
Each field
method defines the name, return type, and nullability.
โ Step 3: Add a Field with Arguments
Now letโs say we want to get a list of posts for a user, but with a limit.
module Types
class UserType < Types::BaseObject
# existing fields...
field :posts, [Types::PostType], null: true do
argument :limit, Integer, required: false, default_value: 5
end
def posts(limit:)
object.posts.limit(limit)
end
end
end
- posts: the field name
- limit: the argument passed to it
- object.posts.limit(limit): ActiveRecord call to fetch posts
โ Step 4: Add a Query to Access This Type
Now expose UserType
from QueryType
.
module Types
class QueryType < Types::BaseObject
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
end
end
โ Step 5: Test the Query
Use GraphiQL or Postman to run this query:
query {
user(id: 1) {
id
name
posts(limit: 3) {
id
title
}
}
}
๐ฏ Summary:
- โ
Use
field
to define accessible data - โ
Add
argument
inside the block for custom input - โ
Define return type clearly (e.g.
String
,[Type]
, etc.) - โ Use method under the hood to return actual data
๐ก Examples
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :posts, [Types::PostType], null: true do
argument :limit, Integer, required: false, default_value: 10
end
def posts(limit:)
object.posts.limit(limit)
end
end
end
๐ Alternative Concepts
- Using
GraphQL::Types::ISO8601DateTime
for date fields - Using custom scalar types for structured data like JSON
- Wrapping field results in pagination types (e.g. Relay)
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: How do I define a non-null field in a GraphQL type?
A: Set null: false
in the field definition.
field :name, String, null: false
This means name
must always be present.
Q2: How can I accept arguments in a field?
A: Use the argument
method inside a field block.
field :posts, [Types::PostType], null: true do
argument :limit, Integer, required: false
end
def posts(limit: nil)
object.posts.limit(limit || 10)
end
Q3: What types can I return in GraphQL fields?
A: Scalar types (e.g., String
, Int
, ID
), object types (e.g., UserType
), and lists (e.g., [UserType]
).
field :user, Types::UserType, null: true
field :ids, [ID], null: false
Q4: How do I resolve the value of a field manually?
A: Define a method with the same name as the field inside the type.
field :full_name, String, null: false
def full_name
"#{object.first_name} #{object.last_name}"
end
Q5: How do I handle default values in arguments?
A: Use default_value:
when defining the argument.
field :recent_posts, [Types::PostType], null: true do
argument :limit, Integer, required: false, default_value: 5
end
Q6: Can I reuse argument definitions?
A: Yes, by creating custom input types using graphql:input
.
rails g graphql:input CreateUserInput
This lets you reuse input structures across mutations or queries.
Q7: What happens if I return nil
for a non-null field?
A: GraphQL will raise an error and not return the full response for that field.
field :email, String, null: false
# If the resolver returns nil, the API will throw a validation error
โ Best Practices (with Examples)
1. Define clear return types and enforce null safety
Specify if a field can be null or not. This improves frontend reliability and API contracts.
field :email, String, null: false # Better
field :bio, String, null: true # Nullable field
2. Use descriptive field names
Avoid short or vague field names. Be explicit and clear about what the field returns.
# โ
Good
field :total_followers, Integer, null: false
# โ Bad
field :count, Integer, null: false
3. Reuse logic using resolvers or service objects
Keep field definitions clean by moving complex logic into resolvers or service classes.
def posts(limit: 10)
PostFetcher.new(user: object, limit: limit).call
end
4. Use input types for structured arguments
Instead of passing multiple arguments to a mutation, define an input object for clarity.
# Create input
rails g graphql:input CreateUserInput
# Use it
argument :input, Types::Inputs::CreateUserInput, required: true
5. Avoid business logic inside type classes
Resolvers should delegate to models or services for better testability and separation of concerns.
# In the GraphQL type
def total_orders
OrderService.total_for_user(object)
end
6. Document your fields using GraphQL-Ruby description
Use the description:
option to explain what each field does.
field :bio, String, null: true, description: "Short biography of the user"
7. Validate arguments in mutations
Donโt rely solely on model validations. Add checks in your mutation resolver when needed.
def resolve(title:)
raise GraphQL::ExecutionError, "Title too short" if title.length < 3
Post.create!(title: title)
end
๐ Real-World Use Case
In a photo-sharing app, define a field like photos(limit: 5)
to fetch a limited number of images for a profile page, ensuring performance and flexibility.
Nullability & Required Arguments in GraphQL (Rails)
๐ง Detailed Explanation
In GraphQL, every field and argument must clearly define whether it can be nullable or required.
- Nullable: Means the value is optional and can return
null
. - Required: Means the value must be present. If itโs missing, GraphQL will return an error before executing any logic.
When defining fields or arguments in Rails GraphQL:
- Use
null: false
on fields to make them required in response data. - Use
required: true
on arguments to make them mandatory inputs for queries or mutations.
This improves API clarity, safety, and validation โ the client knows exactly what is expected.
Think of it like this:
- ๐ข
null: true
: Safe for optional things like bio, nickname - ๐ด
null: false
: Use for required fields like email, ID
Real Example:
field :email, String, null: false # This field must always return an email
argument :name, String, required: true # This input must be provided
If you don't provide a required: true
argument in a mutation, youโll get an automatic GraphQL error โ no code runs until it's fixed!
๐๏ธ Best Implementation (Step-by-Step)
Letโs implement a simple GraphQL mutation to create a user, ensuring we define which fields and arguments are required.
1. โ Define the UserType
This type represents what fields can be returned about a user. We make email required and bio optional.
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :email, String, null: false
field :bio, String, null: true
end
end
2. โ Create the CreateUser Mutation
We define arguments with required: true
or false
depending on the need.
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :email, String, required: true
argument :bio, String, required: false
field :user, Types::UserType, null: true
field :errors, [String], null: false
def resolve(email:, bio: nil)
user = User.new(email: email, bio: bio)
if user.save
{ user: user, errors: [] }
else
{ user: nil, errors: user.errors.full_messages }
end
end
end
end
3. โ Connect Mutation to MutationType
Link your mutation in the root mutation file.
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser
end
end
4. โ Schema File
Make sure your schema is using MutationType and QueryType (if needed).
# app/graphql/my_app_schema.rb
class MyAppSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
end
5. โ Query to Test in GraphiQL or Postman
This mutation will fail if email
is not provided because itโs required.
mutation {
createUser(input: {
email: "[email protected]",
bio: "I love Rails!"
}) {
user {
id
email
}
errors
}
}
Leave out email
and try again โ youโll get a GraphQL error before the code even runs. Thatโs how required arguments help!
โ Summary: Youโve now learned how to:
- Make return fields nullable or non-nullable
- Use
required: true
for input arguments - Let GraphQL validate inputs automatically
๐ก Examples
Required Argument:
field :create_user, mutation: Mutations::CreateUser
# In mutation:
argument :email, String, required: true
Nullable Field:
field :bio, String, null: true
Non-Nullable Field:
field :email, String, null: false
๐ Alternative Concepts
- Use custom validation inside resolvers for additional safety
- Apply model-level validations alongside required: true
- Group inputs using InputObjectType for structured arguments
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: What does null: false
mean in a field definition?
A: It means the field is non-nullable โ the API will always return a value. If it ever returns null, GraphQL throws an error.
field :email, String, null: false
Use case: Ensures clients never have to check for null before using the email.
Q2: Whatโs the difference between null: false
and required: true
?
A:
null: false
โ used in field definitions (for return values)required: true
โ used in argument definitions (for input)
# Return value must not be null
field :email, String, null: false
# Input argument is required
argument :email, String, required: true
Q3: What happens if a required argument is not passed in the request?
A: The request fails at the GraphQL validation level. Your resolver will not even be called.
# Input
mutation {
createUser(input: { bio: "Missing email" }) {
errors
}
}
Result: GraphQL returns an error like: "Argument 'email' is required, but not provided."
Q4: Can I make a field optional but still validate it manually?
A: Yes! Set required: false
, then handle the logic in the resolver.
argument :bio, String, required: false
def resolve(bio: nil)
if bio.present? && bio.length < 10
raise GraphQL::ExecutionError, "Bio too short"
end
end
Q5: How can I return a nullable field based on permissions?
A: You can set null: true
and return nil
if the current user isn't authorized.
field :salary, Integer, null: true
def salary
return nil unless context[:current_user]&.admin?
object.salary
end
โ Best Practices (with Examples)
1. Use null: false
for fields that must always return a value
This ensures clients can safely use the field without null-checking logic.
field :email, String, null: false
Why: Cleaner frontend code and fewer runtime surprises.
2. Use required: true
for critical mutation arguments
This enforces input validation at the GraphQL layer before hitting your resolver.
argument :title, String, required: true
Why: Prevents incomplete or invalid input from hitting your business logic.
3. Use null: true
for fields that depend on permission or context
If a value might be hidden or restricted, allow it to return null and control it in your resolver.
field :salary, Integer, null: true
def salary
return nil unless context[:current_user]&.admin?
object.salary
end
Why: Flexibility to expose data conditionally without breaking GraphQL contracts.
4. Document nullability clearly in frontend documentation
Ensure your API consumers know which fields can return null, so they handle them gracefully.
Why: Prevents frontend bugs and confusion when null values appear.
5. Validate optional inputs manually when needed
If a field is optional, always check for validity in your resolver before processing.
argument :bio, String, required: false
def resolve(bio: nil)
if bio.present? && bio.length < 10
raise GraphQL::ExecutionError, "Bio too short"
end
end
Why: Ensures logic integrity even when fields are not required.
๐ Real-World Scenario
In a signup mutation, email
and password
should always be marked required: true
and null: false
in the return type to enforce validation both ways.
GraphQL Basic Queries (Fields & Nested Associations)
๐ง Detailed Explanation
In GraphQL, a query is how you ask for data from the server. Think of it like telling the backend: "Hey, I want these specific things."
Each query is made of fields. These fields match the attributes or methods in your Ruby classes (like name, title, or email).
What makes GraphQL powerful is its ability to request nested data in one go. For example, you can get a list of users, their posts, and the comments on each post โ all in one query.
This is made possible because GraphQL mirrors the ActiveRecord associations in your Rails models.
Hereโs how it works in simple terms:
- User has many Posts
- Post has many Comments
So in your query, you can go:
users {
name
posts {
title
comments {
body
}
}
}
This lets the frontend control exactly what data it needs, reducing over-fetching or under-fetching.
Result: Cleaner code, faster response, and more control ๐ช
๐๏ธ Best Implementation (Step-by-Step)
Letโs implement a query that fetches users, their posts, and each postโs comments.
1. โ Define Models with Associations
Make sure your Rails models are related properly.
# app/models/user.rb
class User < ApplicationRecord
has_many :posts
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
end
2. โ Generate GraphQL Object Types
Use the generator:
rails g graphql:object User
rails g graphql:object Post
rails g graphql:object Comment
3. โ Define Fields in Each Type
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :posts, [Types::PostType], null: true
end
end
# app/graphql/types/post_type.rb
module Types
class PostType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :comments, [Types::CommentType], null: true
end
end
# app/graphql/types/comment_type.rb
module Types
class CommentType < Types::BaseObject
field :id, ID, null: false
field :body, String, null: false
end
end
4. โ Add the Query to QueryType
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :users, [Types::UserType], null: false
def users
User.includes(posts: :comments).all
end
end
end
This makes sure the associated data is eager-loaded, preventing N+1 query issues.
5. โ Test Your Query
You can now test your nested query in GraphiQL or Postman:
query {
users {
name
posts {
title
comments {
body
}
}
}
}
This returns users with their posts and each postโs comments โ all in one API call!
๐ Result
- โก Fast, efficient data fetching
- ๐ฆ Cleanly structured GraphQL types
- ๐ซ No over-fetching or N+1 issues
- ๐ก Easily scalable for additional fields or nested relations
๐ก Examples
GraphQL Query: Get users with their posts and comments
query {
users {
id
name
posts {
title
comments {
body
}
}
}
}
Schema setup (simplified):
# user_type.rb
field :posts, [Types::PostType], null: true
# post_type.rb
field :comments, [Types::CommentType], null: true
๐ Alternative Methods
- REST APIs with nested serializers (slower and more verbose)
- Separate GraphQL queries for each resource (less efficient)
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: How do I define a query to return a list of records?
A: Use a field in your `QueryType` with a return type of an array.
field :users, [Types::UserType], null: false
def users
User.all
end
Q2: How do I include nested associations like posts and comments?
A: Define those nested fields in the object types and use eager loading in the resolver.
User.includes(posts: :comments).all
Q3: How do I prevent N+1 query problems in GraphQL?
A: Use ActiveRecordโs `includes` method to preload associations in your resolver.
def users
User.includes(posts: :comments).all
end
Q4: Can I add arguments to filter the query results?
A: Yes. Use the `argument` method inside your field block.
field :user, Types::UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
Q5: How do I test this query using Postman?
A: Send a POST request to /graphql
with this body:
{
"query": "query { users { name posts { title comments { body } } } }"
}
Set header: Content-Type: application/json
Q6: How do I test it with curl?
A: Run this in terminal:
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "query { users { name posts { title comments { body } } } }"}'
Q7: What happens if a nested association is null?
A: GraphQL will return `null` for that nested field unless you've marked it as `null: false`.
field :posts, [Types::PostType], null: true
โ Best Practices (with Examples)
1. Define Only Necessary Fields
Only expose the fields needed by the client to avoid over-fetching and increase security.
# Good
field :name, String, null: false
# Avoid exposing sensitive or unused data
# field :password_digest, String, null: true
2. Use Nullability Thoughtfully
Be explicit about which fields can be null. This helps frontend developers write safer code.
field :email, String, null: false
field :bio, String, null: true
3. Eager Load Associations
Use `includes` in your resolvers to avoid N+1 queries when accessing nested associations.
def users
User.includes(posts: :comments).all
end
4. Add Pagination for Large Lists
Use pagination for large result sets to avoid performance issues and improve user experience.
# Use gems like graphql-pagination
field :users, UserType.connection_type, null: false
5. Use Descriptive Field and Type Names
Give meaningful names to fields and types to improve code readability and API usability.
# Good
field :total_comments, Integer, null: false
# Avoid
field :c, Integer, null: false
6. Test Your Queries
Write tests for each query and mutation to ensure schema integrity during changes.
it "returns all users" do
post graphql_path, params: { query: "{ users { name } }" }
expect(response.body).to include("John")
end
7. Validate Input Arguments
Always validate inputs in your resolvers to avoid logic bugs and maintain data integrity.
def user(id:)
raise GraphQL::ExecutionError, "Invalid ID" unless id.to_i > 0
User.find_by(id: id)
end
๐ Real-World Scenario
In a blog app, fetching a list of users with their posts and the comments on each post can be done in one query using nested associations. This improves speed and reduces frontend logic.
Arguments and Filters in GraphQL (Rails)
๐ง Detailed Explanation
In GraphQL, arguments let you send additional data with your queries, like filters or search terms. This helps you get only the data you really need.
For example, instead of fetching all users, you can ask for users with a specific role like "admin" or only those who are active.
Think of arguments like parameters you pass into a method. The resolver method receives them and uses them to filter the results.
Arguments go inside the query like this:
query {
users(role: "admin", active: true) {
id
name
}
}
And in your backend code, you define them like this:
field :users, [Types::UserType], null: false do
argument :role, String, required: false
argument :active, Boolean, required: false
end
def users(role: nil, active: nil)
scope = User.all
scope = scope.where(role: role) if role
scope = scope.where(active: active) unless active.nil?
scope
end
This way, your API is flexible and the client only gets the relevant data it asked for.
๐๏ธ Best Implementation (Step-by-Step)
Letโs implement a GraphQL query that fetches users based on dynamic filters like role and active status in a Rails app.
1. โ Define the `users` field in `QueryType`
Add arguments for filtering and define the return type.
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :users, [Types::UserType], null: false do
argument :role, String, required: false
argument :active, Boolean, required: false
end
def users(role: nil, active: nil)
scope = User.all
scope = scope.where(role: role) if role
scope = scope.where(active: active) unless active.nil?
scope
end
end
end
2. โ Define the `UserType`
Specify what fields should be exposed in the response.
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
field :role, String, null: false
field :active, Boolean, null: false
end
end
3. โ Sample Query to Test
Test your query using GraphiQL, Postman, or curl.
query {
users(role: "admin", active: true) {
id
name
email
}
}
This will return all active admin users.
๐ Done!
Your API now supports dynamic filtering using arguments!
๐ก Examples
# In app/graphql/types/query_type.rb
field :users, [Types::UserType], null: false do
argument :role, String, required: false
argument :active, Boolean, required: false
end
def users(role: nil, active: nil)
scope = User.all
scope = scope.where(role: role) if role
scope = scope.where(active: active) unless active.nil?
scope
end
๐ Alternative Methods
- Using filter objects instead of separate arguments for better structure.
- Pagination with arguments like
limit
,offset
, orcursor
. - Advanced search filters with JSON or custom input types.
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: How do I pass filter arguments to a GraphQL query in Rails?
A: Use the argument
keyword inside the field block to define filters.
field :users, [Types::UserType], null: false do
argument :role, String, required: false
end
Query Example:
query {
users(role: "admin") {
id
name
}
}
Q2: Can I use multiple optional arguments for filtering?
A: Yes, you can define multiple optional arguments and chain them in your resolver logic.
argument :role, String, required: false
argument :active, Boolean, required: false
def users(role: nil, active: nil)
scope = User.all
scope = scope.where(role: role) if role
scope = scope.where(active: active) unless active.nil?
scope
end
Q3: What happens if I don't pass an argument in the query?
A: If the argument is marked required: false
, Rails will use the default value of nil
, and you can skip filtering in that case.
Q4: How can I enforce that at least one argument is passed?
A: You can manually raise an error inside the resolver if all arguments are nil
.
def users(role: nil, active: nil)
if role.nil? && active.nil?
raise GraphQL::ExecutionError, "Please provide at least one filter."
end
...
end
Q5: Can I use enum types as arguments?
A: Yes. First, define an enum type and then use it in the argument definition.
# Define enum
class Types::RoleEnum < Types::BaseEnum
value "ADMIN", value: "admin"
value "USER", value: "user"
end
# Use in query
argument :role, Types::RoleEnum, required: false
โ Best Practices (with Examples)
1. Keep Arguments Optional Unless Absolutely Needed
Mark arguments as required: false
unless they're essential. This makes your API flexible and user-friendly.
# Good
argument :status, String, required: false
# Use in resolver
scope = Task.all
scope = scope.where(status: status) if status
2. Validate Argument Combinations Manually
When multiple filters are accepted, validate logical constraints in the resolver.
if start_date && end_date && start_date > end_date
raise GraphQL::ExecutionError, "Start date must be before end date"
end
3. Use Enums Instead of Free Strings
This prevents typos and invalid data from being passed in filters.
# Use this:
argument :role, Types::UserRoleEnum, required: false
# Not this:
argument :role, String, required: false
4. Avoid Overfetching โ Use Specific Queries
Instead of one big query with many filters, prefer focused, reusable queries with minimal logic inside GraphQL itself.
# Instead of:
query { users(role: "admin", active: true, email: "[email protected]") }
# Use:
query { activeAdmins { id email } }
5. Cache or Paginate Heavy Filters
If filters can result in large datasets (like date range), use pagination or memoization to improve performance.
field :users, Types::UserType.connection_type, null: false do
argument :role, String, required: false
end
6. Document All Accepted Filters Clearly
GraphQL auto-documents argument descriptions, so use description:
for clarity in tools like GraphiQL.
argument :role, String, required: false, description: "Filter users by role (admin/user)"
๐ Real-World Scenario
In an e-commerce Rails app, you might query products with filters like category, price range, and availability. For example:
query {
products(category: "Shoes", minPrice: 50) {
name
price
inStock
}
}
๐ Pagination in GraphQL (Rails)
๐ง Detailed Explanation
When you're working with a lot of data, you donโt want to fetch everything at once. Thatโs where pagination comes in. It helps you load data page by page instead of overwhelming your API and UI.
In GraphQL with Rails, pagination is most commonly handled using the connection_type
helper provided by the graphql-ruby
gem. This follows the Relay pagination standard using first
, after
, last
, and before
arguments.
For example, if you want to get just the first 5 posts, you can run a query like:
query {
posts(first: 5) {
edges {
node {
id
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
This structure gives you:
edges
โ The actual data rows.node
โ The object within each edge.pageInfo
โ Helpful info for navigating like if there's a next page.
Why this is important: Pagination prevents your frontend from slowing down and keeps your API efficient.
๐๏ธ Best Implementation (Step-by-Step)
We'll use `graphql-ruby`โs built-in pagination support via Relay connections.
1. โ Add Pagination to Your Query Field
Define a query field using connection_type
to enable pagination:
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :posts, PostType.connection_type, null: false
def posts
Post.all.order(created_at: :desc)
end
end
end
By using connection_type
, you're telling GraphQL to return paginated results with standard Relay-style arguments.
2. โ Define the Object Type
# app/graphql/types/post_type.rb
module Types
class PostType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :body, String, null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
3. โ Use the GraphQL Query
Now you can test your query in GraphiQL, Postman, or cURL:
query {
posts(first: 3) {
edges {
node {
id
title
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
4. โ Frontend Fetching with Cursor
After getting the endCursor
, you can pass it as the after
argument in the next query:
query {
posts(first: 3, after: "YXJyYXljb25uZWN0aW9uOjM=") {
edges {
node {
id
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
This enables infinite scrolling or "Load More" functionality in your frontend.
๐ฏ Summary:
- โ
Use
connection_type
to paginate GraphQL queries - โ Define object types with all required fields
- โ
Relay-style pagination gives
edges
andpageInfo
for frontend control - โ Ideal for React/Vue apps with endless scrolling
๐ก Examples
# QueryType
field :posts, Types::PostType.connection_type, null: false
# GraphQL Query
query {
posts(first: 5, after: "cursor123") {
edges {
node {
id
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
๐ Alternative Concepts
- Using `kaminari` or `will_paginate` with custom offset logic.
- Manual limit/offset in resolver instead of Relay connection.
- Custom keyset-based pagination for large datasets.
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: How do you implement pagination in GraphQL-Ruby?
A: Use connection_type
when defining fields in your QueryType. This adds built-in support for Relay-style pagination with first
, after
, pageInfo
, and edges
.
field :posts, PostType.connection_type, null: false
Q2: What is the purpose of pageInfo
in GraphQL?
A: It provides metadata about the pagination โ such as hasNextPage
, hasPreviousPage
, endCursor
, and startCursor
โ allowing clients to navigate pages efficiently.
pageInfo {
hasNextPage
endCursor
}
Q3: Whatโs the difference between offset-based and cursor-based pagination?
A:
- Offset-based: Uses numeric offsets (e.g., page 2 = offset 10). Easy but fragile with data changes.
- Cursor-based (Relay): Uses encoded cursors tied to a specific record, making pagination stable even if the data changes.
Q4: How do you fetch the next page of results?
A: Capture the endCursor
from the previous response and use it as the after
argument in the next query.
query {
posts(first: 5, after: "YXJyYXljb25uZWN0aW9uOjU=") {
edges {
node { id title }
}
pageInfo { hasNextPage endCursor }
}
}
Q5: How can I test pagination locally?
A: Use Postman, GraphiQL, or curl to run paginated queries with first
and after
arguments.
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "query { posts(first: 3) { edges { node { id title } } pageInfo { endCursor hasNextPage } } }"}'
โ Best Practices (with Examples)
1. Use Cursor-Based Pagination for Consistency
Cursor-based pagination (via `connection_type`) ensures stability even if records are added or removed between requests.
# QueryType
field :posts, PostType.connection_type, null: false
# GraphQL Query
query {
posts(first: 5) {
edges {
node { id title }
}
pageInfo {
hasNextPage
endCursor
}
}
}
2. Always Return pageInfo
This helps clients know whether to load more data or stop requesting.
pageInfo {
hasNextPage
endCursor
}
3. Set a Sensible Default Limit
Avoid returning too many records. Use a default first
value in resolvers or services.
def posts(first: 10)
Post.limit(first)
end
4. Avoid Deep Nesting Without Pagination
If a nested association returns many records (e.g., comments under posts), apply pagination to avoid performance issues.
field :comments, CommentType.connection_type, null: false
5. Use Relay-Compliant Connections if Working with JS Clients
React clients using Apollo or Relay expect the connection pattern (edges
, node
, pageInfo
) for auto-pagination support.
query {
posts(first: 5) {
edges {
node { id title }
}
pageInfo { hasNextPage endCursor }
}
}
๐ Real-World Use Case
In a photo-sharing app, you want to fetch only 10 photos per page. Pagination allows infinite scroll on frontend without performance drops.
๐ Using Scopes in Resolvers (GraphQL + Rails)
๐ง Detailed Explanation
In Rails, a scope is a way to reuse commonly-used queries. Itโs defined inside a model and returns an ActiveRecord relation. When working with GraphQL, instead of writing complex filters directly in your resolver, you can use these scopes to keep things clean and organized.
For example, if you want to fetch only published blog posts, instead of doing: Post.where(status: 'published')
inside your resolver every time,
you can define a scope in your model: scope :published, -> { where(status: 'published') }
and use Post.published
in your resolver.
This improves code readability, reusability, and makes your business logic easier to test and maintain.
โ Best Implementation
Hereโs how to cleanly implement a scope in your Rails model and use it in a GraphQL resolver.
1. Define a Scope in Your Model
# app/models/post.rb
class Post < ApplicationRecord
scope :published, -> { where(status: 'published') }
end
2. Use the Scope in Your GraphQL Resolver
# app/graphql/types/query_type.rb
field :published_posts, [Types::PostType], null: false
def published_posts
Post.published
end
3. Query from the Frontend
query {
publishedPosts {
id
title
status
}
}
This keeps your resolver logic minimal, promotes DRY code, and allows reusing the same scope in multiple places (e.g., controllers, background jobs, or other GraphQL fields).
๐ก Examples
# app/models/post.rb
scope :published, -> { where(status: 'published') }
# app/graphql/resolvers/posts_resolver.rb
def resolve
Post.published.order(created_at: :desc)
end
๐ Alternative Concepts
- Inline filters directly in resolvers using
Post.where(...)
. - Service objects to encapsulate complex filtering logic.
- Use scopes combined with policy scopes for authorization filtering.
โ Technical Q&A
Q1: Can I pass arguments to scopes from GraphQL?
A: Yes. You can define a scope that accepts parameters, and use those from GraphQL arguments.
# app/models/post.rb
scope :by_status, ->(status) { where(status: status) }
# app/graphql/types/query_type.rb
field :posts_by_status, [Types::PostType], null: false do
argument :status, String, required: true
end
def posts_by_status(status:)
Post.by_status(status)
end
Q2: How do I chain scopes inside a resolver?
A: You can chain multiple scopes just like in any ActiveRecord query.
Post.published.by_author(current_user.id).recent
Q3: Are scopes better than writing raw queries in resolvers?
A: Yes. Scopes promote reusability, are easier to test, and help separate business logic from GraphQL concerns.
Q4: What if I want to paginate scoped results?
A: Use Relay-style pagination or libraries like `kaminari`. Wrap the scope with `.page(params[:page])` in the resolver.
Post.published.page(page).per(10)
โ Best Practices
- ๐ Use named scopes for reusable query logic
Example:
In GraphQL resolver:# app/models/user.rb scope :active, -> { where(active: true) }
User.active
- ๐ Accept arguments in scopes to make them dynamic
Example:scope :by_role, ->(role) { where(role: role) } # GraphQL field :users_by_role, [Types::UserType], null: false do argument :role, String, required: true end def users_by_role(role:) User.by_role(role) end
- ๐งผ Chain scopes to keep resolvers clean
Example:User.active.admins.ordered_by_signup
- ๐ Put complex filtering logic inside scopes, not resolvers
Avoid:
Prefer:User.where(active: true, role: 'admin', created_at: ..Date.today)
User.active.admins.recent_signups
- ๐งช Test your scopes independently from GraphQL
Since scopes are just ActiveRecord, you can write model specs for them using RSpec or Minitest.
๐ Real-World Use Case
In a publishing platform, use Article.published.by_author(current_user)
in the GraphQL resolver to filter data based on business rules and user context.
๐ Best Practices & Folder Structure in GraphQL (Rails)
๐ง Detailed Explanation
When building a GraphQL API in a Rails app, it's important to organize your files clearly. GraphQL has special parts like queries, mutations, and types. If you mix them all together, your app will get messy as it grows.
The best way is to create a graphql/
folder inside app/
. Inside that, use subfolders like:
types/
โ All your object types likeUserType
,PostType
.mutations/
โ For all mutations (create, update, delete).queries/
โ For all custom read operations.inputs/
โ For grouping arguments together cleanly.resolvers/
โ Optional: for big business logic outside mutations.loaders/
โ Used with GraphQL::Batch to avoid N+1 queries.schema.rb
โ This is the main file that combines everything.
This makes your code easier to read, test, and maintain โ especially in large apps with multiple developers.
๐ก Examples
# app/graphql/types/user_type.rb
module Types
class UserType < BaseObject
field :id, ID, null: false
field :email, String, null: false
end
end
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :email, String, required: true
field :user, Types::UserType, null: true
field :success, Boolean, null: false
end
end
โ Best Practices
- Keep each GraphQL component (types, mutations, queries) in separate folders.
- Use
GraphQL::Batch
and place batch loaders insideloaders/
. - Use namespaced modules (e.g.,
Mutations::Users::Create
) for complex domains. - Split large mutations into service objects or resolvers.
- Use `inputs/` folder for complex nested arguments.
๐ Real-World Use Case
In a multi-team Rails monolith using GraphQL, a structured GraphQL folder like this ensures each domain (Users, Orders, Payments) has its own clean query/mutation/types layout โ improving collaboration and easing testing/debugging.
๐ Creating Mutations in GraphQL (Rails)
๐ง Detailed Explanation
Mutations in GraphQL are used when you want to create, update, or delete data.
Unlike queries (which are only for reading data), mutations allow you to send data to the server and make changes in your database.
In Rails, if you're using the graphql-ruby
gem, creating a mutation is super simple using a built-in generator:
rails g graphql:mutation CreatePost
This command creates a new Ruby class where you define:
- โ
What inputs (arguments) the mutation accepts โ like
title
orbody
. - โ What type of data it will return โ usually the object it created or updated.
- โ The actual logic to perform the action โ such as saving to the database.
Example: Letโs say you want to create a new blog post. A mutation will receive the postโs title and body, create the record, and return the newly created post.
This is how a mutation becomes a powerful tool for building interactive frontend apps that talk to your Rails backend through GraphQL.
๐๏ธ Best Implementation (Step-by-Step)
Weโll walk through how to create a GraphQL mutation in Rails to create a new blog post using the graphql-ruby
gem.
1. โ Generate the Mutation File
Run the following generator:
rails generate graphql:mutation CreatePost
This creates a file at: app/graphql/mutations/create_post.rb
2. โ Define Input Arguments and Return Type
# app/graphql/mutations/create_post.rb
module Mutations
class CreatePost < BaseMutation
argument :title, String, required: true
argument :body, String, required: false
field :post, Types::PostType, null: true
field :errors, [String], null: false
def resolve(title:, body: nil)
post = Post.new(title: title, body: body)
if post.save
{ post: post, errors: [] }
else
{ post: nil, errors: post.errors.full_messages }
end
end
end
end
This mutation accepts a title (required) and body (optional), creates the post, and returns the post or error messages.
3. โ Add the Mutation to the Root Mutation Type
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_post, mutation: Mutations::CreatePost
end
end
This makes your mutation accessible under the createPost
field in GraphQL.
4. โ Use the Mutation in GraphiQL or API Clients
You can now test your mutation:
mutation {
createPost(input: { title: "My First Post", body: "This is GraphQL with Rails." }) {
post {
id
title
body
}
errors
}
}
If successful, it will return:
{
"data": {
"createPost": {
"post": {
"id": "1",
"title": "My First Post",
"body": "This is GraphQL with Rails."
},
"errors": []
}
}
}
๐ฏ Summary
- โ
Use
rails g graphql:mutation
to scaffold mutation classes - โ
Define required and optional arguments with
argument
- โ
Return data and errors clearly via
field
- โ
Use
resolve
method to handle mutation logic
๐ก Examples
# Run this command
rails g graphql:mutation CreatePost
# It generates:
module Mutations
class CreatePost < BaseMutation
argument :title, String, required: true
argument :body, String, required: false
type Types::PostType
def resolve(title:, body: nil)
Post.create!(title: title, body: body)
end
end
end
๐ Alternative Concepts
- Use
form objects
to manage complex logic - Use
ActiveModel::Validation
for clean mutation classes - Use a service layer to isolate business logic
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: How do you generate a GraphQL mutation in Rails?
A: Use the generator provided by graphql-ruby
:
rails generate graphql:mutation CreatePost
This will scaffold a mutation class in app/graphql/mutations/
.
Q2: How do you define required vs optional arguments in a mutation?
A: Use the required:
keyword when defining argument
s.
argument :title, String, required: true
argument :body, String, required: false
Q3: How do you return both data and errors from a GraphQL mutation?
A: Define both a data field and an errors array in your mutation:
field :post, Types::PostType, null: true
field :errors, [String], null: false
def resolve(...)
if post.save
{ post: post, errors: [] }
else
{ post: nil, errors: post.errors.full_messages }
end
end
Q4: How is mutation input passed in the GraphQL query?
A: Input is passed inside an input
object in the mutation query:
mutation {
createPost(input: { title: "GraphQL Basics", body: "Mutation example." }) {
post {
id
title
}
errors
}
}
Q5: Can GraphQL mutations return null values?
A: Yes. You must define fields with null: true
or null: false
depending on whether theyโre optional in the response.
field :post, Types::PostType, null: true
field :errors, [String], null: false
Q6: Where do you register the mutation so it becomes available?
A: Add it to your MutationType
:
# app/graphql/types/mutation_type.rb
field :create_post, mutation: Mutations::CreatePost
โ Best Practices (with Examples)
1. Use Clear Input Validation
Always validate input in your mutation to avoid invalid data creation.
def resolve(title:, body: nil)
return { post: nil, errors: ["Title too short"] } if title.length < 3
post = Post.new(title: title, body: body)
if post.save
{ post: post, errors: [] }
else
{ post: nil, errors: post.errors.full_messages }
end
end
2. Return Useful Errors
Use errors
array in your mutation to give meaningful error feedback.
field :errors, [String], null: false
3. Use Required Flags Properly
Only mark truly essential arguments as required: true
.
argument :title, String, required: true
argument :body, String, required: false
4. Group Inputs into a Custom Input Object
This improves reusability and readability, especially for complex mutations.
input_object_class Types::Inputs::PostInput
# Use like:
argument :input, Types::Inputs::PostInput, required: true
5. Use Strong Types in Outputs
Always define return types with appropriate nullability and types.
field :post, Types::PostType, null: true
field :errors, [String], null: false
6. Keep Resolvers Clean
Move business logic into service objects to keep mutations clean and testable.
def resolve(input:)
result = PostCreator.call(input.to_h)
result.success? ? { post: result.post, errors: [] } : { post: nil, errors: result.errors }
end
๐ Real-World Use Case
In a blog app, you use a CreatePost
mutation so that users can publish new posts via the frontend. All validation happens server-side via GraphQL.
๐งพ Input Types vs Arguments in GraphQL (Rails)
๐ง Detailed Explanation
In GraphQL mutations, we send data to the server โ like creating or updating a post. There are two main ways to send this data:
- Arguments: You list each field separately. This is good for simple data.
- Input Types: You group all fields into a single object. This is better for complex or reusable data.
Letโs break it down:
๐ Arguments (Simple and Quick)
You directly define each field in the mutation like this:
mutation {
createPost(title: "GraphQL Guide", body: "Learn GraphQL with Rails") {
post {
id
title
}
}
}
โ
Easy for small forms
โ Hard to manage if there are many fields
๐ฆ Input Types (Organized and Scalable)
You create a custom object called PostInput
to group the data:
mutation {
createPost(input: { title: "GraphQL Guide", body: "Learn GraphQL" }) {
post {
id
title
}
}
}
โ
Clean, reusable, scalable
โ
Better for large or nested data
โ
Great when used in forms or APIs
๐ So, use arguments for quick things, but switch to input types when your mutation grows.
๐๏ธ Best Implementation (Step-by-Step)
For complex mutations, Input Types are recommended because they make your schema cleaner and easier to maintain.
โ Step 1: Generate a Mutation
Use the generator:
rails g graphql:mutation CreatePost
โ Step 2: Create an Input Object Type
Create a reusable input object for the fields:
# app/graphql/types/inputs/post_input_type.rb
module Types
module Inputs
class PostInputType < Types::BaseInputObject
argument :title, String, required: true
argument :body, String, required: false
end
end
end
โ Step 3: Use the Input Type in Your Mutation
# app/graphql/mutations/create_post.rb
module Mutations
class CreatePost < BaseMutation
argument :input, Types::Inputs::PostInputType, required: true
field :post, Types::PostType, null: true
field :errors, [String], null: false
def resolve(input:)
post = Post.new(input.to_h)
if post.save
{ post: post, errors: [] }
else
{ post: nil, errors: post.errors.full_messages }
end
end
end
end
โ Step 4: Add the Mutation to Your Schema
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_post, mutation: Mutations::CreatePost
end
end
โ Step 5: Test the Mutation
mutation {
createPost(input: { title: "GraphQL Input", body: "Input Types make life easy!" }) {
post {
id
title
}
errors
}
}
๐งช You can test this in GraphiQL, Postman, or cURL.
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createPost(input: { title: \"New Post\", body: \"Awesome content\" }) { post { id title } errors } }"}'
๐ฏ Summary:
- โ
Use
BaseInputObject
to group fields - โ
Keep your resolver clean with
input.to_h
- โ Use input types for better reusability and structure
๐ก Examples
Using Arguments:
field :create_post, mutation: Mutations::CreatePost
# Inside mutation file:
argument :title, String, required: true
argument :body, String, required: false
Using Input Type:
# Input object
module Types::Inputs
class PostInput < Types::BaseInputObject
argument :title, String, required: true
argument :body, String, required: false
end
end
# Mutation
argument :input, Types::Inputs::PostInput, required: true
๐ Alternative Concepts
- Direct arguments for lightweight mutations
- Input objects for forms and nested structures
- Service Objects for mutation logic
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: What is the difference between arguments and input types in GraphQL mutations?
A: Arguments are defined inline on the mutation, which is fine for simple cases. For complex or reusable inputs, use an InputObject
to group fields together.
# Using arguments
argument :title, String, required: true
argument :body, String, required: false
# Using input type
argument :input, Types::Inputs::PostInputType, required: true
Q2: Why should we prefer Input Types for complex data structures?
A: Input types allow grouping related fields, reduce schema clutter, and improve maintainability. They're especially useful when you have nested fields or want to reuse input logic across multiple mutations.
# Example input type
class PostInputType < Types::BaseInputObject
argument :title, String, required: true
argument :body, String, required: false
end
Q3: How do you access values from input types in the resolver?
A: Use input.to_h
to convert the input into a plain Ruby hash, then pass it to your model.
def resolve(input:)
post = Post.new(input.to_h)
end
Q4: Can you validate input types in GraphQL?
A: GraphQL only enforces presence and type validations (e.g., required: true
). Deeper validations (like format or uniqueness) should be handled in your Rails models.
argument :email, String, required: true # Type validation only
# Rails model validation
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
Q5: How do input types improve client-side code?
A: Input types give clients a consistent structure for mutation payloads, making frontend form generation and validation easier.
mutation {
createPost(input: { title: "Post Title", body: "Content" }) {
post { id title }
errors
}
}
โ Best Practices (with Examples)
1. Use Input Types for Mutations with Multiple or Nested Fields
When your mutation requires more than 2โ3 arguments or includes nested attributes, always group them into a single InputObject
. It keeps the schema clean and reusable.
# Good practice: group related arguments
argument :input, Types::Inputs::PostInputType, required: true
# Bad practice: too many inline arguments
argument :title, String, required: true
argument :body, String, required: false
argument :tags, [String], required: false
2. Keep Input Type Naming Clear and Consistent
Follow a naming pattern like [Model]InputType
or [Action][Model]InputType
for clarity.
# Good
class PostInputType < Types::BaseInputObject
# Also Good
class CreatePostInputType < Types::BaseInputObject
# Avoid vague names
class InfoType < Types::BaseInputObject
3. Always Use input.to_h
in the Resolver
This ensures the input fields are properly converted into a hash before creating or updating records.
def resolve(input:)
post = Post.new(input.to_h)
post.save
end
4. Leverage Rails Model Validations Instead of GraphQL for Business Logic
Use GraphQL to enforce basic types and presence. Delegate custom validations like uniqueness or formats to your Rails models.
# GraphQL (basic validation)
argument :email, String, required: true
# Rails (business validation)
validates :email, uniqueness: true
5. Reuse Input Types Across Mutations
Define common input types like UserInputType
or AddressInputType
once and reuse them in multiple mutations or even nested fields.
# Reusable input type
class AddressInputType < Types::BaseInputObject
argument :city, String, required: true
argument :zipcode, String, required: true
end
๐ Real-World Use Case
When creating a blog post from a form in a Rails app, using an input object like PostInput
keeps the mutation clean and aligns with form structure.
๐งฉ Using Service Objects in GraphQL Mutations (Rails)
๐ง Detailed Explanation
In GraphQL mutations, you often need to perform actions like creating or updating records in the database. If you put all that logic directly inside the mutation file, it becomes hard to read, test, and maintain.
A Service Object is a plain Ruby class that handles a specific business task. Instead of writing all the logic in the mutation, you move it into a service object and call it from the mutation.
Think of it like this: your mutation is the button click, and the service object is the worker that does the job. This keeps everything clean and organized.
For example, creating a user might involve validations, sending emails, and assigning default roles.
All of that can be done inside CreateUserService
, while the mutation stays simple.
This makes your code:
- โ Easier to read
- โ Easier to test
- โ Easier to reuse in other parts of your app
๐๏ธ Best Implementation (Step-by-Step)
We'll create a mutation to register a user, but all the actual logic will live inside a service object.
1. โ Create the Service Object
First, create a service class in app/services
.
# app/services/create_user_service.rb
class CreateUserService
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
# Example: send welcome email, log activity, assign default role
UserMailer.welcome_email(user).deliver_later
user
else
raise GraphQL::ExecutionError, user.errors.full_messages.join(', ')
end
end
end
2. โ Define the Mutation Class
Use rails g graphql:mutation RegisterUser
to generate the mutation. Then modify it like this:
# app/graphql/mutations/register_user.rb
module Mutations
class RegisterUser < BaseMutation
argument :name, String, required: true
argument :email, String, required: true
argument :password, String, required: true
field :user, Types::UserType, null: true
def resolve(name:, email:, password:)
user = CreateUserService.new(name: name, email: email, password: password).call
{ user: user }
end
end
end
3. โ Add the Mutation to Schema
Register the mutation in your MutationType:
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :register_user, mutation: Mutations::RegisterUser
end
end
4. โ GraphQL Query Example
You can now use this mutation in GraphiQL:
mutation {
registerUser(name: "Ali", email: "[email protected]", password: "secure123") {
user {
id
name
email
}
}
}
๐ฏ Summary:
- โ Move business logic out of GraphQL into a clean Ruby class
- โ Handle all side-effects (email, logging, etc.) in the service
- โ Keep mutations focused on arguments and return values only
๐ก Example
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :name, String, required: true
argument :email, String, required: true
field :user, Types::UserType, null: true
field :errors, [String], null: false
def resolve(name:, email:)
result = CreateUserService.call(name: name, email: email)
if result.success?
{ user: result.user, errors: [] }
else
{ user: nil, errors: result.errors }
end
end
end
end
# app/services/create_user_service.rb
class CreateUserService
attr_reader :user, :errors
def self.call(params)
new(params).tap(&:create)
end
def initialize(params)
@params = params
@errors = []
end
def create
@user = User.new(@params)
if @user.save
true
else
@errors = @user.errors.full_messages
false
end
end
def success?
@errors.empty?
end
end
๐ Alternative Concepts
- Fat Mutations (avoid): Direct logic in mutation class.
- Command Pattern: Slightly more abstract form of service objects.
- Interactor Gem: Rails-specific gem for services (optional).
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: Why should you use service objects in GraphQL mutations?
A: Service objects help keep the mutation classes clean by moving complex business logic to a dedicated class. This improves separation of concerns and makes code easier to test and maintain.
# Mutation class (simple)
def resolve(name:, email:, password:)
user = CreateUserService.new(name: name, email: email, password: password).call
{ user: user }
end
Q2: How do you handle errors in a service object used in a mutation?
A: You can raise a GraphQL::ExecutionError
inside the service when something goes wrong. This error will be captured and shown in the GraphQL response.
# Inside service object
raise GraphQL::ExecutionError, "Invalid email" unless email.include?("@")
Q3: How do you test service objects independently?
A: Since service objects are plain Ruby classes, you can write unit tests for them without loading the GraphQL stack.
# RSpec
describe CreateUserService do
it "creates user with valid params" do
user = CreateUserService.new(name: "Test", email: "[email protected]", password: "123456").call
expect(user).to be_persisted
end
end
Q4: Can you use service objects for nested or multi-step logic?
A: Yes. You can chain or call other services inside a service object to manage complex flows like signup, payment, and confirmation.
# Inside CreateUserService
if user.save
AssignDefaultPlanService.new(user).call
end
Q5: Should service objects return raw errors?
A: No. Itโs better to catch errors and raise a formatted GraphQL::ExecutionError
or return a structured response for client-side clarity.
# Bad
user.save!
# Good
raise GraphQL::ExecutionError, user.errors.full_messages.join(", ") unless user.save
โ Best Practices (with Examples)
1. Keep Resolvers Clean
Use service objects to avoid bloated resolver methods. Move validation, processing, and external service calls into service classes.
# Mutation resolver
def resolve(args)
CreateUserService.new(args).call
end
2. Follow Single Responsibility Principle
Each service object should do one thing well. Donโt mix responsibilities like creating users and sending emails in the same service.
# Separate service for email
SendWelcomeEmailService.new(user).call
3. Graceful Error Handling
Wrap risky operations and return GraphQL::ExecutionError
to make error messages meaningful on the frontend.
raise GraphQL::ExecutionError, "Could not save user: #{user.errors.full_messages.join(', ')}"
4. Make Services Callable
Use a `.call` method convention in services for a consistent interface across your app.
# Standard interface
CreateUserService.call(args)
5. Test in Isolation
Unit test service objects directly to catch business logic issues before they hit GraphQL.
describe CreateUserService do
it "creates a user" do
result = described_class.new(name: "Test", email: "[email protected]", password: "123456").call
expect(result).to be_a(User)
end
end
๐ Real-World Scenario
In a billing system, a single mutation like `CreateInvoice` could involve tax calculations, line item totals, PDF generation, and notification. Putting all this in the mutation would be a mess. Using CreateInvoiceService
makes the process maintainable and testable.
๐จ Error Handling and Response Patterns in GraphQL (Rails)
๐ง Detailed Explanation
In GraphQL, errors donโt work like they do in REST. Even if something goes wrong, GraphQL still responds with 200 OK
. That means you must handle errors inside the response body to let the client know what happened.
With the graphql-ruby
gem, there are two main ways to handle errors in mutations:
- Raise GraphQL::ExecutionError โ This sends the error to the
errors
array in the GraphQL response. - Use a custom return type โ You can return keys like
success
,errors
, and your object (e.g.,user
) to clearly describe the result.
This structure helps frontend developers handle both success and failure without needing to check HTTP codes.
For example, instead of this:
# Bad Example
raise "Email already taken"
Do this:
if user.save
{ user: user, success: true, errors: [] }
else
{ user: nil, success: false, errors: user.errors.full_messages }
end
Why it matters: Clients can use the success
and errors
fields to decide what message to show or what UI to render โ without relying on network errors.
๐๏ธ Best Implementation (Step-by-Step)
Weโll create a user registration mutation that returns a structured response with user
, success
, and errors
.
1. โ Generate Mutation File
rails g graphql:mutation RegisterUser
2. โ Define Mutation with Structured Response
# app/graphql/mutations/register_user.rb
module Mutations
class RegisterUser < BaseMutation
argument :email, String, required: true
argument :password, String, required: true
field :user, Types::UserType, null: true
field :success, Boolean, null: false
field :errors, [String], null: true
def resolve(email:, password:)
user = User.new(email: email, password: password)
if user.save
{
user: user,
success: true,
errors: []
}
else
{
user: nil,
success: false,
errors: user.errors.full_messages
}
end
end
end
end
3. โ Add to Mutation Type
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :register_user, mutation: Mutations::RegisterUser
end
end
4. โ Example Mutation Query
mutation {
registerUser(email: "[email protected]", password: "password123") {
user {
id
email
}
success
errors
}
}
5. โ Example Success Response
{
"data": {
"registerUser": {
"user": {
"id": "1",
"email": "[email protected]"
},
"success": true,
"errors": []
}
}
}
6. โ Example Error Response
{
"data": {
"registerUser": {
"user": null,
"success": false,
"errors": ["Email has already been taken"]
}
}
}
Summary:
- โ Return structured fields instead of throwing exceptions.
- โ
Make frontend error handling predictable with
success
anderrors
. - โ
Provide specific messages to users with
user.errors.full_messages
.
๐ก Examples
# mutation_type.rb
field :createUser, mutation: Mutations::CreateUser
# mutations/create_user.rb
def resolve(name:, email:)
user = User.new(name: name, email: email)
if user.save
{ user: user, success: true, errors: [] }
else
raise GraphQL::ExecutionError, user.errors.full_messages.join(", ")
end
end
๐ Alternative Concepts
- Returning
errors: [String]
field instead of raising exceptions. - Using a result object pattern with
success
,errors
, anddata
fields. - Wrapping logic in service objects for testable and reusable error handling.
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: How do you handle validation errors in a GraphQL mutation?
A: Instead of raising an exception, return a structured response with a success
flag and errors
array. This allows the frontend to handle the result gracefully.
{
user: nil,
success: false,
errors: user.errors.full_messages
}
Q2: What fields should you include in a mutation response?
A: At a minimum, include:
data
(e.g., user, post)success
(Boolean)errors
(Array of messages)
field :user, Types::UserType, null: true
field :success, Boolean, null: false
field :errors, [String], null: true
Q3: Why not raise exceptions directly in the resolver?
A: Exceptions stop execution and return GraphQL-level errors, which are harder to control and less user-friendly. Structured responses give better UX and allow multiple errors to be shown.
Q4: How do you show different types of errors (e.g., auth vs validation)?
A: Add additional fields like auth_error
or use different keys in the errors
array (e.g., { field: 'email', message: 'taken' }).
errors: [
{ field: "email", message: "has already been taken" }
]
Q5: How do you return multiple error messages from a model?
A: Use user.errors.full_messages
in the resolver or service object.
if user.invalid?
return { success: false, errors: user.errors.full_messages }
end
โ Best Practices (with Examples)
1. Always Include a success
Flag
This clearly communicates the mutation result to the frontend.
field :success, Boolean, null: false
def resolve(...)
{ success: true, errors: [] }
end
2. Avoid Raising Exceptions for Business Logic
Instead, return structured errors in the response.
return { success: false, errors: ['Email already taken'] } unless user.save
3. Standardize Your Error Structure
Return a consistent format that your frontend can easily parse.
errors: [
{ field: "email", message: "has already been taken" }
]
4. Use Service Objects to Isolate Business Logic
This keeps your mutation class clean and separates concerns.
result = Users::Create.call(params)
return { success: false, errors: result.errors } unless result.success?
5. Include Detailed Model Errors When Available
Use Railsโ full error messages for better UX.
user.errors.full_messages
# => ["Email can't be blank", "Password is too short"]
6. Return the Affected Object (if created/updated)
Allow the frontend to use returned object data immediately.
{
user: created_user,
success: true,
errors: []
}
๐ Real-World Use Case
In a user registration form, return both validation errors (like password length) and success status without crashing the entire mutation. This helps frontend handle errors gracefully.
๐จ Use of GraphQL::ExecutionError in Rails
๐ง Detailed Explanation
In a normal Rails app, if something goes wrong, you usually show an error or return an error code like 400 or 500. But in GraphQL, itโs different โ even if there is an error, the server will respond with 200 OK
. So, we need a special way to send errors in the response body.
Thatโs where GraphQL::ExecutionError
helps. Instead of crashing your app or returning an HTML error, it allows you to safely send a clear error message to the user inside the GraphQL response.
For example, if a user tries to sign up with an email thatโs already taken, you can do this:
raise GraphQL::ExecutionError, "Email has already been taken"
This puts the message inside the errors
part of the GraphQL response, so the frontend can show it to the user properly โ without the request crashing.
Why itโs useful: It keeps your app running smoothly and helps you return helpful error messages in a way that the frontend expects.
๐๏ธ Best Implementation (Step-by-Step)
Letโs build a simple user creation mutation where we return validation errors using GraphQL::ExecutionError
.
1๏ธโฃ Generate the Mutation File
Create a new mutation using Rails generator:
rails g graphql:mutation CreateUser
2๏ธโฃ Define the Mutation and Use ExecutionError
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :email, String, required: true
argument :password, String, required: true
field :user, Types::UserType, null: true
def resolve(email:, password:)
user = User.new(email: email, password: password)
if user.save
{ user: user }
else
# ๐ This returns a clean error message to the GraphQL client
raise GraphQL::ExecutionError, user.errors.full_messages.join(", ")
end
end
end
end
3๏ธโฃ Register the Mutation
Add the mutation to your root mutation type:
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser
end
end
4๏ธโฃ Run an Example Mutation
mutation {
createUser(email: "[email protected]", password: "123456") {
user {
id
email
}
}
}
5๏ธโฃ Error Example Response
If the email is taken or validation fails:
{
"errors": [
{
"message": "Email has already been taken",
"locations": [...],
"path": ["createUser"]
}
],
"data": {
"createUser": null
}
}
โ Summary:
- Use
GraphQL::ExecutionError
for readable client-side error handling. - It avoids breaking the response while still showing errors clearly.
- Ideal for validation or business logic errors.
๐ก Example
# app/graphql/mutations/create_user.rb
def resolve(name:)
user = User.new(name: name)
return user if user.save
raise GraphQL::ExecutionError, user.errors.full_messages.join(", ")
end
๐ Alternatives
- Returning structured fields like
success
anderrors
- Using result objects with
OpenStruct
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: What is GraphQL::ExecutionError
and why use it?
A: It's a special error class provided by graphql-ruby
that lets you return errors inside the GraphQL errors
array without crashing the entire response.
raise GraphQL::ExecutionError, "Email has already been taken"
Q2: Whatโs the difference between raising GraphQL::ExecutionError
vs returning errors
in the data object?
A: Raising GraphQL::ExecutionError
puts the error in the errors
array. Returning an errors
field inside data
keeps the result consistent and easier for clients to parse.
# Raises error to "errors" array
raise GraphQL::ExecutionError, "Invalid input"
# OR structured response
{ user: nil, success: false, errors: ["Invalid input"] }
Q3: How do you include field-specific validation messages in GraphQL errors?
A: Format the error with field/message pairs:
errors = user.errors.map do |field, msg|
{ field: field.to_s, message: msg }
end
raise GraphQL::ExecutionError, errors.to_json
Q4: How do you differentiate between authentication errors and validation errors?
A: Use custom messages or custom error classes, or add metadata:
raise GraphQL::ExecutionError.new("Unauthorized", extensions: { code: "AUTH_ERROR" })
Q5: Can you return both partial success and errors in a single mutation?
A: Yes, by returning a structured response like this:
{
data: {
updateUser: {
user: { name: "Updated" },
success: false,
errors: ["Email format invalid"]
}
}
}
โ Best Practices (with Examples)
1. Use GraphQL::ExecutionError
for unexpected or non-validation errors
For errors like authentication, permission denied, or system exceptions, raise GraphQL::ExecutionError
.
raise GraphQL::ExecutionError, "You are not authorized to perform this action"
2. Avoid raising for validation errors โ return structured data instead
Validation failures are not system errors; return them as part of the mutation response.
return {
user: nil,
success: false,
errors: user.errors.full_messages
}
3. Always include a success
flag and errors
array
This helps frontend developers handle response logic without relying on status codes.
field :success, Boolean, null: false
field :errors, [String], null: true
4. Use OpenStruct
or Result objects in service layers
Decouple business logic and provide success/error handling cleanly to the resolver.
if result.success?
{ user: result.user, success: true, errors: [] }
else
{ user: nil, success: false, errors: result.errors }
end
5. Add extensions
to GraphQL::ExecutionError
for custom error codes
Helps frontend distinguish error types programmatically.
raise GraphQL::ExecutionError.new("Permission Denied", extensions: { code: "AUTH_ERROR" })
6. Never expose raw exceptions to the client
Catch them and format the output for the client to avoid leaking internal logic.
rescue => e
raise GraphQL::ExecutionError, "An unexpected error occurred"
7. Log detailed errors but show generic messages to clients
For production environments, log full trace but return safe errors.
Rails.logger.error(e.message)
raise GraphQL::ExecutionError, "Something went wrong. Please try again."
๐ Real-World Use Case
In a multi-step checkout process, if the payment gateway fails, use GraphQL::ExecutionError
to alert the user while logging full stack traces server-side for debugging.
๐งฑ Consistent Error Structure in GraphQL (Rails)
๐ง Detailed Explanation
In a GraphQL API, even if something goes wrong (like a validation failure), the server still responds with a 200 OK
HTTP status. This means we can't rely on HTTP status codes to detect errors like we often do in REST APIs.
Instead, we must include error information inside the GraphQL response itself. The best way to do this is by using a consistent structure in all your mutation and query responses.
A good structure includes:
success
: a boolean showing whether the request was successfulerrors
: an array of messages or objects explaining what went wrongdata
: the result object (ornull
if there was an error)
By using this consistent format, frontend developers always know what to expect โ making it easier to display error messages or success messages to users.
๐๏ธ Best Implementation (Step-by-Step)
Letโs implement a mutation that uses a consistent error structure: success
, errors
, and the data
object.
1๏ธโฃ Generate the Mutation
rails g graphql:mutation CreatePost
2๏ธโฃ Define the Mutation with Standard Fields
# app/graphql/mutations/create_post.rb
module Mutations
class CreatePost < BaseMutation
argument :title, String, required: true
argument :content, String, required: true
field :post, Types::PostType, null: true
field :success, Boolean, null: false
field :errors, [String], null: true
def resolve(title:, content:)
post = Post.new(title: title, content: content)
if post.save
{
post: post,
success: true,
errors: []
}
else
{
post: nil,
success: false,
errors: post.errors.full_messages
}
end
end
end
end
3๏ธโฃ Register the Mutation
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_post, mutation: Mutations::CreatePost
end
end
4๏ธโฃ Sample Mutation Request
mutation {
createPost(title: "GraphQL in Rails", content: "This is a demo.") {
post {
id
title
}
success
errors
}
}
5๏ธโฃ Example Success Response
{
"data": {
"createPost": {
"post": {
"id": "1",
"title": "GraphQL in Rails"
},
"success": true,
"errors": []
}
}
}
6๏ธโฃ Example Failure Response
{
"data": {
"createPost": {
"post": null,
"success": false,
"errors": ["Title can't be blank"]
}
}
}
โ This structure ensures your frontend always receives:
- post: the created object (or
null
) - success: easy to check status
- errors: human-readable feedback
๐ก Example
# Good pattern:
{
success: false,
errors: [
{ field: "email", message: "has already been taken" }
],
data: null
}
๐ Alternative Concepts
- Flat errors:
errors: ["Email already taken"]
- Nested errors with metadata:
{ field, message, code }
- GraphQL::ExecutionError (for permission or system-level issues)
๐ ๏ธ Technical Questions & Answers (with Examples)
Q1: Why is it important to include a success
flag in GraphQL mutation responses?
A: GraphQL always returns a 200 HTTP statusโeven on failures. A success
boolean helps the frontend distinguish successful operations from failed ones without relying on HTTP status codes.
# response sample
{
success: false,
errors: ["Validation failed"]
}
Q2: How do you return model errors from a GraphQL mutation?
A: Use model.errors.full_messages
to convert Rails validation errors into a human-readable array and return it in the errors
field.
if user.invalid?
return {
user: nil,
success: false,
errors: user.errors.full_messages
}
end
Q3: Can you return field-specific error messages?
A: Yes, use a structured error array with field
and message
keys. This enables the frontend to show errors next to the correct input fields.
errors: [
{ field: "email", message: "has already been taken" },
{ field: "password", message: "is too short" }
]
Q4: How do you ensure all mutations follow the same error response structure?
A: Use a shared result object or service object that wraps all responses consistently. This reduces duplicate logic across mutations.
class Result
attr_reader :data, :errors
def initialize(success:, data: nil, errors: [])
@success = success
@data = data
@errors = errors
end
def to_h
{ success: @success, **@data, errors: @errors }
end
end
Q5: What happens if you raise GraphQL::ExecutionError
instead of returning an error structure?
A: The error appears in the root errors
array of the GraphQL response and not in the specific mutation field. This breaks structured frontend handling.
# โ Avoid this for validation errors
raise GraphQL::ExecutionError, "Invalid email"
โ Best Practices (with Examples)
1. Always Include a success
Flag
This clearly communicates whether the mutation succeeded, avoiding confusion from always-200 responses.
# return example
{
success: true,
errors: [],
user: { id: 1, email: "[email protected]" }
}
2. Use a Standard Error Format
Instead of just strings, return structured errors with field
and message
.
errors: [
{ field: "email", message: "has already been taken" },
{ field: "password", message: "is too short (minimum is 6 characters)" }
]
3. Keep Response Shape Predictable
All mutations should return a predictable structure with success
, errors
, and relevant objects.
# Every mutation response
{
success: Boolean,
errors: [String or Object],
result_object: Object (if any)
}
4. Never Raise Runtime Errors for Validation
Validation errors should be handled inside the mutation and returned in the response body.
# โ
return { success: false, errors: user.errors.full_messages }
# โ Don't do this
raise GraphQL::ExecutionError, "Invalid input"
5. Use Helpers or Services for Reusability
Create a shared `ResultBuilder` or `BaseResponse` class to enforce consistent responses across all mutations.
# app/services/base_response.rb
class BaseResponse
def self.success(data = {})
{ success: true, errors: [], **data }
end
def self.failure(errors)
{ success: false, errors: Array(errors) }
end
end
6. Include Contextual Info in Error Messages
Messages should be actionable and understandable to frontend users and developers.
errors: [
{ field: "username", message: "already taken. Try another one." }
]
๐ Real-World Use Case
In a form-heavy SaaS platform, using a consistent structure for mutation responses allowed frontend developers to create a universal form error handler that worked across registration, settings, profile update, and contact forms.
Learn more aboutย Railsย setup
Pingback: GraphQL Frontend Guide: Next.js & Apollo Best Practices - Randomize Blog