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.
TheCompute platform allows you to respond to HTTP requests at the edge using a variety of programming languages that compile toWebAssembly. For the purposes of this solution, we will useRust, as it has a rich ecosystem of libraries including thejuniper 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 thecache override interfaces in theFastly 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 theFastly CLI installed. If you are new to the platform, read ourGetting Started guide.
Initialize a project
If you haven't already created a Rust-based Compute project, runfastly compute init in a new directory in your terminal and follow the prompts to provision a new service using thedefault Rust starter kit:
$ mkdir graphql && cd graphql$ fastly compute initCreating 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 thejuniper crate which you will use for this solution.
Add this to your project'sCargo.toml file, optionally disabling thedefault-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 theserde_json crate.
This solution also usesserde_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 theserde-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 atmock.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 theGET /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 theFastly 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 aBackendResponse type, which uses ageneric type parameter<T> to allow the encapsulation of both of the documents, removing the need to duplicate theok anddata parameters when adding more types.
You need to be able to deserialize these types from JSON. By adding theDeserialize implementation fromserde, 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 tosrc/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)]structBackendResponse<T>{ ok:bool, data:T,}structQuery;#[derive(Deserialize, GraphQLObject)]structUser{/// User ID id:String,/// Metadata name:String, email:String,}#[derive(Deserialize, GraphQLObject)]structProduct{/// Product ID id:String,/// Metadata name:String, year:i32, color:String,}Make requests to the backend
Now you can define anApiClient 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.
constBACKEND_NAME:&str="api_backend";/// The default TTL for requests.constTTL:u32=60;structApiClient;impljuniper::ContextforApiClient{}implApiClient{pubfnnew()->ApiClient{ApiClient{}}/// Get a user, given their ID.pubfnget_user(&self, id:String)->FieldResult<User>{let req=Request::get(format!("https://host/users/{}", id)).with_pass(true);letmut resp= req.send(BACKEND_NAME)?;// Read the response body into a BackendResponselet response:BackendResponse<User>= resp.take_body_json()?;Ok(response.data)}/// Get a product, given its ID.pubfnget_product(&self, id:String)->FieldResult<Product>{let req=Request::get(format!("https://host/products/{}", id)).with_ttl(TTL);letmut resp= req.send(BACKEND_NAME)?;// Read the response body into a BackendResponselet 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 thejuniper crate, which will build your GraphQL schema and call into yourApiClient 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 withGraphQLObject, and change the query response types toFieldResult fromjuniper. This allows the crate to derive a GraphQL schema from our Rust types:
#[juniper::graphql_object(Context = ApiClient)]implQuery{fnuser(&self, id:String, context:&ApiClient)->FieldResult<User>{ context.get_user(id)}fnproduct(&self, id:String, context:&ApiClient)->FieldResult<Product>{ context.get_product(id)}}Expose the GraphQL endpoint
We now have a GraphQL schema thatjuniper can work with, so let's work on the main function and have it handle requests to thePOST /graphql endpoint:
#[fastly::main]fnmain(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 schemalet 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 requestlet graphql_request:GraphQLRequest= req.take_body_json()?;// Execute the request, serialize the response to JSON, and return itlet 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_backendBackend (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/PS1Z4isxPaoZGVKVdv0eYView this service at: https://random-funky-words.edgecompute.appSUCCESS: 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 exposeGraphQL Playground, which is an in-browser IDE for working with GraphQL services. Helpfully, the source for the playground is built into thejuniper crate. Let's import this now, and add a route handler for the root path to serve the GraphQL playground source:
usejuniper::http::playground::playground_source;#[fastly::main]fnmain(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 schemalet 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 requestlet graphql_request:GraphQLRequest= req.take_body_json()?;// Execute the request, serialize the response to JSON, and return itlet 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 thelogging section of the Rust SDK guide.