Expose REST APIs as GraphQL

The guidance on this page was tested with an older version (0.10.4) of the Rust SDK. It may still work with the latest version (0.11.2), but the change log may help if you encounter any issues.

Your infrastructure consists of one or more REST APIs and you want to expose a unified GraphQL endpoint to fetch and cache data for your next-generation applications.

Illustration of concept

GraphQL is a typed query language for APIs that allows you to fetch data for your application with rich, descriptive queries. Your API defines a schema that can be used by clients to request exactly the data they need and nothing more, often in one single request to the API.

The Compute platform allows you to respond to HTTP requests at the edge using a variety of programming languages that compile to WebAssembly. For the purposes of this solution, we will use Rust, as it has a rich ecosystem of libraries including the juniper GraphQL crate. This allows you to expose a GraphQL endpoint that could be fetching data from multiple backend systems, increasing the pace at which you can build new applications on top of your data stack.

On top of this, you can make use of the cache override interfaces in the Fastly Rust SDK to intelligently cache the responses from your backend APIs, reducing latency for your end-users and decreasing the load on your backend servers.

Instructions

IMPORTANT: This solution assumes that you already have the Fastly CLI installed. If you are new to the platform, read our Getting Started guide.

Initialize a project

If you haven't already created a Rust-based Compute project, run fastly compute init in a new directory in your terminal and follow the prompts to provision a new service using the default Rust starter kit:

$ mkdir graphql && cd graphql
$ fastly compute init
Creating a new Compute project.
Press ^C at any time to quit.
Name: [graphql]
Description: A GraphQL processor at the edge
Author: My Name
Language:
[1] Rust
[2] JavaScript
[4] Other ('bring your own' Wasm binary)
Choose option: [1]
Starter kit:
[1] Default starter for Rust
A basic starter kit that demonstrates routing, simple synthetic responses and
overriding caching rules.
https://github.com/fastly/compute-starter-kit-rust-default
[2] Beacon termination
Capture beacon data from the browser, divert beacon request payloads to a log
endpoint, and avoid putting load on your own infrastructure.
https://github.com/fastly/compute-starter-kit-rust-beacon-termination
[3] Static content
Apply performance, security and usability upgrades to static bucket services such as
Google Cloud Storage or AWS S3.
https://github.com/fastly/compute-starter-kit-rust-static-content
Choose option or paste git URL: [1]

Install dependencies

The Rust ecosystem makes available several libraries to parse GraphQL queries, including the juniper crate which you will use for this solution.

Add this to your project's Cargo.toml file, optionally disabling the default-features as they are not required for this solution.

Cargo.toml
TOML
juniper = { version = "0.15.10", default-features = false }

Juniper will take care of parsing the inbound GraphQL queries, and Fastly can then make the necessary requests to your backend REST API.

When the responses come back, you'll need something to parse those so that they can be presented to the user as a GraphQL response. serde is a Rust crate that provides (de)serialization APIs for common formats. To parse the backend responses' JSON bodies, you will use the serde_json crate.

This solution also uses serde_json to encode the outgoing JSON responses to GraphQL requests.

HINT: If your backends respond with XML, you could adapt the code in this solution to use the serde-xml-rs crate.

Add these dependencies to your Cargo.toml file:

Cargo.toml
TOML
serde = { version = "^1", features = ["derive"] }
serde_json = "^1"

Using the mock backend

To allow you to build this solution without creating your own REST API backend, we've made a mock REST API (using the Compute platform), hosted at mock.edgecompute.app that you're welcome to use. The mock server provides two endpoints:

  • GET /users/:id - Retrieve a user
  • GET /products/:id - Retrieve a product

Your new GraphQL endpoint can unify these calls so a client application can get both of these types with a single request.

Responses from the mock backend are always JSON objects with an "ok" boolean and a "data" payload. A request to the GET /users/:id endpoint would result in a response like this:

GET /users/123
HTTP
{
"ok": true,
"data": {
"id": "123",
"name": "Test Bot",
"email": "me@example.com"
}
}

Make sure to add this backend to your Fastly service, which you can do in either one of the following ways:

  • On manage.fastly.com: Connecting to origins

  • Using the Fastly CLI:

    $ fastly backend create --name=api_backend --address=mock.edgecompute.app --service-id=<service> --version=<version>

Define data types

So how can you model the APIs data types in Rust when the response follows a predictable format? You can build a BackendResponse type, which uses a generic type parameter <T> to allow the encapsulation of both of the documents, removing the need to duplicate the ok and data parameters when adding more types.

You need to be able to deserialize these types from JSON. By adding the Deserialize implementation from serde, you will be able to build these types from the responses you get from the backend.

To define these response, user, and product types, add the following definitions to src/main.rs:

HINT: If you're starting from scratch, feel free to un-collapse and copy the entire code sample as a replacement for the default main.rs file.

main.rs
Rust
#[derive(Deserialize)]
struct BackendResponse<T> {
ok: bool,
data: T,
}
struct Query;
#[derive(Deserialize, GraphQLObject)]
struct User {
/// User ID
id: String,
/// Metadata
name: String,
email: String,
}
#[derive(Deserialize, GraphQLObject)]
struct Product {
/// Product ID
id: String,
/// Metadata
name: String,
year: i32,
color: String,
}

Make requests to the backend

Now you can define an ApiClient type to handle making queries to the backend API:

HINT: If you had multiple backends, you could adapt this code to use the correct backend for each query, and introduce new backend response types if needed.

