Building a Supergraph GraphQL API with Rust and Neo4j: A Step-by-Step Guide
In this article, I'll walk you through creating a powerful and modular GraphQL API using Rust and Neo4j. We'll explore how to set up a GraphQL server, connect it to a Neo4j database, and organize our code for scalability and maintainability.
Why Rust and Neo4j?
Rust is known for its performance and safety, making it an excellent choice for building high-performance web applications. Neo4j, on the other hand, is a powerful graph database that excels in managing and querying relationships. Combining these two technologies allows us to build a fast and efficient API that's perfect for relationship-heavy data models.
What We'll Cover:
- Setting Up the Project: Initializing a Rust project and installing the necessary dependencies.
- Connecting to Neo4j: Establishing a connection to a Neo4j database using the `neo4rs` crate.
- Creating the GraphQL Schema: Defining a modular GraphQL schema using subgraphs.
- Running the Server: Deploying the API using Actix, a powerful and fast web framework for Rust.
- Testing the API: Using GraphQL Playground to test queries and mutations.
Before we get started you can find my full code HERE
Step 1: Setting Up the Project
Let's start by creating a new Rust project and adding the dependencies we'll need.
1. Create a New Rust Project
First, create a new Rust project using Cargo:
bash
cargo new neo4j_graphql_example
cd neo4j_graphql_example
2. Add Dependencies
Open `Cargo.toml` and add the following dependencies:
toml
[dependencies]
async-graphql = "5.0"
async-graphql-actix-web = "5.0"
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
neo4rs = "0.8.0"
tokio-stream = "0.1"
- `async-graphql`: A GraphQL server library for Rust.
- `actix-web`: A powerful, lightweight, and fast web framework.
- `tokio`: An asynchronous runtime for Rust, used for async programming.
- `neo4rs`: A Neo4j driver for Rust, allowing us to interact with the database.
- `tokio-stream`: Utilities for working with asynchronous streams.
Step 2: Connecting to Neo4j
Next, we need to set up a connection to Neo4j. We'll do this by creating a `db.rs` file that handles the connection.
in`src/db.rs`:
use neo4rs::*;
use std::sync::Arc;
use tokio::sync::Mutex;
pub type Neo4jGraph = Arc<Mutex<Graph>>;
pub async fn connect_to_neo4j(uri: &str, user: &str, password: &str) -> Neo4jGraph {
let config = ConfigBuilder::default()
.uri(uri)
.user(user)
.password(password)
.db("neo4j")
.fetch_size(500)
.max_connections(10)
.build()
.unwrap();
let graph = Graph::connect(config).await.unwrap();
Arc::new(Mutex::new(graph))
}This function sets up a connection to a Neo4j database using the `neo4rs` crate. The connection parameters, including the URI, username, and password, are passed in as arguments.
Step 3: Creating the GraphQL Schema
We'll organize our GraphQL schema into subgraphs. This modular approach makes it easier to manage and scale our API.
1. Subgraph for Users
`src/schema/subgraph1.rs`
use async_graphql::{Context, Object, Result, SimpleObject};
use crate::db::Neo4jGraph;
use neo4rs::query;
use tokio_stream::StreamExt;
#[derive(SimpleObject)]
pub struct User {
id: String,
name: String,
email: String,
}
#[derive(Default)]
pub struct Subgraph1Query;
#[Object]
impl Subgraph1Query {
async fn user(&self, ctx: &Context<'_>, id: String) -> Result<User> {
let graph = ctx.data::<Neo4jGraph>()?.lock().await;
let mut result = graph
.execute(query("MATCH (u:User {id: $id}) RETURN u").param("id", id))
.await?;
if let Ok(Some(row)) = result.next().await {
let node = row.get::<neo4rs::Node>("u").unwrap();
let user = User {
id: node.get("id").unwrap(),
name: node.get("name").unwrap(),
email: node.get("email").unwrap(),
};
Ok(user)
} else {
Err("User not found".into())
}
}
}
This subgraph defines the `User` type and a query to fetch a user by their ID.
2. Subgraph for Products
`src/schema/subgraph2.rs`
use async_graphql::{Context, Object, Result, SimpleObject};
use crate::db::Neo4jGraph;
use neo4rs::query;
use tokio_stream::StreamExt;
#[derive(SimpleObject)]
pub struct Product {
id: String,
name: String,
price: f64,
}
#[derive(Default)]
pub struct Subgraph2Query;
#[Object]
impl Subgraph2Query {
async fn product(&self, ctx: &Context<'_>, id: String) -> Result<Product> {
let graph = ctx.data::<Neo4jGraph>()?.lock().await;
let mut result = graph
.execute(query("MATCH (p:Product {id: $id}) RETURN p").param("id", id))
.await?;
if let Ok(Some(row)) = result.next().await {
let node = row.get::<neo4rs::Node>("p").unwrap();
let product = Product {
id: node.get("id").unwrap(),
name: node.get("name").unwrap(),
price: node.get("price").unwrap(),
};
Ok(product)
} else {
Err("Product not found".into())
}
}
}
This subgraph defines the `Product` type and a query to fetch a product by its ID.
3. Combine Subgraphs into a Main Schema
`src/schema/mod.rs`
use async_graphql::{Schema, SchemaBuilder, MergedObject};
use crate::schema::subgraph1::Subgraph1Query;
use crate::schema::subgraph2::Subgraph2Query;
pub mod subgraph1;
pub mod subgraph2;
#[derive(MergedObject, Default)]
pub struct QueryRoot(Subgraph1Query, Subgraph2Query);
pub type AppSchema = Schema<QueryRoot, async_graphql::EmptyMutation, async_graphql::EmptySubscription>;
pub fn create_schema() -> SchemaBuilder<QueryRoot, async_graphql::EmptyMutation, async_graphql::EmptySubscription> {
Schema::build(QueryRoot::default(), async_graphql::EmptyMutation, async_graphql::EmptySubscription)
}
This module combines the subgraphs into a single schema that can be used by the GraphQL API.
Step 4: Running the Server
Finally, we'll set up the Actix web server to serve our GraphQL API.
`src/main.rs`
use actix_web::{web, App, HttpServer};
use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use actix_web::{guard, HttpResponse};
mod schema;
mod db;
use db::connect_to_neo4j;
async fn graphql_handler(schema: web::Data<schema::AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
async fn graphql_playground() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let neo4j_graph = connect_to_neo4j("bolt://localhost:7687", "neo4j", "password").await;
let schema = schema::create_schema()
.data(neo4j_graph)
.finish();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(schema.clone()))
.service(
web::resource("/graphql")
.guard(guard::Post())
.to(graphql_handler),
)
.service(
web::resource("/playground")
.guard(guard::Get())
.to(graphql_playground),
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}This code sets up an Actix web server that serves the GraphQL API at `http://127.0.0.1:8080/graphql` and provides a GraphQL Playground interface at `http://127.0.0.1:8080/playground`.
Step 5: Testing the API
1. Start the Server
Run the server using:
bash console
cargo run
2. Access the GraphQL Playground
Open your browser and navigate to `http://127.0.0.1:8080/playground`. This interface allows you to test your GraphQL API.
3. Example Queries
Fetching a User:
in graphql
{
user(id: "1") {
id
name
email
}
}
Fetching a Product:
in graphql
{
product(id: "1") {
id
name
price
}
}
Conclusion
In this article, we've walked through the process of building a modular and scalable GraphQL API in Rust, backed by Neo4j. This setup provides a robust foundation for any application that requires complex data relationships and high performance.
Key Takeaways:
- Rust and Neo4j: Rust's performance and safety, combined with Neo4j's powerful graph database capabilities, offer an excellent solution for building scalable APIs.
- Modular Design: Organizing your GraphQL schema into subgraphs makes your codebase easier to maintain and scale.
- Actix for Performance: Actix provides a fast and efficient web server, perfect for serving your GraphQL API.
What's Next?
You can extend this project by adding mutations, more complex queries, and even subscriptions. Consider exploring how to handle authentication, authorization, and more sophisticated error handling.
If you found this guide helpful, consider subscribing to get more articles like this directly in your inbox. And if you have any questions or suggestions, feel free to leave a comment below!
Also feel free to check out my full code of this example at my site HERE.
---
Happy coding!🎉


