Generate synthetic content

IMPORTANT: The content on this page is written for version 0.7.0 of the fastly crate. If you have previously used this example, your project may be using an older SDK version. View the changelog to learn how to migrate your program.

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

Illustration of concept

WARNING: This information is part of a limited availability release. Portions of this API may be subject to changes and improvements over time. Fields marked deprecated may be removed in the future and their use is discouraged. For more information, see our product and feature lifecycle descriptions.

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 templated into a view 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 OpenWeatherMap. The OpenWeatherMap API is maintained by OpenWeather, a team of data scientists offering a free plan to build your own tools on top of their data. Later in the guide, you will create an account and get your own API token to use for the example.

You can see below exactly what you will be building:

OpenWeatherMap API

The OpenWeatherMap API provides an endpoint that you can send a request to with latitude and longitude co-ordinates of any place in the world, and receive back JSON-encoded weather information for that location. This is ideal for a 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 OpenWeatherMap API:

Example Response

GET http://api.openweathermap.org/data/2.5/onecall?lat=51.521&lon=-0.136&appid=490fdb5dcff22cf704461c3ca3b616ba&units=metric
json
{
"lat":51.521,
"lon":-0.136,
"timezone":"Europe/London",
"timezone_offset":0,
"current":{
"dt":1612189870,
"sunrise":1612165128,
"sunset":1612198163,
"temp":4.21,
"feels_like":0.78,
"pressure":999,
"humidity":87,
"dew_point":2.24,
"uvi":0.26,
"clouds":90,
"visibility":10000,
"wind_speed":2.57,
"wind_deg":10,
"weather":[
{
"id":804,
"main":"Clouds",
"description":"overcast clouds",
"icon":"04d"
}
]
},
...
}

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@Edge 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 weather && cd weather
$ fastly compute init
Name: [weather]
Description: A weather dashboard served at the edge
Author: My Name
Language:
[1] Rust
[2] AssemblyScript (beta)
Choose option: [1] 1
Starter kit:
[1] Default (https://github.com/fastly/compute-starter-kit-rust-default.git)
Choose option or type URL: [1]

Install dependencies

When the JSON responses come back from OpenWeatherMap, you'll need something to parse those 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 parse the response data from the API, you can use the serde_json crate.

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. The chrono crate is also useful to provide functions for parsing and formatting dates and times.

Add these dependencies to your Cargo.toml file:

Cargo.toml
TOML
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tinytemplate = "1.1"
chrono="0.4.15"

You will also need some helper methods specific to this tutorial, including manipulation of weather data and icon matching. We have packaged these into a Rust crate that you can include in your project:

Cargo.toml
TOML
weather_helpers = "0.1.1"

Use the OpenWeatherMap backend

Obtain an API token

To use the free OpenWeatherMap API, you will need an account and API key from them. Head to the sign up page and enter your details. Follow their instructions to activate your account so you can generate an API token.

Head to the API keys dashboard and create a new key for this project. Or, just copy the default that has been generated for you. You will need to add this key as a property named key to an edge dictionary 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 from an edge dictionary

Remove the contents of the main method generated by the default starter kit, and write a method to retrieve the API key you added to the edge dictionary earlier:

main.rs
Rust
use fastly::Dictionary;
fn get_api_key() -> String {
match Dictionary::open("weather_auth").get("key") {
Some(key) => key,
None => panic!("No OpenWeatherMap API key!"),
}
}

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

Match request route

This tutorial will serve the templated HTML, stylesheets, and images from the same Compute@Edge service. To achieve this, you can use Rust's pattern syntax to run different logic based on properties of the incoming request, such as the path. Add this code to your now-empty main function:

main.rs
Rust
// We provide helpful constants for HTTP methods, headers, status codes, and more:
use fastly::http::{header, Method, StatusCode};
/// 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)
.with_body("This method is not allowed"));
}
let resp = match req.get_path() {
// Serve weather widget
"/" => {
}
// Serve background image
"/bg-image.jpg" => {
}
// Serve static assets
"/style.css" => {
}
// 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 weather 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 OpenWeatherMap. Add the imports and backend constant as shown below, some structs to define the shape of the data that comes back from OpenWeatherMap, and the logic within the "/" branch of the match clause to make a request to the backend you defined earlier:

main.rs
Rust
use chrono::{Date, Datelike, Local};
use fastly::geo::{geo_lookup, Geo};
// Define a constant for the backend name, as shown in your Fastly service:
const BACKEND_NAME: &str = "api.openweathermap.org";
// Serve weather widget
"/" => {
// Get the end user's location
let location = geo_lookup(req.get_client_ip_addr().unwrap()).unwrap();
// Get the local time
let local = Local::now().date();
// Log output helps you debug issues when developing your service.
// Run `fastly logs 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.openweathermap.org/data/2.5/onecall?lat={}&lon={}&appid={}&units={}",
location.latitude(),
location.longitude(),
get_api_key(),
units
);
let bereq = Request::new(Method::GET, url)
.with_header(header::HOST, "api.openweathermap.org")
.with_pass(true);
// Send the request to the backend
let mut beresp = bereq.send(BACKEND_NAME)?;
// Get the response body into an APIResponse
let api_response = beresp.take_body_json::<APIResponse>()?;
let body_response = generate_view(api_response, location, local, &units);
Response::from_body(body_response)
.with_status(StatusCode::OK)
.with_content_type(fastly::mime::TEXT_HTML_UTF_8)
}
/// Struct representing API response
#[derive(Deserialize)]
struct APIResponse {
current: CurrentReport,
daily: Vec<DailyReport>,
minutely: Vec<MinutelyReport>,
}
/// Struct representing a single response entry
#[derive(Deserialize)]
struct CurrentReport {
temp: f32,
wind_speed: f32,
humidity: f32,
weather: Vec<WeatherReport>,
}
/// Struct representing a single day's weather
#[derive(Deserialize)]
struct DailyReport {
dt: i32,
temp: Temperatures,
weather: Vec<WeatherReport>,
}
/// Struct representing a single weather report
#[derive(Deserialize)]
struct WeatherReport {
description: String,
icon: String,
}
/// Struct representing precipitation data
#[derive(Deserialize)]
struct MinutelyReport {
precipitation: f32,
}
/// Struct representing a set of temperatures
#[derive(Deserialize)]
struct Temperatures {
day: f32,
}
/// Basic struct with minimal info about the next days
#[derive(Serialize)]
struct NextDay {
day: String,
temp: String,
icon: String,
}
fn generate_view(
api_response: APIResponse,
location: Geo,
local: Date<Local>,
units: &str,
) -> String {
}

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 predefined static template. If you'd like to use the template and 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 using the variables defined in the TemplateContext struct.

