Generate synthetic content

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

You need to fetch personalized data from external APIs and serve it to the user without a full web stack.

Illustration of concept

While the view layer of the web consists of HTML, CSS, and JavaScript, much of the data you want to present to your users is better represented and exchanged using formats like JSON. Generally, data is interpolated into a template within the core infrastructure of an application and then the HTML can be cached at the edge. In this tutorial you can learn how to move the templating process to the edge, allowing your core application to focus on pure data.

This tutorial documents the creation of a fast, personalized weather dashboard powered by Fastly's geolocation API and WeatherAPI, a provider of weather data in partnership with governments and meteorological agencies around the world. Later in the guide, you will create a free WeatherAPI account and get your own API token to retrieve weather data.

You can see below exactly what you will be building:

WeatherAPI.com

WeatherAPI provides the Forecast API endpoint where you can send requests with the latitude and longitude coordinates of any place in the world, and receive JSON-encoded weather information for that location. This is ideal for a basic weather app, and can be combined with the Fastly geolocation API to fetch weather information for the location associated with the end user's IP address.

You will integrate with this later, but for now let's look at an example request and response from the Forecast endpoint:

Current weather data

curl -L -X GET 'https://api.weatherapi.com/v1/forecast.json?q=51.521,-0.136&days=4&key=4f6b298d55504374b78203319220712'
GET api.weatherapi.com/v1/forecast.json?q={lat},{long}&days={n}&key={api_key}
json
{
"location": {
"name": "London",
"region": "City of London, Greater London",
"country": "United Kingdom",
"lat": 51.52,
"lon": -0.14,

Instructions

IMPORTANT: This tutorial assumes that you have 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 empty Rust starter kit:

$ mkdir weather && cd weather
$ fastly compute init
Creating a new Compute project.
Press ^C at any time to quit.
Name: [weather]
Description: A weather dashboard served at the edge
Author: You!
Language:
[1] Rust
[2] JavaScript
[3] Go
[5] 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] Empty starter for Rust
An empty starter kit project template.
https://github.com/fastly/compute-starter-kit-rust-empty
...
Choose option or paste git URL: [2]

Install dependencies

When the JSON response comes back from WeatherAPI, you'll need something to parse it so that you can use the values in your template. serde is a Rust crate that provides serialization and deserialization for common data formats like JSON.

To generate the view that will be sent to the client, you will need a templating library. For the purposes of this tutorial we suggest tinytemplate.

Add these dependencies to your Cargo.toml file:

Cargo.toml
TOML
serde = { version = "^1.0.149", features = ["derive"] }
tinytemplate = "^1.2.1"

You will also need a helper data structure specific to this tutorial. We have packaged this into a Rust crate that you can include in your project:

Cargo.toml
TOML
weather_demo_helpers = "^0.1.1"

Use the WeatherAPI backend

Obtain an API token

To use the free plan of WeatherAPI, you will need an account and API key. Head to the sign up page and enter your details. Follow their instructions to activate your account.

Head to your dashboard and copy the API key that has been generated for you. You will need to add this key as a property named key to an edge dictionary (also known as a Compute Config Store) named weather_auth on your Fastly service, which you will use in the program next to securely fetch the API token. You can do this however you prefer:

Fetch the API key

Remove the contents of the main method generated by the empty starter kit, and write a method to retrieve the API key you added to the Config Store (dictionary) earlier:

main.rs
Rust
use fastly::ConfigStore;
/// Retrieves the WeatherAPI key from the "auth" Comput@Edge Config Store.
fn get_api_key() -> String {
match ConfigStore::open("auth").get("key") {
Some(key) => key,
None => panic!("No WeatherAPI key!"),
}
}

You will use this method later to generate an API request to WeatherAPI.

Match the request route

This tutorial will serve the generated HTML, stylesheets, and images from the same Compute service. To achieve this, you can use Rust's pattern syntax to execute different logic based on properties of the incoming request, such as the path. Write some basic routing code inside your now-empty main function:

main.rs
Rust
// We provide helpful constants for HTTP methods, headers, status codes, and more:
use fastly::http::{Method, StatusCode, header};
/// The entry point for your application.
#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
// Return early if the request method is not GET.
if req.get_method() != Method::GET {
return Ok(Response::from_status(StatusCode::METHOD_NOT_ALLOWED));
}
let resp = match req.get_path() {
// Serve the weather widget.
"/" => {
}
// Serve static assets.
// Catch all other requests and return a 404.
_ => Response::from_body("The page you requested could not be found")
.with_status(StatusCode::NOT_FOUND),
};
Ok(resp)
}

