GraphQL szerver beállítása Rust, Juniper, Diesel és Actix használatával; megismerkedhetünk Rust webes keretrendszereivel és az erőteljes makrókkal.
Forráskód: github.com/iwilsonq/rust-graphql-example
Az alkalmazások kiszolgálása a GraphQL-en keresztül gyorsan a legegyszerűbb és leghatékonyabb módja az adatok ügyfeleknek történő eljuttatásának. Akár mobil eszközön, akár böngészőben van, az biztosítja a kért adatokat, és semmi mást.
Az ügyfélalkalmazásoknak már nem kell különálló adatforrásokból összeállítani az információkat. A GraphQL szerverek felelősek az integrációért, így nincs szükség többletadatokra és oda-vissza igénylésre.
Természetesen ez azt jelenti, hogy a szervernek különféle forrásokból, például otthoni tulajdonú háttérszolgáltatásokból, adatbázisokból vagy harmadik felek API-ból származó összesített adatokat kell kezelnie. Ez erőforrásigényes lehet, hogyan optimalizálhatjuk a CPU idejére?
A rozsda egy nyelv csodája, amely párosítja a C-hez hasonló alacsony szintű nyelv nyers teljesítményét a modern nyelvek kifejezõképességével. Hangsúlyozza a típus és a memória biztonságát, különösen akkor, ha potenciálisan adatversenyek vannak egyidejű műveletek során.
Lássuk, mi jár a GraphQL szerver Rust-tal való felépítésében. Tanulni fogunk
- Juniper GraphQL Server
- A Juniperrel integrált Actix webkeret
- Diesel SQL adatbázis lekérdezéséhez
- Hasznos Rust makrók és származtatott tulajdonságok a könyvtárak használatához
Ne feledje, hogy nem részletezem a Rust vagy a Cargo telepítését. Ez a cikk néhány előzetes ismeretet feltételez a Rust eszközláncról.
HTTP szerver beállítása
A kezdéshez inicializálnunk kell a projektünket, cargo
majd telepítenünk kell a függőségeket.
cargo new rust-graphql-example cd rust-graphql-example
Az inicializáló parancs elindítja a Cargo.toml fájlt, amely tartalmazza a projektfüggőségeinket, valamint a main.rs fájlt, amely egy egyszerű "Hello World" példát tartalmaz.
// main.rs fn main() { println!("Hello, world!"); }
Egészségügyi ellenőrzésként nyugodtan fusson cargo run
a program végrehajtása érdekében.
A szükséges könyvtárak Rustba történő telepítése azt jelenti, hogy hozzáad egy sort, amely tartalmazza a könyvtár nevét és verziószámát. Frissítsük a Cargo.toml függőségi szakaszait így:
# Cargo.toml [dependencies] actix-web = "1.0.0" diesel = { version = "1.0.0", features = ["postgres"] } dotenv = "0.9.0" env_logger = "0.6" futures = "0.1" juniper = "0.13.1" serde = "1.0" serde_derive = "1.0" serde_json = "1.0"
Ez a cikk egy GraphQL kiszolgáló megvalósításáról szól, amely Juniper-t használja GraphQL könyvtárként, és az Actix-et, mint mögöttes HTTP-kiszolgálót. Az Actix nagyon szép API-val rendelkezik, és jól működik a Rust stabil verziójával.
Ezeknek a soroknak a hozzáadásakor a projekt legközelebbi fordításakor a könyvtárakat is tartalmazza. Mielőtt lefordítanánk, lehetővé teszi a main.rs frissítését egy alap HTTP szerverrel, kezelve az index útvonalát.
// main.rs use std::io; use actix_web::{web, App, HttpResponse, HttpServer, Responder}; fn main() -> io::Result { HttpServer::new(|| { App::new() .route("/", web::get().to(index)) }) .bind("localhost:8080")? .run() } fn index() -> impl Responder { HttpResponse::Ok().body("Hello world!") }
A tetején található első két sor a szükséges modul hatókörébe hozza. A fő függvény itt egy io::Result
típust ad vissza , amely lehetővé teszi számunkra, hogy a kérdőjelet rövidítésként használjuk az eredmények kezeléséhez.
A kiszolgáló útválasztása és konfigurálása a példányban App
jön létre, amely a HTTP szerver konstruktora által biztosított lezárásban jön létre.
Magát az útvonalat az index függvény kezeli, amelynek neve tetszőleges. Amíg ez a funkció megfelelően megvalósul, Responder
addig a GET kérés paramétereként használható az index útvonalán.
Ha REST API-t írnánk, folytathatnánk további útvonalak és kapcsolódó kezelők hozzáadását. Hamarosan látni fogjuk, hogy az útvonalkezelők listáját kereskedjük tárgyakkal és azok kapcsolataival kapcsolatban.
Most bemutatjuk a GraphQL könyvtárat.
A GraphQL séma létrehozása
Minden GraphQL séma gyökerében egy gyökér lekérdezés található. Ebből a gyökérből lekérdezhetjük az objektumok listáját, adott objektumokat és bármilyen mezőt tartalmazhatnak.
Nevezzük ezt QueryRoot-nak, és azonos kóddal jelöljük a kódunkban. Mivel nem fogunk adatbázist vagy harmadik féltől származó API-kat létrehozni, ezért az itt található kevés adatot keményen kódoljuk.
Egy kis szín hozzáadásához ehhez a példához a séma egy általános taglistát fog ábrázolni.
Az src alatt adjon hozzá egy új, graphql_schema.rs nevű fájlt a következő tartalommal együtt:
// graphql_schema.rs use juniper::{EmptyMutation, RootNode}; struct Member { id: i32, name: String, } #[juniper::object(description = "A member of a team")] impl Member { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } } pub struct QueryRoot; #[juniper::object] impl QueryRoot { fn members() -> Vec { vec![ Member { id: 1, name: "Link".to_owned(), }, Member { id: 2, name: "Mario".to_owned(), } ] } }
Az importálás mellett meghatározzuk a projekt első GraphQL objektumát, a tagot. Egyszerű lények, azonosítóval és névvel. A későbbiekben bonyolultabb területeken és mintákon gondolkodunk.
Miután elnyomta a QueryRoot
típust mint egységszerkezetet, meg kell határoznunk magát a mezőt. Juniper kitesz egy Rust makrót, object
amely lehetővé teszi számunkra, hogy mezőket definiáljunk a sémánk különböző csomópontjain. Egyelőre csak a QueryRoot csomópont áll rendelkezésünkre, így egy tagoknak nevezett mezőt teszünk ki rajta.
A rozsda makrók gyakran szokatlan szintaxissal rendelkeznek a szokásos funkciókhoz képest. Nem csupán néhány érvet vesznek fel és eredményt hoznak, hanem fordításkor sokkal összetettebb kódokká bővülnek.
A séma bemutatása
A tagok mező létrehozására szolgáló makrohívásunk alatt meghatározzuk a RootNode
sémánknak kitett típust.
// graphql_schema.rs pub type Schema = RootNode<'static, QueryRoot, EmptyMutation>; pub fn create_schema() -> Schema { Schema::new(QueryRoot {}, EmptyMutation::new()) }
A Rust erős tipizálása miatt kénytelenek vagyunk megadni a mutációs objektum argumentumot. A boróka EmptyMutation
éppen erre az alkalomra tár fel egy struktúrát, vagyis amikor csak olvasható sémát akarunk létrehozni.
Most, hogy elkészült a séma, frissíthetjük a main.rs szerverünket a "/ graphql" útvonal kezelésére. Mivel a játszótér birtoklása is jó, hozzáadunk egy útvonalat a GraphiQL-hez, az interaktív GraphQL játszótérhez.
// main.rs #[macro_use] extern crate juniper; use std::io; use std::sync::Arc; use actix_web::{web, App, Error, HttpResponse, HttpServer}; use futures::future::Future; use juniper::http::graphiql::graphiql_source; use juniper::http::GraphQLRequest; mod graphql_schema; use crate::schema::{create_schema, Schema}; fn main() -> io::Result { let schema = std::sync::Arc::new(create_schema()); HttpServer::new(move || { App::new() .data(schema.clone()) .service(web::resource("/graphql").route(web::post().to_async(graphql))) .service(web::resource("/graphiql").route(web::get().to(graphiql))) }) .bind("localhost:8080")? .run() }
You'll notice I've specified a number of imports that we will be using, including the schema we've just created. Also see that:
- we call
create_schema
inside an Arc (atomically reference counted), to allow shared immutable state across threads (cooking with ? here I know) - we mark the closure inside HttpServer::new with move, indicating that the closure takes ownership of the inner variables, that is, it gains a copy of
schema
schema
is passed to thedata
method indicating that it is to be used inside the application as shared state between the two services
We must now implement the handlers for those two services. Starting with the "/graphql" route:
// main.rs // fn main() ... fn graphql( st: web::Data
, data: web::Json, ) -> impl Future { web::block(move || { let res = data.execute(&st, &()); Ok::(serde_json::to_string(&res)?) }) .map_err(Error::from) .and_then(|user| { Ok(HttpResponse::Ok() .content_type("application/json") .body(user)) }) }
Our implementation of the "/graphql" route takes executes a GraphQL request against our schema from application state. It does this by creating a future from web::block
and chaining handlers for success and error states.
Futures are analogous to Promises in JavaScript, which is enough to understand this code snippet. For a greater explanation of Futures in Rust, I recommend this article by Joe Jackson.
In order to test out our GraphQL schema, we'll also add a handler for "/graphiql".
// main.rs // fn graphql() ... fn graphiql() -> HttpResponse { let html = graphiql_source("//localhost:8080/graphql"); HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html) }
This handler is much simpler, it merely returns the html of the GraphiQL interactive playground. We only need to specify which path is serving our GraphQL schema, which is "/graphql" in this case.
With cargo run
and navigation to //localhost:8080/graphiql, we can try out the field we configured.