Now you need to define a type for the data you will pass to the template, and fill in the empty generate_view method that you added earlier. This will take in the APIResponse parsed from the API response in the last step to generate the HTML.

main.rs
Rust
use tinytemplate::TinyTemplate;
use weather_helpers;
/// Context for TinyTemplate
#[derive(Serialize)]
struct TemplateContext {
day: String,
day_short: String,
date: String,
city: String,
temp: String,
rain: String,
wind: String,
humidity: String,
description: String,
icon: String,
next_days: Vec<NextDay>,
is_metric: bool,
}
fn generate_view(
api_response: APIResponse,
location: Geo,
local: Date<Local>,
units: &str,
) -> String {
// Initialize template
let mut tt = TinyTemplate::new();
tt.add_template("weather", include_str!("static/index.html"))
.unwrap();
// Get the data for the next three days and put them in a vector to iterate them later in
// the template
let mut next_days: Vec<NextDay> = Vec::new();
for i in 0..3 {
next_days.push(NextDay {
day: weather_helpers::datetime_to_day(format!("{}", api_response.daily[i + 1].dt)),
temp: (api_response.daily[i + 1].temp.day as i32).to_string(),
icon: weather_helpers::get_feather_weather_icon(
&api_response.daily[i + 1].weather[0].icon,
),
});
}
// Fill the template context
let context = TemplateContext {
day: weather_helpers::weekday_full(local.weekday().to_string()),
day_short: local.weekday().to_string(),
date: local.format("%e %B %Y").to_string(),
city: String::from(location.city()),
temp: (api_response.current.temp as i32).to_string(),
rain: format!("{}", api_response.minutely[0].precipitation),
wind: format!("{}", api_response.current.wind_speed),
humidity: format!("{}", api_response.current.humidity),
description: format!("{}", api_response.current.weather[0].description).replace("\"", ""),
icon: weather_helpers::get_feather_weather_icon(&api_response.current.weather[0].icon),
next_days,
is_metric: units == "metric",
};
tt.render("weather", &context).unwrap()
}

Serve static assets

The dashboard should now load and show your current weather! It doesn't look that good though, so set up some routes to serve the CSS file, icon script, and one of the background images from the asset bundle.

Passing in the user's geographical location to the get_season helper will allow you to serve a dynamic background image based on the current season:

main.rs
Rust
use weather_helpers::Season;
// Serve background image
"/bg-image.jpg" => {
// Serve dynamic background image based on season
let location = geo_lookup(req.get_client_ip_addr().unwrap()).unwrap();
let local = Local::now().date();
let image: &[u8] = match weather_helpers::get_season(location, local) {
Season::Summer => include_bytes!("static/img/summer.jpg"),
Season::Autumn => include_bytes!("static/img/autumn.jpg"),
Season::Winter => include_bytes!("static/img/winter.jpg"),
Season::Spring => include_bytes!("static/img/spring.jpg"),
};
Response::from_body(image)
.with_status(StatusCode::OK)
.with_content_type(fastly::mime::IMAGE_JPEG)
}
// Serve static assets
"/style.css" => {
Response::from_body(include_str!("static/style.css"))
.with_content_type(fastly::mime::TEXT_CSS)
}
"/feather.min.js" => {
Response::from_body(include_str!("static/feather.min.js"))
.with_content_type(fastly::mime::TEXT_JAVASCRIPT)
}

Switch units per-user

Bearing in mind that your application will be deployed globally, it is a good idea to localise your views to make them accessible for as many users as possible. For temperatures, you can read a query string parameter to determine the desired units to use:

main.rs
Rust
#[derive(Deserialize)]
struct QueryParams {
units: Option<String>,
}
// Fetch the query string and parse it into the `QueryParams` type
let query: QueryParams = req.get_query()?;
// Get units from query params, or default to "metric"
let units = match query.units {
Some(units) => units,
None => String::from("metric"),
};

The "Switch Units" link in the example template should now work as intended.

Build and deploy

Congratulations! You now have an elegant weather dashboard that can run at the edge. If you haven't yet, run the following command to build and deploy your service to the Fastly network. When prompted, enter api.openweathermap.org 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.
Domain: [random-funky-words.edgecompute.app]
Backend (originless, hostname or IP address): [originless] api.openweathermap.org
✓ Initializing...
✓ Reading package manifest...
✓ Fetching latest version...
✓ Validating package...
✓ Cloning latest version...
✓ Uploading package...
✓ Activating version...
✓ Updating package manifest...
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 would allow you to log data to any of Fastly's supported logging backends.