GraphQL has become a new alternative for developing APIs other than the common REST approach. Being more lightweight by selectively requesting fields (querying) instead of transmitting unnecessary data just makes sense. With these and other benefits there also are disadvantages like the knowledge it requires to maintain, develop or offer such an API (not every customer wants GraphQL).
What you could do is to add GraphQL to your existing REST API, basically as a thin layer on top of it. This gives you the benefit of being able to use both. Of course, there is also the cost of maintaining both.
I decided to create a GraphQL server for the open source PokeAPI. For that, I've used Rust, with the Juniper GraphQL library, with Actix Web as a web server implementation. Serialization of objects has been done with Serde. The whole thing is a wrapper on top of the pokerust REST client. Lastly, I've built a small web interface with Svelte in form of a PokeDex which displays some basic Pokemon information.
In GraphQL you have a single endpoint or URL that the client is communicating with. You can enable clients to authenticate, query (Read), mutate (Create, Update, Delete) data and also listen or subscribe to data changes. When the GraphQL server receives a query, it needs to resolve all data that is required. This can happen through different data sources, like a data base or REST api.
From an HTTP layer perspective, GraphQL is usually based on POST and not GET. Reason for that is mostly the limited amount of data that you can send (query) with GET. This also comes with the challenge of no client side caching. You may read more about it in this article.
GraphQL libraries often provide a playground, which is a tool to explore the API with. That is a pretty much self documented way to develop an API, similar to what you might know from the OpenAPI specification. You can explore the whole schema with it and test or execute queries. You can find an example here.
I chose Rust as a programming language because it aims to deliver high performance and an effective or safe way of memory management. The rough idea is that memory that you use or allocate is managed or freed after specific conditions. These conditions are defined statically on the language level, so that as long as it compiles, memory will usually be safely handled.
One of the best choices for Rust web server libraries currently is Actix Web. It provides ways to setup things like CORS and all necessary routes to the GraphQL endpoint. The main endpoint is defined via POST on
/graphql and the playground via GET on
We can also take use of environment variables to change port and ip of the server without recompiling. This is where Dotenv can be helpful.
There are different ways of creating the GraphQL schema. Either schema first, where you directly define the schema or code first, where your schema is generated based on the code that you have written. Juniper uses the code first schema approach, but there is an alternative schema-to-juniper generator.
This function translates to the query named
pokemonByName. However, Juniper provides a way to adjust the final name. This can be useful for example if an object has the field
move, which is a keyword in the Rust language. As you see in the query, we only want the
height of the queried pokemon.
Traits in Rust are pretty much its way of an interface, so you can specify a set of shared behavior for types with it. Since we are using the pokerust library, we receive objects (structs) that are not fully compatible with Juniper. For example, some data types were
i16 or similar, which are not natural to GraphQL. Basic data types in GraphQL are called Scalars.
You may create custom scalars for GraphQL with Juniper, but I have decided to just convert them to
String. This is where the object conversion trait
From<T> is useful. It allows to implement a type conversion so that you can do the following:
let pokemon = GraphedPokemon::from(pokemon_to_convert).
Code optimization is done at compile time. Also, state management is much simpler, by being reactive itself. Some downsides could be, as with every framework, that is less adopted, less popular and you may find less developers to maintain your code.
Showing Svelte code here would be a bit too much, but the syntax is quite easy to understand.
I have used Github Actions as a simple CI/CD. The Rust app is deployed to Heroku via Docker. I have added the
pokeapi repository as a git submodule and deployed the Python app in the Docker container, too. On start of the container, both server are started and GraphQL server uses the local REST API. For Heroku, you have to make sure that your server's port can be changed via the
PORT environment variable.
The technology stack of Juniper and Actix to build a GraphQL server is probably the best way to do it in Rust right now, even though Juniper is still pretty much in development where API changes are common. In practice, you would usually have a database, where Diesel is used. The way of creating a GraphQL schema via code is also not always the preferred way. Svelte is interesting to work with, had pretty much no dependencies to install and compiled fast.
Originally published at https://mariusreimer.com on February 8, 2021.