Building a Scalable Supergraph GraphQL API with Rust and MongoDB
In this article, I’m excited to share a journey into building a scalable and efficient GraphQL API using Rust and MongoDB. We’ll walk through setting up a Rust project, integrating it with MongoDB, and deploying a GraphQL API that can handle complex data queries with ease. Whether you’re a seasoned Rustacean or just getting started, this guide will provide valuable insights into leveraging Rust’s performance and MongoDB’s flexibility.
Why Rust and MongoDB?
Rust is renowned for its performance, memory safety, and concurrency capabilities, making it an excellent choice for building high-performance APIs. MongoDB, with its flexible document model, allows you to store and query data in a way that aligns naturally with many application data needs. Combining these technologies provides a powerful platform for building modern, scalable web services.
What We’ll Cover
Setting Up the Project: Initializing a Rust project and installing necessary dependencies.
Connecting to MongoDB: Setting up a connection to MongoDB using the
mongodbcrate.Creating a Modular GraphQL Schema: Organizing the API into subgraphs to maintain clean and scalable code.
Running the Server: Deploying the API using Actix, a fast and lightweight web framework for Rust.
Testing the API: Using GraphQL Playground to test queries and mutations against the MongoDB backend.
Before you start, you can also find the code example of this project HERE
Step 1: Setting Up the Project
Let’s start by creating a new Rust project and adding the required dependencies.
1. Create a New Rust Project
First, create a new Rust project using Cargo:
cargo new mongodb_graphql_example
cd mongodb_graphql_example
2. Add Dependencies
Next, open the Cargo.toml file and add the following dependencies:
[dependencies]
async-graphql = "5.0"
async-graphql-actix-web = "5.0"
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
mongodb = "2.3.0"
async-graphql: A GraphQL server library for Rust.actix-web: A powerful, lightweight, and fast web framework.tokio: An asynchronous runtime for Rust, enabling async programming.mongodb: The official MongoDB driver for Rust, providing a simple and efficient way to interact with MongoDB.
Step 2: Connecting to MongoDB
With the project set up, our next task is to connect to MongoDB. We’ll do this by creating a db.rs file that handles the database connection.
src/db.rs
use mongodb::{Client, Database};
use std::sync::Arc;
use tokio::sync::Mutex;
pub type MongoDb = Arc<Mutex<Database>>;
pub async fn connect_to_mongodb(uri: &str, db_name: &str) -> MongoDb {
let client = Client::with_uri_str(uri).await.expect("Failed to initialize MongoDB client");
let db = client.database(db_name);
Arc::new(Mutex::new(db))
}
This function establishes a connection to a MongoDB database. The connection parameters, including the URI and database name, are passed in as arguments. We use Arc<Mutex<T>> to allow safe shared access to the MongoDB database connection across asynchronous tasks.
Step 3: Creating a Modular GraphQL Schema
We’ll organize our GraphQL schema into subgraphs, which will make the code easier to maintain and scale as the project grows.
1. Subgraph for Users
Let’s start by defining a subgraph for handling user-related queries.
src/schema/subgraph1.rs
use async_graphql::{Context, Object, Result, SimpleObject};
use crate::db::MongoDb;
use mongodb::bson::doc;
#[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 db = ctx.data::<MongoDb>()?.lock().await;
let collection: mongodb::Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "id": &id };
let user_doc = collection.find_one(filter, None).await?;
if let Some(user_doc) = user_doc {
let user = User {
id: user_doc.get_str("id").unwrap().to_string(),
name: user_doc.get_str("name").unwrap().to_string(),
email: user_doc.get_str("email").unwrap().to_string(),
};
Ok(user)
} else {
Err("User not found".into())
}
}
}
This subgraph defines the User type and a query to fetch a user by their ID from MongoDB.
2. Subgraph for Products
Next, we’ll define a subgraph for handling product-related queries.
src/schema/subgraph2.rs
use async_graphql::{Context, Object, Result, SimpleObject};
use crate::db::MongoDb;
use mongodb::bson::doc;
#[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 db = ctx.data::<MongoDb>()?.lock().await;
let collection: mongodb::Collection<mongodb::bson::Document> = db.collection("products");
let filter = doc! { "id": &id };
let product_doc = collection.find_one(filter, None).await?;
if let Some(product_doc) = product_doc {
let product = Product {
id: product_doc.get_str("id").unwrap().to_string(),
name: product_doc.get_str("name").unwrap().to_string(),
price: product_doc.get_f64("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 from MongoDB.
3. Combine Subgraphs into a Main Schema
Finally, we’ll combine these subgraphs into a single GraphQL 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
With the schema and MongoDB connection in place, we’ll set up an 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_mongodb;
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 mongodb = connect_to_mongodb("mongodb://localhost:27017", "mydatabase").await;
let schema = schema::create_schema()
.data(mongodb)
.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
With the server running, it’s time to test our API using the GraphQL Playground.
1. Start the Server
Run the server using:
cargo run
2. Access the Graph
QL Playground
Open your browser and navigate to http://127.0.0.1:8080/playground. This interface allows you to test your GraphQL API interactively.
3. Example Queries
Here are some example queries to test the API:
Fetching a User:
{
user(id: "1") {
id
name
email
}
}
Fetching a Product:
{
product(id: "1") {
id
name
price
}
}
Step 6: Inserting Data into MongoDB
If your MongoDB database is empty, you can use the MongoDB shell or a client to insert test data.
Example commands:
use mydatabase;
db.users.insertOne({ id: "1", name: "Alice", email: "alice@example.com" });
db.products.insertOne({ id: "1", name: "Laptop", price: 999.99 });
Conclusion
In this article, we’ve successfully built a scalable GraphQL API in Rust, with MongoDB as the backend database. This setup is powerful for modern web applications that require both performance and flexibility.
Key Takeaways:
Rust and MongoDB: The combination of Rust’s performance and MongoDB’s flexible document model provides a robust foundation for building scalable APIs.
Modular Design: Organizing the GraphQL schema into subgraphs makes the 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 additional features like authentication. If you found this guide helpful, consider subscribing for more content like this, and feel free to share your thoughts or questions in the comments below!
You can find my full code example HERE
Happy coding! 🎉
This Substack article serves as a comprehensive guide for anyone interested in building a GraphQL API with Rust and MongoDB. It provides clear instructions and explanations, making it accessible even to those new to these technologies.