It does seem to take a little more effort than setting up a GraphQL server with Node.js and Apollo but the static typing of Rust combined with its incredible performance makes it a worthy trade — if you're willing to work at it.
Setting up Postgres for Real Data
If I stopped here, I wouldn't even be doing the examples in the docs much justice. A static list of two members that I wrote myself at dev time will not fly in this publication.
Installing Postgres and setting up your own database belongs in a different article, but I'll walk through how to install diesel, the popular Rust library for handling SQL databases.
See here to install Postgres locally on your machine. You can also use a different database like MySQL in case you are more familiar with it.
The diesel CLI will walk us through initializing our tables. Let's install it:
cargo install diesel_cli --no-default-features --features postgres
After that, we will add a connection URL to a .env file in our working directory:
echo DATABASE_URL=postgres://localhost/rust_graphql_example > .env
Once that's there, you can run:
diesel setup # followed by diesel migration generate create_members
Now you'll have a migrations folder in your directory. Within it, you'll have two SQL files: one up.sql for setting up your database, the other down.sql for tearing it down.
I will add the following to up.sql:
CREATE TABLE teams ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL ); CREATE TABLE members ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, knockouts INT NOT NULL DEFAULT 0, team_id INT NOT NULL, FOREIGN KEY (team_id) REFERENCES teams(id) ); INSERT INTO teams(id, name) VALUES (1, 'Heroes'); INSERT INTO members(name, knockouts, team_id) VALUES ('Link', 14, 1); INSERT INTO members(name, knockouts, team_id) VALUES ('Mario', 11, 1); INSERT INTO members(name, knockouts, team_id) VALUES ('Kirby', 8, 1); INSERT INTO teams(id, name) VALUES (2, 'Villains'); INSERT INTO members(name, knockouts, team_id) VALUES ('Ganondorf', 8, 2); INSERT INTO members(name, knockouts, team_id) VALUES ('Bowser', 11, 2); INSERT INTO members(name, knockouts, team_id) VALUES ('Mewtwo', 12, 2);
And into down.sql I will add:
DROP TABLE members; DROP TABLE teams;
If you've written SQL in the past, these statements will make some sense. We are creating two tables, one to store teams and one to store members of those teams.
I am modeling this data based on Smash Bros if you have not yet noticed. It helps the learning stick.
Now to run the migrations:
diesel migration run
If you'd like to verify that the down.sql script works to destroy those tables, run: diesel migration redo
.
Now the reason why I named the GraphQL schema file graphql_schema.rs instead of schema.rs, is because diesel overwrites that file in our src direction by default.
It keeps a Rust macro representation of our SQL tables in that file. It is not so important to know how exactly this table!
macro works, but try not to edit this file — the ordering of the fields matters!
// schema.rs (Generated by diesel cli) table! { members (id) { id -> Int4, name -> Varchar, knockouts -> Int4, team_id -> Int4, } } table! { teams (id) { id -> Int4, name -> Varchar, } } joinable!(members -> teams (team_id)); allow_tables_to_appear_in_same_query!( members, teams, );
Wiring up our Handlers with Diesel
In order to serve the data in our tables, we must first update our Member
struct with the new fields:
// graphql_schema.rs + #[derive(Queryable)] pub struct Member { pub id: i32, pub name: String, + pub knockouts: i32, + pub team_id: i32, } #[juniper::object(description = "A member of a team")] impl Member { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } + pub fn knockouts(&self) -> i32 { + self.knockouts + } + pub fn team_id(&self) -> i32 { + self.team_id + } }
Note that we are also adding the Queryable
derived attribute to Member
. This tells Diesel everything it needs to know in order to query the right table in Postgres.
Additionally, add a Team
struct:
// graphql_schema.rs #[derive(Queryable)] pub struct Team { pub id: i32, pub name: String, } #[juniper::object(description = "A team of members")] impl Team { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } pub fn members(&self) -> Vec { vec![] } }
In a short while, we will update the members
function on Team
to return a database query. But first, let us add a root call for members.
// graphql_schema.rs + extern crate dotenv; + use std::env; + use diesel::pg::PgConnection; + use diesel::prelude::*; + use dotenv::dotenv; use juniper::{EmptyMutation, RootNode}; + use crate::schema::members; pub struct QueryRoot; + fn establish_connection() -> PgConnection { + dotenv().ok(); + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url)) + } #[juniper::object] impl QueryRoot { fn members() -> Vec { - vec![ - Member { - id: 1, - name: "Link".to_owned(), - }, - Member { - id: 2, - name: "Mario".to_owned(), - } - ] + use crate::schema::members::dsl::*; + let connection = establish_connection(); + members + .limit(100) + .load::(&connection) + .expect("Error loading members") } }
Very good, we have our first usage of a diesel query. After initializing a connection, we use the members dsl, which is generated from our table!
macros in schema.rs, and call load, indicating that we wish to load Member
objects.
Establishing a connection means connecting to our local Postgres database by using the env variable we declared earlier.
Assuming that was all input correctly, restart the server with cargo run
, open GraphiQL and issue the members query, perhaps adding the two new fields.
The teams query will be very similar — the difference being we must also add a part of the query to the members function on the Team
struct in order to resolve the relationship between GraphQL types.
// graphql_schema.rs #[juniper::object] impl QueryRoot { fn members() -> Vec { use crate::schema::members::dsl::*; let connection = establish_connection(); members .limit(100) .load::(&connection) .expect("Error loading members") } + fn teams() -> Vec { + use crate::schema::teams::dsl::*; + let connection = establish_connection(); + teams + .limit(10) + .load::(&connection) + .expect("Error loading teams") + } } // ... #[juniper::object(description = "A team of members")] impl Team { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } pub fn members(&self) -> Vec { - vec![] + use crate::schema::members::dsl::*; + let connection = establish_connection(); + members + .filter(team_id.eq(self.id)) + .limit(100) + .load::(&connection) + .expect("Error loading members") } }
When running this is GraphiQL, we get:

