Introduction
When it comes to writing an API, sometimes you might have several data sources and want to coalesce them into one easy-to-query API on the frontend. This is where GraphQL comes in: an query language made for APIs and declarative data fetching (you only query what you want). Here are some advantages that GraphQL can bring to your Rust web application:
- Test your queries out in real-time via the GraphQL playground
- Makes it much easier for your frontend to query your backend
- You can use any data source
In this example, we will use GraphQL through the async-graphql
Rust crate as an Axum endpoint with an SQL data source and we'll be creating an API that can create, update, and delete a table of records about dogs, as well as subscribing to any updates.
Stuck or want to know what the final code looks like? You can find the repository here.
Getting Started
You'll want to initiate a new Shuttle project (requires cargo-shuttle
):
For this article we'll be using the project name "graphql-example". When the CLI asks you what framework you want, pick Axum.
Next, you'll want to make a migrations schema file like so in the root of your project:
Now you'll want to install the required dependencies. We can do this with a one-line command:
Setting up GraphQL
At the very minimum, we'll want to create an endpoint that serves the GraphQL playground so we can quickly try queries out, and then a basic "Hello world!" query in GraphQL. Let's have a look at how this would look in code:
If we use shuttle run
to load up our program and go to http://localhost:8000
, we should see the GraphQL playground.
Clicking the Queries on the left hand side will show us all the queries we can run - there should be one called "howdy" (which corresponds to the function we wrote under the Query implementation). You can verify it works by running it.
Now we're ready to get started on queries, mutations and subscriptions!
Queries
Although a simple "hello world" query shows how basic data fetching works, we probably want to figure out how to do more complicated queries: for example, returning some records from our SQL data source. Let's change our impl Query
to include a method for getting a list of Dogs:
As you can see, we've written a function that returns the vector of structs. Because we're using query_as
, it automatically binds the query results to the structs so we don't have to worry about mapping it out - you can find more about this here.
What about if we want to query only specific rows based on filter criteria? We will want to make sure to add a description on each of the parameters so that when anyone visits our GraphQL playground, they'll be able to understand what each of the parameters actually does - then in our SQL query, we will want to filter conditionally based on what parameters have been filled in:
Ideally, we want to be able to use GraphQL to extract data out of it by only calling specific fields. Now that we've retrieved our records, we can write an impl
for our Dog struct, like so (make sure to attach the #[Object]
macro so it gets picked up by GraphQL!):
If you run shuttle run
and go to http://localhost:8000
, you'll be able to see that if you click on Queries on the left-hand side, it'll let you use dogs
as a query.
Mutations
Now for the next part: mutations! Mutations in GraphQL are methods for changing our data through GraphQL. To use a mutation, we need to create a unit struct (for this article we'll call it Mutation
) and create an impl
for it with the #[Object]
macro, just like with the GraphQL queries.
As you can see, it's practically the same as if we just did it normally in SQL - we grab the SQL connection and insert the record, then return the ID. We'll also want to be able to only update certain parameters - for example, if a dog's name needs to be updated but not their age. We learned about how we can use optional parameters in async-graphql
, and we can write the function like so:
Similarly, we can do it exactly the same way for delete functions.
Subscriptions
Subscriptions in GraphQL are a way of subscribing to changes - for example, when a new record gets created or updated, you might want a way for your users to know about or get real time updates. Subscriptions in this respect are similar to PostgreSQL Listen/Notify functions which you can use to listen and notify updates through channels in Postgres - which if you don't know about yet, which you can read more about here.
In async-graphql
, subscriptions are types that implement futures_util::Stream
and always return an impl Stream<Item = T>
; that is to say, the type we're returning needs to implement Stream
so that the compiler knows that the type can return a stream of data. The most common way to do this is through types that wrap channel Senders/Receivers, and we will show how to do this below.
We can get started by defining some types:
The BrokerStream
struct doesn't get added to the Subscribers hashmap itself, but is returned to the users. When users subscribe to the GraphQL subscription, we create a channel with a Sender/Receiver and then insert the Sender<T>
into the subscribers list while returning the receiver to the HTTP client.
Next, we'll want to set up the methods needed for our stuff to work. Let's start with retrieving the list of senders from the HashMap:
There's quite a few generics here, but don't be intimidated: these are simply required in order for the function to be usable with more than one time. Let's break it down:
- The
T
type must implement Sync, Send, Clone and 'static. This means that the type must be able to be marked as being able to safely share and synchronise across threads. - The
F
type is a function that must implement a closure, where the item inside a closure is aSenders<T>
(which we created earlier).
Next, we need to implement Drop
and futures_util::Stream
for our type - for Drop
we want a custom implementation because we need it to work a specific way. futures_util::Stream
is required by async-graphql
for the type to work.
Note that the T type, as above, requires Sync + Send + Clone + 'static
- this is also required for us to use more than one type with the SimpleBroker
. Otherwise, we will end up being able to only stream one type - which is good in some cases, but let's assume we want to stream more than one type eventually and will therefore, need to make it generic.
Once we're done with the above, we need to write the implementation for the broker itself. See below:
Our code for writing the broker is done, so we can get started with the GraphQL subscription as below:
We can now add the subscription method itself:
However, this won't work on its own and we still need to push the messages to the broker. We can publish our mutation updates to the SimpleBroker
by using the SimpleBroker::publish
method after a successful SQL update, like so:
We've finished all of the parts we need to make our GraphQL server fully functional, so it's time to hook it all up!
Connecting it all up
See below for what your main file should look like:
If you use shuttle run
and go to http://localhost:8000
, you'll be able to access all of your queries, mutations and the subscription we created.
Deployment
Once you're done, feel free to deploy by using shuttle deploy
(with --allow-dirty
if on a dirty Git branch). Your app will then be deployed to Shuttle servers along with a provisioned database - nothing more is needed! When finished, you'll be able to view your connection string (if you lose it for whatever reason, you can use cargo-shuttle resource list
to get the connection string again).
If you're looking for something a bit more isolated, we also offer a completely isolated AWS RDS database as a paid add-on. Find out more about our pricing here.
Finishing Up
I hope you enjoyed reading this article about using GraphQL in Rust! It can be a powerful resource for data fetching if you're in a team, but it's important to cover all angles so that we can make the most of it.