Expose REST APIs as GraphQL
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.
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 edgeAuthor: My NameLanguage:[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-contentChoose 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.
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:
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 userGET /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:
{ "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.
#[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.
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:
#[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:
#[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 serviceadd the Service ID to the fastly.toml file, otherwise follow the prompts to create aservice 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.appBackend port number: [80] 443Backend 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:
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.