I really like the way this is turning out, but there is one more thing we must add in order to call this tutorial complete.
The Create Member Mutation
What good is a server if it is read-only and not writable? Well I'm sure those have their uses too, but we would like to write data to our database, how hard can it be?
First we'll create a MutationRoot
struct that will eventually replace our usage of EmptyMutation
. Then we will add the diesel insertion query.
// graphql_schema.rs // ... pub struct MutationRoot; #[juniper::object] impl MutationRoot { fn create_member(data: NewMember) -> Member { let connection = establish_connection(); diesel::insert_into(members::table) .values(&data) .get_result(&connection) .expect("Error saving new post") } } #[derive(juniper::GraphQLInputObject, Insertable)] #[table_name = "members"] pub struct NewMember { pub name: String, pub knockouts: i32, pub team_id: i32, }
As GraphQL mutations typically go, we define an input object called NewMember
and make it the argument of the create_member
function. Inside this function, we establish a connection and call the insert query on the members table, passing the entire input object.
It is super convenient that Rust allows us to use the same structs for GraphQL input objects as well as Diesel insertable objects.
Let me make this a little more clear, for the NewMember
struct:
- we derive
juniper::GraphQLInputObject
in order to create a input object for our GraphQL schema - we derive
Insertable
in order to let Diesel know that this struct is valid input for an insertion SQL statement - we add the
table_name
attribute so that Diesel knows which table to insert it in
There is a lot of magic going on here. This is what I love about Rust, it has great performance but the code has features like macros and derived traits to abstract away boilerplate and add functionality.
Finally, at the bottom of the file, add the MutationRoot
to the schema:
// graphql_schema.rs pub type Schema = RootNode; pub fn create_schema() -> Schema { Schema::new(QueryRoot {}, MutationRoot {}) }
I hope that everything is there, we can test out all of our queries and mutations thus far now:
# GraphiQL mutation CreateMemberMutation($data: NewMember!) { createMember(data: $data) { id name knockouts teamId } } # example query variables # { # "data": { # "name": "Samus", # "knockouts": 19, # "teamId": 1 # } # }
If that mutation ran successfully, you can pop open a bottle of champagne as you are on your way to building performant and type-safe GraphQL Servers with Rust.
Thanks For Reading
I hope you have enjoyed this article, I also hope that it gave you some sort of inspiration for your own work.
If you'd like to keep up with the next time I drop an article in the realm of Rust, ReasonML, GraphQL, or software development at large, feel free to give me a follow on Twitter, dev.to, or on my website at ianwilson.io.
The source code is here github.com/iwilsonq/rust-graphql-example.
Other Neat Reading Material
Here are some of the libraries we worked with here. They have great documentation and guides as well so be sure to give them a read :)
- Implementation of Rust Futures in Tokio
- Juniper - GraphQL Server for Rust
- Diesel - Safe, Extensible ORM and Query Builder for Rust
- Actix - Rust's powerful actor system and most fun web framework