main.rs
Rust
const BACKEND_NAME: &str = "api_backend";
/// The default TTL for requests.
const TTL: u32 = 60;
struct ApiClient;
impl juniper::Context for ApiClient {}
impl ApiClient {
pub fn new() -> ApiClient {
ApiClient {}
}
/// Get a user, given their ID.
pub fn get_user(&self, id: String) -> FieldResult<User> {
let req = Request::get(format!("https://host/users/{}", id)).with_pass(true);
let mut resp = req.send(BACKEND_NAME)?;
// Read the response body into a BackendResponse
let response: BackendResponse<User> = resp.take_body_json()?;
Ok(response.data)
}
/// Get a product, given its ID.
pub fn get_product(&self, id: String) -> FieldResult<Product> {
let req = Request::get(format!("https://host/products/{}", id)).with_ttl(TTL);
let mut resp = req.send(BACKEND_NAME)?;
// Read the response body into a BackendResponse
let response: BackendResponse<Product> = resp.take_body_json()?;
Ok(response.data)
}
}

This is great! You now have an API client that is aware of the shape of your backend data types, and you can invoke it like this:

let backend = ApiClient::new();
let product = backend.get_product("abcdef".to_string())?;

Build the GraphQL schema

Now we can introduce the juniper crate, which will build your GraphQL schema and call into your ApiClient to fulfil client requests.

First, you need a root query type for the queries to use. This contains the logic that will run to handle an incoming GraphQL query. Your implementation will pass the request on to the API client you built earlier.

You also need to annotate your types with GraphQLObject, and change the query response types to FieldResult from juniper. This allows the crate to derive a GraphQL schema from our Rust types:

main.rs
Rust
#[juniper::graphql_object(Context = ApiClient)]
impl Query {
fn user(&self, id: String, context: &ApiClient) -> FieldResult<User> {
context.get_user(id)
}
fn product(&self, id: String, context: &ApiClient) -> FieldResult<Product> {
context.get_product(id)
}
}

Expose the GraphQL endpoint

We now have a GraphQL schema that juniper can work with, so let's work on the main function and have it handle requests to the POST /graphql endpoint:

main.rs
Rust
#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
// Dispatch the request based on the method and path.
// The GraphQL API itself is at /graphql. All other paths return 404s.
let resp: Response = match (req.get_method(), req.get_path()) {
(&Method::GET, "/") => Response::from_body(playground_source("/graphql", None)),
(&Method::POST, "/graphql") => {
// Instantiate the GraphQL schema
let root_node = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
// Add context to be used by the GraphQL resolver functions,
// in this case a wrapper for a Fastly backend.
let ctx = ApiClient::new();
// Deserialize the post body into a GraphQL request
let graphql_request: GraphQLRequest = req.take_body_json()?;
// Execute the request, serialize the response to JSON, and return it
let res = graphql_request.execute_sync(&root_node, &ctx);
Response::new().with_body_json(&res)?
}
_ => Response::from_body("404 Not Found").with_status(404),
};
Ok(resp)
}

Congratulations! You now have a working GraphQL endpoint running at the edge. If you haven't yet, run the following command to build and deploy your service to the edge:

$ fastly compute publish
✓ Initializing...
✓ Verifying package manifest...
✓ Verifying local rust toolchain...
✓ Building package using rust toolchain...
✓ Creating package archive...
SUCCESS: Built rust package graphql (pkg/graphql.tar.gz)
There is no Fastly service associated with this package. To connect to an existing service
add the Service ID to the fastly.toml file, otherwise follow the prompts to create a
service now.
Press ^C at any time to quit.
Create new service: [y/N] y
✓ Initializing...
✓ Creating service...
Domain: [random-funky-words.edgecompute.app]
Backend (hostname or IP address, or leave blank to stop adding backends): mock.edgecompute.app
Backend port number: [80] 443
Backend name: [backend_1] api_backend
Backend (hostname or IP address, or leave blank to stop adding backends):
✓ Initializing...
✓ Creating domain 'random-funky-words.edgecompute.app'...
✓ Creating backend 'api_backend' (host: mock.edgecompute.app, port: 443)...
✓ Uploading package...
✓ Activating version...
Manage this service at:
https://manage.fastly.com/configure/services/PS1Z4isxPaoZGVKVdv0eY
View this service at:
https://random-funky-words.edgecompute.app
SUCCESS: Deployed package (service PS1Z4isxPaoZGVKVdv0eY, version 1)

Serve GraphQL Playground

Wouldn't it be great if there was some easy way to visualize your new graph API? Let's expose GraphQL Playground, which is an in-browser IDE for working with GraphQL services. Helpfully, the source for the playground is built into the juniper crate. Let's import this now, and add a route handler for the root path to serve the GraphQL playground source:

main.rs
Rust
use juniper::http::playground::playground_source;
#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
// Dispatch the request based on the method and path.
// The GraphQL API itself is at /graphql. All other paths return 404s.
let resp: Response = match (req.get_method(), req.get_path()) {
(&Method::GET, "/") => Response::from_body(playground_source("/graphql", None)),
(&Method::POST, "/graphql") => {
// Instantiate the GraphQL schema
let root_node = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
// Add context to be used by the GraphQL resolver functions,
// in this case a wrapper for a Fastly backend.
let ctx = ApiClient::new();
// Deserialize the post body into a GraphQL request
let graphql_request: GraphQLRequest = req.take_body_json()?;
// Execute the request, serialize the response to JSON, and return it
let res = graphql_request.execute_sync(&root_node, &ctx);
Response::new().with_body_json(&res)?
}
_ => Response::from_body("404 Not Found").with_status(404),
};
Ok(resp)
}

Build and deploy your service again, and you should be presented with GraphQL Playground. Explore the schema and run some queries to see data from the backend API served and cached at the edge.

Next Steps

This solution shows how to use a singular backend for queries, but you could adapt this code to work with multiple backends. You could also perform validation of responses from your backends at the edge to improve the observability of your systems. See the logging section of the Rust SDK guide.