Alec Aivazis
PathCreated with Sketch.GroupCreated with Sketch.

Building an Ethereum Dapp With GraphQL

Using GraphQL as a query language for your smart contracts

Even the simplest decentralized applications involve information that is spread across many different sources. On top of that, unless we are very careful, our components can easily become intimately tied with the architecture details of our application. In this blog post, I will show you how to use GraphQL as a powerful abstraction over your contracts that ultimately simplifies the construction and maintenance of your dapp's user interface.

This post is not meant to be an introduction to ethereum, smart contracts, or graphql. That being said, I will try to explain enough that you shouldn't need too much more than a conceptual familiarity with either to follow along. For a good introduction to the basics of decentralized applications, I recommend this article. For an introduction to GraphQL I recommend starting here.

In this post we will build an auction platform like eBay where users can post items for sale. For the sake of brevity, our example will only fire a single query that lists the auctions that are available on our platform. Since we are going to be able to send any kind of query we want to our schema, you can easily plug into your favorite GraphQL framework and work how you want.

Ethereum and GraphQL

When you first heard about GraphQL, it's very likely that it was as a query language for a remote server's API. In the decentralized world, there is no central server like there is in traditional architectures. That does not mean, however, that we cannot take advantage of all of the amazing tooling that the GraphQL community has produced.

By defining a schema that exists only in our client, we can use GraphQL as an abstraction layer over our collection of contracts. This allows us to query the current state of our platform as if it was a traditional server and resolve queries against the blockchain.

Our Contracts

The code for this post lives on GitHub. The platform is made up of two different contracts. One is called AuctionHub which maintains a list of addresses that point to instances of another contract called Auction. That second contract only has a single public field: the name of the item for sale.

Let's begin by deploying some contracts so we have something to query in our UI. Clone the demo project and install the project dependencies. Just a heads up: this might take a bit.

git clone git@github.com:AlecAivazis/ethereum-graphql-demo.git \
    && cd ethereum-graphql-demo \
    && npm i

Once it's done, start a local ethereum blockchain we can test against:

npm run testnet

Notice the list of accounts under Available Accounts. We'll need one of them later. In another terminal, compile the contracts:

npm run build:contracts

