EctoGraphql
ecto_graphql is a library for deriving Absinthe GraphQL APIs from Ecto schemas.
It derives:
- GraphQL object and input types from Ecto schemas
- Association fields with automatic Dataloader resolution
- Query and mutation definitions
- Resolver stubs ready for your business logic
- Automatic integration with your root schema
The goal is to eliminate repetitive boilerplate by deriving your GraphQL API directly from your Ecto schemas.
Installation
Add the dependency to your mix.exs:
def deps do
[
{:ecto_graphql, "~> 0.4.0"},
{:dataloader, "~> 2.0"} # Required for association support
]
endThen run:
mix deps.getWhat Gets Generated
Using a single Mix task, EctoGraphql generates:
- GraphQL types — object types and input types for mutations
- Queries — list all and get by ID
- Mutations — create, update, and delete operations
- Resolvers — function stubs for you to implement business logic
- Automatic imports — seamless integration into your root schema
All generated code is plain Elixir that you can modify, extend, or refactor as needed.
Mix Task
From Ecto Schema (Recommended)
mix gql.gen Accounts lib/example/accounts/user.exThis reads the Ecto schema file and automatically:
- Extracts all schema fields
- Maps Ecto types to GraphQL types
- Generates type definitions, queries, mutations, and resolvers
- Integrates generated modules into your root schema
Override Schema Name
mix gql.gen Accounts Person lib/example/accounts/user.exUse this when your GraphQL schema name should differ from the Ecto table name.
Manual Field Definition
mix gql.gen Accounts User name:string email:string age:integerFor quick prototyping or when you don't have an Ecto schema yet.
Generated File Structure
For context Accounts and schema User, the generator creates:
lib/example_web/graphql/accounts/
├── type.ex # GraphQL object and input types
├── schema.ex # Query and mutation definitions
└── resolvers.ex # Resolver function stubsExisting files are updated intelligently without overwriting your custom code.
Example Ecto Schema
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :password_hash, :string
timestamps(type: :utc_datetime)
end
endRuntime Macros
EctoGraphql provides two powerful macros for defining GraphQL types at compile-time from your Ecto schemas:
gql_object- Creates complete object definitionsgql_fields- Generates field definitions within existing objects
Quick Start
defmodule MyAppWeb.Schema.Types do
use Absinthe.Schema.Notation
use EctoGraphql
# Complete object definition
gql_object(:user, MyApp.Accounts.User)
# Or use gql_fields within an object
object :product do
gql_fields(MyApp.Catalog.Product)
end
endAssociation Support
EctoGraphql automatically detects has_one, has_many, and belongs_to associations and generates fields with Dataloader resolvers:
# Given this Ecto schema:
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field :name, :string
has_one :profile, MyApp.Accounts.Profile
has_many :posts, MyApp.Blog.Post
end
end
# This:
gql_object(:user, MyApp.Accounts.User)
# Generates:
object :user do
field :id, :id
field :name, :string
field :profile, :profile, resolve: dataloader(:ecto)
field :posts, list_of(:post), resolve: dataloader(:ecto)
endNote: Input objects (gql_input_object) automatically exclude associations since they're not valid input types.
Dataloader Setup
To use associations, configure Dataloader in your schema:
defmodule MyAppWeb.Graphql.Schema do
use Absinthe.Schema
def context(ctx) do
loader =
Dataloader.new()
|> Dataloader.add_source(:ecto, Dataloader.Ecto.new(MyApp.Repo))
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
endEcto.Enum Support
EctoGraphql automatically detects Ecto.Enum fields and generates corresponding GraphQL enum types.
Defining Enum Fields in Ecto
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :status, Ecto.Enum, values: [:active, :inactive, :pending]
field :role, Ecto.Enum, values: [:admin, :user, :guest]
end
end
Using gql_enums Macro
To generate GraphQL enum types, use the gql_enums macro at the top level (not inside object blocks):
defmodule MyAppWeb.Schema.Types do
use Absinthe.Schema.Notation
use EctoGraphql
# Generate enum types from Ecto.Enum fields
gql_enums(MyApp.Accounts.User)
# Then define your objects
gql_object(:user, MyApp.Accounts.User)
endThis generates:
enum(:user_status, values: [:active, :inactive, :pending])
enum(:user_role, values: [:admin, :user, :guest])
object :user do
field :id, :id
field :name, :string
field :status, :user_status # References the enum type
field :role, :user_role # References the enum type
endEnum Naming Convention
Enum types are automatically named using the pattern: {schema_name}_{field_name}
Userschema withstatusfield →:user_statusenum typePostschema withvisibilityfield →:post_visibilityenum type
Filtering Enum Generation
You can control which enums are generated:
# Generate only specific enum types
gql_enums(MyApp.User, only: [:status])
# Exclude specific enum types
gql_enums(MyApp.User, except: [:internal_status])gql_object - Complete Object Definitions
Use gql_object to quickly create a complete GraphQL object from an Ecto schema.
Basic Usage
# Generate all fields
gql_object(:user, MyApp.Accounts.User)Field Filtering
# Include only specific fields
gql_object(:user_public, MyApp.Accounts.User, only: [:id, :name, :email])
# Exclude sensitive fields
gql_object(:user, MyApp.Accounts.User, except: [:password_hash, :recovery_token])Custom Fields
Add or override fields using a do block:
gql_object :user, MyApp.Accounts.User do
# Add a custom field
field :full_name, :string do
resolve fn user, _, _ ->
{:ok, "#{user.first_name} #{user.last_name}"}
end
end
# Override an auto-generated field
field :email, :string do
resolve fn user, _, _ ->
if user.email_public, do: {:ok, user.email}, else: {:ok, "[hidden]"}
end
end
endCombining Options and Custom Fields
gql_object :user, MyApp.Accounts.User, except: [:inserted_at, :updated_at] do
field :member_since, :string do
resolve fn user, _, _ ->
days = DateTime.diff(DateTime.utc_now(), user.inserted_at, :day)
{:ok, "#{days} days"}
end
end
endNon-null Fields
Mark fields as non_null to make them required in GraphQL. This matches GraphQL's type system where non_null fields cannot be null.
# Mark specific fields as non-null
gql_object(:user, MyApp.Accounts.User, non_null: [:id, :name, :email])
# Generates:
# field :id, non_null(:id)
# field :name, non_null(:string)
# field :email, non_null(:string)
# field :password_hash, :string # nullableOverride with :nullable (takes precedence):
gql_object(:user, MyApp.Accounts.User,
non_null: [:id, :name, :email],
nullable: [:email] # Make email nullable despite being in non_null
)
# Result:
# field :id, non_null(:id)
# field :name, non_null(:string)
# field :email, :string # nullable due to overrideImportant:non_null is NOT applied to input_object types, as input fields are typically optional:
gql_input_object(:user_input, MyApp.Accounts.User, non_null: [:name])
# All fields remain nullable in input objectsgql_fields - Field Generation
Use gql_fields when you need fine-grained control over your object structure.
Basic Usage
object :user do
gql_fields(MyApp.Accounts.User)
endMixing with Custom Fields
object :user do
gql_fields(MyApp.Accounts.User, except: [:password_hash])
# Add custom fields
field :avatar_url, :string do
resolve fn user, _, _ ->
{:ok, "https://cdn.example.com/avatars/#{user.id}.jpg"}
end
end
field :is_admin, :boolean do
resolve fn user, _, _ ->
{:ok, user.role == :admin}
end
end
endMultiple Schemas in One Object
object :user_profile do
gql_fields(MyApp.Accounts.User, only: [:id, :name, :email])
gql_fields(MyApp.Accounts.Profile, except: [:user_id, :id])
# Add computed fields
field :display_name, :string
end
Non-null with gql_fields
The non_null and nullable options work the same way with gql_fields:
object :user do
gql_fields(MyApp.Accounts.User, non_null: [:id, :name, :email])
endWhen to Use Each Macro
Use gql_object when:
- You want a quick, complete object definition
- Most fields map directly from your Ecto schema
- You only need to add a few custom fields
Use gql_fields when:
- You need precise control over field ordering
- You're combining fields from multiple schemas
- You want to mix auto-generated and custom fields explicitly
- You're building complex object structures
Mix Tasks
Generate GraphQL schemas, types, and resolvers from Ecto schemas using Mix tasks:
Generate from Ecto Schema
mix gql.gen Accounts lib/my_app/accounts/user.exThis generates:
lib/my_app_web/graphql/accounts/types.ex- Object and input_object typeslib/my_app_web/graphql/accounts/schema.ex- Query and mutation definitionslib/my_app_web/graphql/accounts/resolvers.ex- Resolver function stubs
Initialize
mix gql.gen.initGenerated GraphQL Types
object :user do
field(:id, :id)
field(:name, :string)
field(:email, :string)
field(:inserted_at, :datetime)
field(:updated_at, :datetime)
end
input_object :user_params do
field(:id, :id)
field(:name, :string)
field(:email, :string)
field(:inserted_at, :datetime)
field(:updated_at, :datetime)
endGenerated Resolvers
Resolver stubs are created for you to implement your business logic:
def list_users(_parent, _args, _resolution) do
{:ok, Accounts.list_users()}
end
def get_user(_parent, %{id: id}, _resolution) do
Accounts.get_user!(id)
end
def create_user(_parent, args, _resolution) do
Accounts.create_user(args)
end
def update_user(_parent, %{id: id} = args, _resolution) do
user = Accounts.get_user!(id)
Accounts.update_user(user, args)
endThis preserves the separation between your GraphQL layer and business logic.
Automatic Schema Integration
Generated modules are automatically imported into your root schema:
lib/example_web/graphql/types.ex:
defmodule ExampleWeb.Graphql.Types do
use Absinthe.Schema.Notation
# Import generated types here
import_types(ExampleWeb.Graphql.Accounts.Schema)
endlib/example_web/graphql/schema.ex:
defmodule ExampleWeb.Graphql.Schema do
use Absinthe.Schema
import_types(Absinthe.Type.Custom)
import_types(ExampleWeb.Graphql.Types)
query do
import_fields(:user_queries)
end
mutation do
import_fields(:user_mutations)
end
endNo manual wiring required. If these files don't exist, they'll be created for you.
Type Mapping
Ecto types are intelligently mapped to GraphQL types:
| Ecto Type | GraphQL Type |
|---|---|
:binary_id | :id |
:string | :string |
:integer | :integer |
:boolean | :boolean |
:utc_datetime | :datetime |
:map | :json |
See the full documentation for complete type mapping reference.
Features
- ✅ Automatic field extraction from Ecto schemas
- ✅ Association support with Dataloader resolution
- ✅ Ecto.Enum support with automatic enum type generation
- ✅ Non-null field support for required fields
- ✅ Smart type mapping (Ecto → GraphQL)
-
✅ Table name singularization (
users→user) - ✅ Auto-integration with existing schemas
-
✅ Customizable EEx templates in
priv/templates - ✅ Incremental updates — doesn't overwrite existing files
- ✅ Phoenix-friendly structure and conventions
Philosophy
EctoGraphql follows these principles:
- Generated code is yours — modify, extend, or refactor as needed
- No runtime magic — plain Absinthe code you can read and understand
- Explicit over clever — predictable generation, no surprises
- Single source of truth — Ecto schemas drive your GraphQL API
If the generated code is hard to read or modify, it doesn't belong here.
Documentation
Full documentation is available on HexDocs:
https://hexdocs.pm/ecto_graphql
License
MIT