GraphQL in Django: queries, mutations, and permissions

Build a Django GraphQL API with Graphene-Django, bounded queries, exact decimal prices, staff-only mutations, and explicit authorization.

Best practices
Illustration of connected application signals and engineering workflows

GraphQL lets a client select the fields it needs and traverse related data through one schema. In Django, Graphene-Django connects that schema to models and querysets. This can reduce unnecessary response fields and extra client round trips, but it does not automatically reduce database queries or grant access safely.

This example builds a public book catalog with staff-only creation. It uses Django and Graphene-Django 3; the public-read policy is intentional. A private or tenant-scoped catalog needs additional queryset restrictions on every access path.

Decide whether GraphQL fits the API

A mobile list may need three fields while a detail view needs a book, its author, and related information. GraphQL allows those clients to request different shapes without creating a new REST endpoint for every combination.

The server still owns the schema, resolvers, permissions, and cost controls. A client cannot request a field the schema does not expose, and a new business capability still needs server work. REST remains a straightforward choice for simple resource operations. Many applications use both.

Set up Graphene-Django

In an existing Django project with an application named blog, install and lock compatible dependency versions:

python -m pip install graphene-django

Add the integration without replacing the standard Django apps:

# settings.py: append to the existing list.
INSTALLED_APPS += ["graphene_django"]
GRAPHENE = {"SCHEMA": "blog.schema.schema"}

Ensure blog is also installed. Keep Django's session, authentication, and CSRF middleware. In the project's URL configuration:

from django.conf import settings
from django.urls import path
from graphene_django.views import GraphQLView

urlpatterns = [
    path("graphql", GraphQLView.as_view(graphiql=settings.DEBUG)),
]

Merge this route into existing urlpatterns. GraphiQL is an interactive development UI, not an access-control mechanism. Disable it in production and apply the API's authentication and authorization policies separately. Session-authenticated POST requests need a valid CSRF token; do not exempt the endpoint merely to make mutations work. See Graphene-Django installation.

Define the models and exposed fields

The example uses these models in blog/models.py:

from decimal import Decimal
from django.core.validators import MinValueValidator
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=200)

class Book(models.Model):
    title = models.CharField(max_length=200)
    price = models.DecimalField(
        max_digits=10, decimal_places=2,
        validators=[MinValueValidator(Decimal("0.00"))],
    )
    author = models.ForeignKey(Author, on_delete=models.PROTECT)

Create and apply the migration in your development database, then add at least one author before creating a book:

python manage.py makemigrations blog
python manage.py migrate

In blog/schema.py, explicitly allow the fields clients may see:

import graphene
from graphene_django import DjangoObjectType
from .models import Author, Book

class AuthorType(DjangoObjectType):
    class Meta:
        model = Author
        fields = ("id", "name")

class BookType(DjangoObjectType):
    class Meta:
        model = Book
        fields = ("id", "title", "price", "author")

class Query(graphene.ObjectType):
    all_books = graphene.List(BookType)
    book = graphene.Field(BookType, id=graphene.Int(required=True))
    books = graphene.List(
        BookType,
        title_contains=graphene.String(),
        max_price=graphene.Decimal(),
    )

    def resolve_all_books(root, info):
        return Book.objects.select_related("author").order_by("id")[:100]

    def resolve_book(root, info, id):
        return Book.objects.select_related("author").filter(pk=id).first()

    def resolve_books(root, info, title_contains=None, max_price=None):
        qs = Book.objects.select_related("author").order_by("id")
        if title_contains:
            qs = qs.filter(title__icontains=title_contains)
        if max_price is not None:
            if not max_price.is_finite() or max_price < 0:
                from graphql import GraphQLError
                raise GraphQLError("Invalid maximum price")
            qs = qs.filter(price__lte=max_price)
        return qs[:100]

schema = graphene.Schema(query=Query)

Graphene's default field naming exposes all_books as allBooks and title_contains as titleContains. A client can select only the title and author name:

query Catalog {
  allBooks {
    title
    author { name }
  }
}

select_related("author") loads the foreign-key relationship in the same SQL query for this resolver. It addresses this specific N+1 pattern; it does not optimize every nested relationship elsewhere in the schema. Use prefetch_related or a suitable batching loader when the relationship and execution model require it.

