GraphQL has revolutionized how we think about APIs, offering developers precise control over data fetching and reducing over-fetching issues common with REST APIs. When combined with the lightning-fast startup times and efficiency of WebAssembly, GraphQL becomes even more powerful for serverless applications.
In this blog, we’ll walk through building a complete GraphQL client using Fermyon Wasm Functions that queries GitHub’s GraphQL API to fetch and report repository stargazer information. By the end, you’ll have a globally distributed, fully functional serverless application that demonstrates the power of combining type-safe GraphQL queries with WebAssembly.
What We’re Building
We’ll create a serverless GraphQL client as a Spin application that:
- Uses Rust’s type-safe GraphQL client generation
- Demonstrates secure API token handling in serverless environments
- Queries GitHub’s GraphQL API for repository stargazer counts
- Renders results as a simple HTML page
- Runs on the Fermyon Wasm Functions platform with sub-millisecond cold starts
Prerequisites
Before we dive in, make sure you have:
- Spin CLI installed
- Rust toolchain with
wasm32-wasip1
target - A GitHub personal access token with repository read permissions
- Basic familiarity with GraphQL and Rust
- Access to either Fermyon Wasm Functions platform (used in this tutorial) or Fermyon Cloud
To deploy this on Fermyon Wasm Functions, you must request access to the service. If you do not have global distribution requirements, you can follow along using Fermyon Cloud instead which offers a free developer tier.
Setting Up Your Development Environment
First, let’s clone the sample which is available in the Fermyon Wasm Functions example repository.
git clone https://github.com/fermyon/fwf-examples.git
cd samples/graphql-stargazer
A quick check of the folder reveals a typical Spin application directory structure with a source code directory, spin.toml
(application manifest), and cargo.toml
.
ls
Cargo.lock Cargo.toml README.md spin.toml src target
Understanding the GraphQL Schema and Query
Our application uses GitHub’s public GraphQL API. In this sample, for the sake of simplicity, we’re only interested in extracting the stargazers count for a particular repository.
For our stargazer query, we create a focused query file src/query_1.graphql
:
query RepoView($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
homepageUrl
stargazers {
totalCount
}
}
}
This query accepts two variables (repository owner and name) and returns the total count of stargazers along with the homepage URL.
Implementing the GraphQL Client
Let’s build our implementation step by step, focusing on the key concepts that make this approach powerful. If you’re following along locally, navigate to src/lib.rs
as we dive into the application logic.
Type-Safe GraphQL Code Generation
The magic of our implementation starts with Rust’s type-safe GraphQL code generation. Using the graphql_client
crate, we can generate compile-time validated Rust code from our GraphQL schema and queries:
use graphql_client::GraphQLQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/schema.graphql",
query_path = "src/query_1.graphql",
response_derives = "Debug"
)]
struct RepoView;
This single derive macro does a lot of work behind the scenes:
- Generates type-safe Rust structs from your GraphQL schema
- Creates query builder methods like
RepoView::build_query()
- Provides compile-time validation that your queries match the schema
- Handles all serialization/deserialization automatically
The generated code creates a repo_view::Variables
struct for query parameters and repo_view::ResponseData
for the response, ensuring you can never send malformed queries or access non-existent fields.
Secure Token Management with Spin Variables
Instead of hardcoding API tokens, we use Spin’s secure variable system which you can see in this snippet from lib.rs
here:
let github_api_token = spin_sdk::variables::get("gh_api_token")
.expect("Missing gh_api_token variable");
We’ll review in more detail how this variable is set later on. By using this approach, we ensure tokens are:
- Never stored in source code
- Injected securely at runtime
- Easy to rotate without code changes
Building and Sending the GraphQL Request
Once we have our query variables, building the request is straightforward:
// Create variables for our GraphQL query using the generated types
let variables = repo_view::Variables {
owner: owner.to_string(),
name: name.to_string(),
};
// Build the GraphQL query structure with our variables
let body = RepoView::build_query(variables);
// Serialize the query to JSON for the HTTP request
let body = serde_json::to_string(&body).unwrap();
// Construct the HTTP POST request to GitHub's GraphQL endpoint
let outgoing = Request::post("https://api.github.com/graphql", body)
.header("user-agent", "graphql-rust") // GitHub requires a user-agent
.header("content-type", "application/json") // GraphQL queries are sent as JSON
.header("Authorization", format!("Bearer {}", github_api_token)) // API authentication
.build();
// Send the request and await the response
let res: Response = send(outgoing).await?;
The type-safe build_query()
method ensures we’re sending exactly the right GraphQL query structure to GitHub’s API.
Processing the GraphQL Response
The response handling showcases the utility of generated types:
// Parse the GraphQL response into our generated type-safe structs
let response: graphql_client::Response<repo_view::ResponseData> =
serde_json::from_slice(res.body()).unwrap();
// Extract the actual data from the GraphQL response wrapper
let response_data = response.data.expect("missing response data");
// Navigate the response with complete type safety
// The compiler ensures these fields exist and have the right types
let stars = response_data
.repository
.as_ref() // Handle the case where repository might be null (not found)
.map(|repo| repo.stargazers.total_count);
Notice how we can navigate the response with complete type safety: response_data.repository.stargazers.total_count
is validated at compile time to match our GraphQL schema.
URL Parsing and Error Handling
Our application expects URLs like /fermyon/finicky-whiskers
and extracts the repository information:
fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), anyhow::Error> {
// Split the path on '/' and skip the first empty element
// For "/fermyon/finicky-whiskers", this gives us ["", "fermyon", "finicky-whiskers"]
let mut parts = repo_name.split('/').skip(1);
// Extract owner and repository name, ensuring both exist
match (parts.next(), parts.next()) {
(Some(owner), Some(name)) => Ok((owner, name)),
_ => Err(format_err!(
"wrong format for the repository name param (we expect something like spinframework/spin)"
))
}
}
Configuration with spin.toml
Now that we’ve completed our application logic, let’s take a look at how our application’s capabilities and constraints are expressed in the application manifest (spin.toml
). Our spin.toml
file configures the application with proper security constraints. Here we have a single component application that is invoked with an HTTP trigger. This component has the capability to make an outbound HTTP call and can access a variable called gh_api_token
, which we used above to make calls to GitHub’s public API. This is the value that get passed into our lib.rs
file.
spin_manifest_version = 2
[application]
name = "graphql"
version = "0.1.0"
authors = ["Till Schneidereit <till@tillschneidereit.net>"]
description = "GraphQL experiments"
[variables]
gh_api_token = { secret = true, required = true }
[[trigger.http]]
route = "/..."
component = "graphql"
[component.graphql]
source = "target/wasm32-wasip1/release/graphql.wasm"
allowed_outbound_hosts = ["https://api.github.com"]
variables = { gh_api_token = "{{ gh_api_token }}" }
[component.graphql.build]
command = "cargo build --target wasm32-wasip1 --release"
watch = ["src/**/*.rs", "Cargo.toml"]
Security Features
- Secret variables: The GitHub token is marked as
secret = true
- Outbound host restrictions: Only
https://api.github.com
is allowed - Secure by default: WebAssembly’s sandbox model provides additional isolation such as preventing access to the filesystem unless explicitly granted to the Spin app
Building and Testing Locally
To run the application locally:
# Install the WebAssembly target if you haven't already
rustup target add wasm32-wasip1
# Build the application
spin build
# Run with your GitHub token
SPIN_VARIABLE_GH_API_TOKEN=[your-token-here] spin up
Note that you must set the variable name exactly as above to pass in the variable when running a Spin app locally.
Now you can test it by visiting URLs that follow this format, corresponding to the desired GitHub organization and repo:
http://localhost:3000/gh-org/gh-repo
curl localhost:3000/fermyon/finicky-whiskers
Each will show you the stargazer count for that repository!
Deploying to Fermyon Wasm Functions
Deployment is straightforward with Fermyon Wasm Functions. With one simple command, your application will be globally distributed:
# Log into Fermyon Wasm Functions if you haven't already
spin aka login
# Deploy with your GitHub token
spin aka deploy --variable gh_api_token=[your-token-here]
The deployment will provide you with a unique URL where your GraphQL client is running in production.
<...>
App Routes:
- graphql: https://87f9c916-331c-4a44-b7ad-d2a8331185ff.aka.fermyon.tech (wildcard)
If you’re following along using Fermyon Cloud, you can find steps on how to deploy here.
Conclusion
Building GraphQL clients with Fermyon Wasm Functions combines the precision of GraphQL with the performance benefits of WebAssembly. This approach is particularly powerful for:
- APIs requiring type safety and compile-time validation
- Applications with unpredictable traffic patterns
- Microservices architectures requiring fast startup times
- Teams wanting to leverage Rust’s ecosystem for API clients
The serverless nature of Wasm Functions ensures your applications run fast and secure on Fermyon Wasm Functions.
Resources and Further Reading: