Code-first GraphQL with Strawberry
Asynchronous Django and GraphQL
Mutations with Apollo Client
In the previous chapter, we introduced the concept of schema-first for GraphQL APIs with Ariadne.
We explored queries and Apollo Client. In this chapter, we switch to a code-first approach to build our GraphQL API with Strawberry. In the process, we add mutations in the frontend to the mix, and we learn how to work with asynchronous code in Django.
The rest of this chapter assumes you are in the repo root decoupled-dj, with the Python virtual environment active and with DJANGO_SETTINGS_MODULE configured as decoupled_dj.settings.development.
Getting Started with Strawberry in Django
In the beginning, GraphQL was mainly targeted at JavaScript.
In this example, we declared a Python class with no behavior, but with two attributes, name and email. These attributes are also strongly typed; that is, they are able to enforce the kind of type they can hold, which is strings in this case. Strawberry makes heavy use of Python type hints. Type hints are an optional Python feature that can improve the robustness of our code. Python, much like JavaScript, is a dynamic language that does not enforce static types. With type hints, we can add a type layer to our Python code, which can be checked with a tool named MyPy before releasing the code in production. This can catch nasty bugs that could make their way to the runtime environment. Additionally, static type checks improve the developer experience. In Strawberry, we will use dataclasses to define our GraphQL types and type hints all along the way. Time to practice!
Installing Strawberry
decoupled_dj/settings/base.py - Enabling Strawberry in INSTALLED_APPS
With Strawberry enabled, we can move to refactoring the schema from schema-first to code-first.
Designing the GraphQL Schema in Strawberry
In the previous chapter, we created a GraphQL schema in a .graphql file.
billing/schema.graphql - The Original GraphQL Schema
In this schema, we used most of the GraphQL scalar types the language has to offer, plus our custom types and input types definitions. We also created two queries and a mutation. To appreciate what Strawberry has to offer, let’s port each element of our schema from a plain text schema to Python code.
Types and Enums in Strawberry
To start off we begin with the base types of our GraphQL schema.
billing/schema.py - Initial Imports
billing/schema.py - First Types in Strawberry
For someone new to Python typings, there a lot of things here that need a bit of explanation. Thankfully, Python is expressive enough to not overcomplicate things. Let’s start from the top.
To declare a new GraphQL type in Strawberry we use the @strawberry.type decorator, which goes on top of our dataclasses. Next, each type is declared as a dataclass, each containing a set of attributes. In Chapters 1 and 11, we saw GraphQL scalar types. In Strawberry there isn’t anything special to describe these scalars, apart from strawberry.ID. As you can see in Listing 12-4, most scalar types are represented as Python primitives: str, int, and bool. The only exception to this are the types for date and due_date, which we declared as strings in the original GraphQL schema. Since types in Strawberry are dataclasses, and dataclasses are “just” Python code, instead of strings for our date, we can now use datetime.date objects. This was one of our unsolved problems in Chapter 11, and it’s now fixed.
You may wonder what’s the deal with due_date here and dueDate from the previous chapter. In the original GraphQL schema, we used dueDate in camel case. Ariadne converts this syntax to snake case before it reaches the Django ORM. Now we use snake case again in the GraphQL schema. Why? Being Python code, the convention is to use snake case for longish variables and function names. But this time, the conversion happens the other way around: in the GraphQL documentation schema, Strawberry will display the field as camel case!
Moving forward, notice how the relations are described by associating the dataclass attribute with the corresponding entity, like the User dataclass assigned to user in Invoice. Also note that the List type from Python typings to associate ItemLine to items. Previously, we used the Float scalar from GraphQL for the price of each ItemLine. In Python, we can use a more appropriate decimal.Decimal. Even from a simple listing like this, we can deduce that the Strawberry approach to writing GraphQL schemas as Python code brings a lot of benefits, including type safety, flexibility, and better handling of scalar types.
billing/schema.py - Enum Type in Strawberry
This is quite similar to the choices for our Invoice model in Django. With a bit of creativity, one could reuse this Strawberry enum in the Django model (or the other way around). With the enum in place, we are almost ready to test things out. Let’s add resolvers and queries in the next sections.
Working with Resolvers (Again)
We already learned that a GraphQL schema needs resolvers to return data.
billing/schema.py - Adding Resolvers to the Schema
To avoid clashing with the User GraphQL type here, we import our user model as UserModel. Next up, we declare resolvers to fulfill the original GraphQL queries, namely getClient and getClients. Notice how we pass an id as the argument to the second resolver to fetch a single user by ID as we did in the previous chapter. With these resolvers in place we can add a Query type and finally wire up the GraphQL endpoint in the next section.
Queries in Strawberry and Wiring Up the GraphQL Endpoint
With the fundamental types and the resolvers in place, we can create a code-first Query type for our API.
billing/schema.py - Adding a Root Query Type
get_clients returns a list of Users and is connected to the resolver named resolve_clients
get_client returns a single User and is connected to the resolver named resolve_client
Both resolvers are wrapped with strawberry.field(). Notice that both queries will be converted to camel case in the GraphQL documentation, even though they are declared as snake case in our code. In the last line we load our schema into Strawberry, so it is picked up and served to the user. It is important to note that resolvers in Strawberry don’t have to be disconnected from the Query dataclass itself. In fact, we could have declared them as methods in the Query. We leave these two resolvers outside of the dataclass, but we will see mutations as methods of the Mutation dataclass in a moment.
billing/urls.py - Wiring Up the GraphQL Endpoint
Throughout the next sections, we will run Django under Uvicorn. Now we can head over to http://127.0.0.1:8000/billing/graphql/. This should open GraphiQL, a playground for exploring the GraphQL API. In the playground, we can send out queries and mutations and explore the schema and the integrated documentation, just as we did with Ariadne. Now that you have the big picture, you can complete the schema with input types and mutations.
Input Types and Mutations in Strawberry
We saw that input types in GraphQL are basically arguments for a mutation.
billing/schema.py - Adding Input Types to the Schema
billing/schema.py - Adding a Mutation to the Schema
This code bears a bit of explanation. First off, we import our Django models again, this time by aliasing them to avoid clashes with the dataclasses. Next up, we define a Mutation and a method inside it. The method, named create_invoice(), takes InvoiceInput input type as a parameter and is decorated with @strawberry.mutation. Inside the method we convert the dataclass input type to a dictionary. This is important because the mutation parameter is a dataclass, not a dictionary. This way, we can pop out the keys we need, as we did with Ariadne. In the mutation, we also pop out state, which is later passed as state.value to InvoiceModel.objects.create(). At the time of this writing, Strawberry doesn’t convert automatically enums keys to strings, so we need to do a bit of data drilling. Finally, notice the type annotation for the return value of this mutation, Invoice. At the very end of the file we also load the mutation dataclass into the schema.
Mutation and input types complete our GraphQL schema for now. At this stage we could use the GraphiQL Playground to send an invoiceCreate mutation, but instead of trying things manually, we will implement the mutation in our React frontend. But first, let’s look at the implications of running Strawberry asynchronously with Django.
Working Asynchronously with the Django ORM
After setting everything up, you might have noticed that by sending even a simple query in GraphiQL, everything blows up.
billing/schema.py - Converting ORM Queries to Work Asynchronously
Let’s look at what is going on here. First off, we move the ORM logic to two regular functions. In the first function, _get_all_clients(), we fetch all the clients from the database with .all(). We also force Django to evaluate the queryset by converting it to a list with list(). It’s necessary to evaluate the query in the asynchronous context, because Django querysets are lazy by default. In the second function, _get_client(), we simply get a single user from the database. Both functions are then called in two asynchronous functions, wrapped in sync_to_async(). This machinery will make ORM code work under ASGI.
billing/schema.py - Converting ORM Queries to Work Asynchronously
This might seem like a lot of code to do something that Django provides out-of-the-box, namely SQL queries, but this is the price to pay at this moment in order to run Django asynchronously. In the future, we hope to have better async support for the ORM layer. For now, with these changes, we are ready to run Strawberry and Django side by side asynchronously. We can now move to the frontend to implement mutations with Apollo Client.
Working Again on the Frontend
In Chapter 11, we began to work on a React/TypeScript frontend, which acted as a client for our GraphQL API.
So far, we implemented a simple query in the frontend for a <select> component. First, we worked with Apollo client.query(), which is a lowish-level method for making queries. Then, we refactored to use the useQuery() hook. In the following sections, we tackle mutations in the frontend with Apollo Client and useMutation().
For the React part, we work in decoupled_dj/billing/react_spa. Each proposed file must be created or changed in the appropriate subfolder, starting from this path.
Creating Invoices with a Mutation
src/App.tsx - GraphQL Query with Apollo
src/App.tsx - Importing useMutation
src/App.tsx - Declaring a Mutation
src/App.tsx - Using the useMutation Hook
src/App.tsx - Using the useMutation Hook with Loading and Error
src/App.tsx - The Complete Form
This form contains all the inputs for creating a new invoice. At the bottom, note the conditional rendering based on the state of mutationLoading.
We build the mutation payload starting from a FormData
In the building logic, we convert quantity to an integer and taxed to a Boolean
src/App.tsx - Logic for Sending the Mutation
We send out the mutation with createInvoice() by providing an invoice payload
If everything goes well, we reset the form with event.target.reset()
In Figure 12-1, we can see the data object containing a property named createInvoice, which holds the fields we requested from the mutation. We can also see __typename. This is part of GraphQL’s introspection capabilities, which make it possible to ask GraphQL “what type is this object”? An explanation of GraphQL introspection is out of the scope of this book, but the official documentation is a good starting point for learning more.
It is a good moment to commit the changes you made so far and to push the work to your Git repo. You can find the source code for this chapter at https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_12_graphql_strawberry.
What’s Next?
Authentication and deployment: In fully decoupled setups, GraphQL works well with JWT tokens for authentication. However, the specification does not enforce any particular type of authentication method. This means it is possible to use session-based authentication for GraphQL API, as we saw in Chapter 10 for REST.
Subscriptions: GraphQL Python libraries for Django can integrate with Django Channels to provide subscriptions over WebSocket.
Testing: Testing GraphQL API does not involve any magic. Since they accept and return JSON, any testing HTTP client for Python or Django can be used to test a GraphQL endpoint.
Sorting, filtering, and pagination: It’s easy to sort, filter, and paginate responses with Django and the Django REST Framework tools. However, to implement the same things in GraphQL, we need to write a bit of code by hand. But since GraphQL queries accept arguments, it’s not so hard to build custom filtering capabilities in a GraphQL API.
Performances : Since queries in GraphQL can be nested, great care must be taken to avoid crashing our database with N+1 queries. Most GraphQL libraries include a so-called dataloader, which takes care of caching database queries.
The wireframe in Chapter 6 has a Send Email button. Try to implement this logic in the React frontend, with a mutation. On the backend, you will also need a new asynchronous mutation to send the email.
Add tests to this simple application: you can test the GraphQL endpoint with Django testing tools and test the interface with Cypress.
Summary
Use GraphQL mutations in the backend and in the frontend
Work with asynchronous Django
Now it’s your turn! Go build your next Django project!