This should generate two files for each contract under contracts/*. The *.bin files are the compiled version of each contract and the *.abi are json files that describe the contents of the contract (attributes, methods, etc.).

The demo project comes with a script that will bootstrap a few contracts and print the address of the hub. Before you can run it, you have to update the wallet address that you want to use when creating our test contracts here. Once that is updated, you can create the demo contracts with:

npm run init

If everything went according to plan, you should see the address of the hub that we just deployed. Take note of this address. We'll need it later when defining our schema. With our contracts deployed, we are now ready to start querying them with GraphQL.

Building Our Client

The repo already contains a very basic UI that that we can just use without worrying too much about how it works. Just know that if you update the queries that your components are firing, or the graphql schema, you have to re-run the compiler with npm run relay for the changes to take effect.

Defining the Schema

The first step to building our client-side graphql layer is the same as in the traditional world: define a schema that represents our domain. This representation should apply regardless of the persistence and execution details of your product. Whereas before the schema acted as an agreement between the server and client, in our decentralized world, this schema encodes the internal API between our UI and contracts. It can even act as the handoff between separate teams responsible for blockchain and UI development (if that separation makes sense for you).

Let's see how this works. Start by adding a file named schema.js under the src/ directory with this content:

import { makeExecutableSchema } from 'graphql-tools'

const schema = `
    type Auction {
        itemName: String!
    }

    type Query {
        allAuctions: [Auction!]!
    }
`

export default makeExecutableSchema({
    typeDefs: schema,
})

If you have experience with the graphql-tools package then this should look familiar. What we've done here is define a GraphQL schema that lets us query for all of the auctions in our system.

Specifying Resolvers

On its own, a schema is not enough to resolve queries. We also need to tell the runtime how to resolve the requested fields. This is done by defining an object whose keys are the type name and whose values are another object with resolvers for each field:

import { makeExecutableSchema } from 'graphql-tools'
import { HubABI, AuctionABI, web3 } from '../contracts'

const schema = ...

const resolvers = {
    Auction: {
        // auction is an instance of the Auction contract wrapper
        itemName: auction => auction.methods.itemName().call(),
    },
    Query: {
        allAuctions: async () => {
            // create a reference to the hub contract we created earlier
            const hub = new web3.eth.Contract(HubABI, 'hub address from before')

            // build the list of auctions
            const auctions = []
            for (let i = 0; i < await hub.methods.auctionCount().call(); i++) {
                auctions.push(
                    new web3.eth.Contract(
                        AuctionABI,
                        await hub.methods.auctions(i).call()
                    )
                )
            }

            return auctions
        }
    },
}

export default makeExecutableSchema({
    typeDefs: schema,
    resolvers,
})

As you can see, we've told the runtime that resolving the itemName field on the Auction object type requires invoking the itemName method on the instance of the auction contract. This returns a promise with the the value of a constant method which will give us the string we want. Similarly, we resolve the list of all available auctions by looking at the auction hub we created earlier and returning an instance of the auction contract for each address stored in the hub.

By doing this, we have completely abstracted away the existence of the hub from the user interface. If we do in fact have separate teams working on the UI and the blockchain infrastructure, the UI team doesn't have to understand how to find each auction. All they have to do is fire a query like { allAuctions { itemName } }.

Querying Our Schema

With our schema in place, we are now ready to start wiring up the user interface and our contracts. In Apollo (the graphql framework used by the demo project), you can provide something called a "link" which is responsible for handling the queries. If you open up src/ client.js in the the demo project, you'll see that it already contains a lot of the boilerplate necessary to define a custom link. There is, however, a core piece missing - the actual logic to resolve the query. Let's add that now:

import { ApolloLink } from 'apollo-link'
import { Observable } from 'apollo-link-core'
import { graphql } from 'graphql'
import { print } from 'graphql/language/printer'
import schema from './schema'

const blockChainLink = new ApolloLink(
    operation =>
        new Observable(observer => {
            graphql(schema, print(operation.query), null, null, operation.variables).then(
                result => {
                    observer.next(result)
                    observer.complete()
                }
            )
        })
)

// ... everything else...

As you can see, resolving our queries does not involve firing a network request as it would in the centralized case. Instead, we just pass the query string and variables to the graphql function which uses the resolvers that we defined earlier to handle queries.

With those two files in place, we should now be able to run our client and see a list of the auctions in our platform. Start the development server with:

npm run relay && npm run dev

and navigate to http://localhost:8000. If everything goes right, you should now see a list of items - one for the three auctions we created in the init script.

Why is this Better?

By now you might be asking yourself if going through all this effort was worth it. If you have experience working with web3 then you know that the imperative API can become extremely duplicative without some kind of domain specific abstraction layer. Take for example the code necessary to grab a list of every auction in our platform (copied from above):

// create a reference to the hub contract we created earlier
const hub = new web3.eth.Contract(HubABI, 'hub address from earlier')

// build the list of auctions
const auctions = []
for (let i = 0; i < (await hub.methods.auctionCount().call()); i++) {
    auctions.push(new web3.eth.Contract(AuctionABI, await hub.methods.auctions(i).call()))
}

return auctions

There are a lot of questions to answer when trying to figure out the best way to abstract this logic into something reusable. Some parts of this are specific to our Auction/Hub breakdown. Other parts are boilerplate that you will find any time you want to build up a list of something. Once we do have it broken up how do we wrap up the logic in a way that makes our UI easy to consume? While there is no silver bullet to all of these questions, each solution has its pros and cons. By using GraphQL as a data-layer for your smart contracts, you can provide a robust integration for your UI components that abstracts the details of the retrieval logic behind a declarative API. This does, however, come at the cost of an extra logical layer in your application.

Once you have gotten over effort of encapsulating your contracts in a single schema, you can take advantange of all of the awesome tooling that the graphql community has produced. On top of that, this model also enables an extremely smooth story for a semi-decentralized approach where information comes from sources both on and off the chain. We can stitch our schema with remote services and build a unified abstraction that lets us build our interfaces without worrying about where the information is coming from. I have used a similar approach in one of my dapps to integrate with a popular oauth provider.

Conclusion and Next Steps

In this post, I showed you how to build a client-side GraphQL schema that resolves against smart contracts running on an ethereum blockchain. We then wired the schema to our user interface so we could easily query the state of our contracts. This provided a nice abstraction for our UI to query our contracts without worrying about how to retrieve the requested data.

To keep things focused, we only went over how to resolve a single query against the blockchain. However, there is a lot of things that can be added on top to make the developer experience and your application plus ultra. Some of those include:

That's it for now - thanks for reading! I was originally going to start a multi-post series that covers a few of the extras I've mentioned but wasn't sure if there was interest. If you would like to see something like that, please let me know on this blog's repo.

If you think this is a terrible idea, if you liked it, if you took a stab at the next steps, or you did something completely different, I want to know! Please reach out on twitter with your thoughts.

As always, if this is where we part ways, I hope you have a nice rest of your day.