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.
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.getUsing 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 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
mix gql.gen Accounts Person lib/example/accounts/user.exUse this when your GraphQL schema name should differ from the Ecto table name.
mix gql.gen Accounts User name:string email:string age:integerFor quick prototyping or when you don't have an Ecto schema yet.
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 stubs
Existing files are updated intelligently without overwriting your custom code.
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
endEctoGraphql 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
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
endEctoGraphql 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.
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
endEctoGraphql automatically detects Ecto.Enum fields and generates corresponding GraphQL enum types.
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
endTo 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 types are automatically named using the pattern: {schema_name}_{field_name}
Userschema withstatusfield →:user_statusenum typePostschema withvisibilityfield →:post_visibilityenum type
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])Use gql_object to quickly create a complete GraphQL object from an Ecto schema.
# Generate all fields
gql_object(:user, MyApp.Accounts.User)# 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])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
endgql_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
endMark 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 objectsUse gql_fields when you need fine-grained control over your object structure.
object :user do
gql_fields(MyApp.Accounts.User)
endobject :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
endobject :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
endThe 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])
endUse 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
Generate GraphQL schemas, types, and resolvers from Ecto schemas using Mix tasks:
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
mix gql.gen.initobject :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)
endResolver 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.
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.
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.
- ✅ 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
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.
Full documentation is available on HexDocs:
https://hexdocs.pm/ecto_graphql
MIT