Filter a bounded query

The books field in the same Query class combines optional filters:

query AffordableBooks {
  books(titleContains: "Django", maxPrice: "60.00") {
    id
    title
    price
    author { name }
  }
}

Money uses a decimal model field and Graphene's Decimal scalar, with a string literal at the GraphQL boundary. This avoids converting the price through a binary floating-point argument. See Graphene scalar types.

The 100-row cap is a demonstration boundary, not a complete pagination design. Add stable, bounded pagination for larger catalogs. django-filter can help define filters, but generated filters still need review for access and query cost.

In a private application, apply the same visibility policy to lists, individual lookups, mutations, and any Relay node access. Hiding a field is different from restricting which objects a caller can fetch. See Graphene-Django authorization.

Add a staff-only mutation

Append the following classes to blog/schema.py, and replace the final schema assignment with the one shown here:

from django.core.exceptions import ValidationError
from graphql import GraphQLError

class CreateBook(graphene.Mutation):
    class Arguments:
        title = graphene.String(required=True)
        price = graphene.Decimal(required=True)
        author_id = graphene.Int(required=True)

    book = graphene.Field(BookType)

    def mutate(root, info, title, price, author_id):
        user = info.context.user
        if not user.is_authenticated or not user.is_staff:
            raise GraphQLError("You do not have permission to create books")
        if not price.is_finite():
            raise GraphQLError("Invalid book fields")
        book = Book(title=title, price=price, author_id=author_id)
        try:
            book.full_clean()
        except ValidationError:
            raise GraphQLError("Invalid book fields")
        book.save()
        return CreateBook(book=book)

class Mutation(graphene.ObjectType):
    create_book = CreateBook.Field()

schema = graphene.Schema(query=Query, mutation=Mutation)

An authenticated staff client can submit:

mutation CreateCatalogBook {
  createBook(title: "Learning Django", price: "59.90", authorId: 1) {
    book { id title price }
  }
}

Use an author ID that exists. full_clean() checks model validation, including the price constraints and foreign-key value, before saving. It is not called automatically by save(), nor does it replace database constraints or eliminate concurrent-write races. Add the transaction and error policy required by the real data model.

The staff flag is this example's creation rule. Production systems may need a specific permission and restrictions on which author or tenant the caller may modify. Do not expose raw validation or database details to clients. Also inspect GraphQL's errors array: an HTTP response can contain execution errors and partial data, so transport status alone is insufficient for application-error monitoring.

Control query cost and observe the result

Measure SQL count, result count, and latency for a named operation before and after resolver changes. A single HTTP request can still issue many queries or return a large nested result. Apply depth and complexity controls appropriate to your GraphQL implementation, plus pagination, request-size, timeout, and rate limits. A list cap on one field does not bound every possible query.

Authentication identifies the caller; resolvers and domain services must enforce access to the requested records. For instrumentation, record the operation name and useful timing/error metadata rather than indiscriminately logging variables that may contain personal data.

When using Guance, collect application errors through DataKit logging and verify that GraphQL execution failures remain visible even when the HTTP layer reports success. Use tracing instrumentation compatible with the Django deployment to connect resolver work to database calls. The presence of a /graphql request span alone does not establish resolver-level coverage.

Common questions

Will GraphQL replace REST?

It need not. REST can be simpler for predictable resource operations; GraphQL is useful when clients need different shapes of related data. Choose according to access patterns and operating requirements.

Should a new Django project use Graphene or Strawberry?

Graphene provides the model-oriented integration used here; Strawberry builds schemas around Python type annotations. Compare current Django integration, permissions, async behavior, pagination, and ecosystem compatibility using a representative operation rather than choosing on syntax alone.

How should file uploads work?

File upload is not part of the core GraphQL transport contract. A multipart-upload extension requires compatible client/server libraries and deliberate CSRF and upload limits. A separate authenticated upload endpoint or direct object-storage upload flow can be simpler; pass the resulting authorized file reference into a mutation.

For related examples, the Flask WebSocket guide and FastAPI GraphQL guide are currently in Chinese.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting