July 27, 2025

Building a GraphQL API with Fermyon Wasm Functions

MacKenzie Adam MacKenzie Adam

fermyon akamai rust graphql

Building a GraphQL API with Fermyon Wasm Functions

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:

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!

Simple HTML rendered by Spin app

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:

 


🔥 Recommended Posts


Quickstart Your Serverless Apps with Spin

Get Started