Fetch the weather data for the user's location

Using Fastly's geolocation API to fetch the user's latitude and longitude will give you all the data needed to construct a request to WeatherAPI. Add the imports and BACKEND_NAME constant as shown below, and the logic within the "/" branch of the match clause to make a request to the backend you defined earlier:

main.rs
Rust
use fastly::geo::{geo_lookup, Geo};
// Define a constant for the backend name:
const BACKEND_NAME: &str = "weather_api";
// Serve the weather widget.
"/" => {
// Get the end user's location.
let location = geo_lookup(req.get_client_ip_addr().unwrap()).unwrap();
// Log output helps you debug issues when developing your service.
// Run `fastly log-tail` to see this output live as you make requests.
println!(
"Requesting weather for {}, {} ({}, {})",
location.latitude(),
location.longitude(),
location.city(),
location.country_name()
);
// Build the API request, and set the cache override to PASS.
let url = format!(
"http://api.weatherapi.com/v1/forecast.json?q={},{}&key={}&days=3",
location.latitude(),
location.longitude(),
get_api_key()
);
let bereq = Request::new(Method::GET, url).with_pass(true);
// Send the request to the backend.
let mut beresp = bereq.send(BACKEND_NAME)?;
}

If you look at the JSON response from WeatherAPI, you'll see that it contains a lot of data you don't need for the weather dashboard – and also that it doesn't contain some elements you do need, like the names of weekdays, or today's date in a friendly format.

πŸ’‘ The take_response_body method of the Fastly Response struct takes a custom return type which can be used to deserialize the JSON response body. You can either define your own type to represent the data you need for the dashboard, or use one from the weather_demo_helpers crate that accompanies this tutorial, to save time.

Import the ForecastAPIResponse struct from the helper crate, and pass it to the take_response_body method:

main.rs
Rust
use weather_demo_helpers::ForecastAPIResponse;
// Send the request to the backend.
let mut beresp = bereq.send(BACKEND_NAME)?;
// Parse the response body into a ForecastAPIResponse.
let api_response = beresp.take_body_json::<ForecastAPIResponse>()?;

πŸ‘‰ Under the hood, ForecastAPIResponse performs sophisticated transformations on the JSON response – for example, by converting timestamps to weekday names, or weather conditions to the names of Material symbols – thanks to the serde framework. The final data structure contains all the variables you need to power your dashboard:

ForecastAPIResponse {
at: LocationData {
location: "London",
dt: DateElements {
weekday: "Thursday",
weekday_short: "Thu",
pretty_date: "8 December 2022",

At this point, visiting your application's domain will fetch your weather info, but will not yet return it to your browser. You'll fix this in the next step.

Generate HTML using a template

This tutorial uses the tinytemplate crate to interpolate dynamic weather data into a static HTML template. You'll need to define a serializable type for the data you will pass to the template, and a method to generate HTML from the data.

main.rs
Rust
use serde::{Deserialize, Serialize};
use tinytemplate::TinyTemplate;
// Serve the weather widget.
"/" => {
let body_response = generate_view(api_response, is_metric);
Response::from_status(StatusCode::OK).with_body_text_html(&body_response)
}
/// Context for TinyTemplate
#[derive(Serialize)]
struct TemplateContext {
weather: ForecastAPIResponse,
is_metric: bool,
}
/// Generates an interpolated HTML string with TinyTemplate.
fn generate_view(
api_response: ForecastAPIResponse,
metric: bool,
) -> String {
// Initialize the template.
let mut tt = TinyTemplate::new();
tt.add_template("weather", include_str!("static/index.html")).unwrap();
// Define the template context.
let context = TemplateContext {
weather: api_response,
is_metric: metric,
};
// Render the interpolated template string.
tt.render("weather", &context).unwrap()
}

If you'd like to use the static/index.html template and other assets from this example, download static.zip and extract it in the src folder of your project. Otherwise, feel free to build your own frontend starting from the TemplateContext data structure.

Serve static assets

Your dashboard should now load and show your current weather! It doesn't look that good though, so you will need to set up a route to serve your stylesheet:

main.rs
Rust
// Serve static assets.
"/style.css" => Response::from_body(include_str!("static/style.css"))
.with_content_type(fastly::mime::TEXT_CSS),

Next, set up some routes to serve the background images from the asset bundle, and avoid repetition by writing a function that constructs image responses:

main.rs
Rust
// Serve static assets.
"/img/snow.jpg" => image_response(include_bytes!("static/img/snow.jpg")),
"/img/rain.jpg" => image_response(include_bytes!("static/img/rain.jpg")),
"/img/cloud.jpg" => image_response(include_bytes!("static/img/cloud.jpg")),
"/img/fog.jpg" => image_response(include_bytes!("static/img/fog.jpg")),
"/img/lightning.jpg" => image_response(include_bytes!("static/img/lightning.jpg")),
"/img/sunshine.jpg" => image_response(include_bytes!("static/img/sunshine.jpg")),
/// Returns an image response from a byte slice.
fn image_response(bytes: &[u8]) -> Response {
Response::from_body(bytes)
// Add a long cache header to the response.
.with_header(header::CACHE_CONTROL, "public, max-age=86400")
.with_content_type(fastly::mime::IMAGE_JPEG)
}

Switching units

Your Compute service will be deployed globally. Some users will expect to read temperatures in Β°C (the metric scale), and others in Β°F. It's a good idea to default to a scale based on the end user's location, but also to provide a way to switch units.

To achieve this, you can read a query string parameter, metric, to determine the desired scale based on user input. If this parameter is not present:

  1. Use the country_code method from Fastly's geolocation API to get the 2-letter country code associated with the user's IP address.
  2. Unless the user is in the United States, the Bahamas, the Cayman Islands, Liberia, Palau, Micronesia, or the Marshall Islands, default to the metric scale to display temperatures.
main.rs
Rust
#[derive(Deserialize)]
struct QueryParams {
metric: Option<bool>,
}
// Parse the query string into the `QueryParams` type.
let query: QueryParams = req.get_query()?;
// Decide whether metric units should be used, based on the
// `metric` query param, falling back on the user's country code.
let is_metric = match query.metric {
Some(metric) => metric,
None => match location.country_code() {
// Countries that use the non-metric Fahrenheit scale
"US" | "BS" | "KY" | "LR" | "PW" | "FM" | "MH" => false,
_ => true,
},
};

The "Switch units" button in the example template should now work as intended. Clicking on this button will reload the page with the metric query parameter set to the opposite of its current value.

Build and deploy

Congratulations! You now have an elegant weather dashboard that can run at the edge. If you haven't yet, run fastly compute publish to build and deploy your service to the Fastly network. When prompted, enter api.weatherapi.com as the backend hostname:

$ fastly compute publish
βœ“ Initializing...
βœ“ Verifying package manifest...
βœ“ Verifying local rust toolchain...
βœ“ Building package using rust toolchain...
βœ“ Creating package archive...
SUCCESS: Built rust package weather (pkg/weather.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): api.weatherapi.com
Backend port number: [80] 443
Backend name: [backend_1] weather_api
Backend (hostname or IP address, or leave blank to stop adding backends):
βœ“ Initializing...
βœ“ Creating domain 'random-funky-words.edgecompute.app'...
βœ“ Creating backend 'weather_api' (host: api.weatherapi.com, 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)

Next Steps

Now that you understand how to make your program act on geolocation data, why not use this to select an appropriate default unit based on the user's locale?

You could also try improving the logging provided in the tutorial, using the log-fastly crate. This allows you to log data to any of Fastly's supported logging backends.

This page is part of a series in the Personalization use case.