Integrating a REST API into an Amplify app's GraphQL model
Tags: graphql, amplify, appsync, aws
Published: 2020-04-09 03:28:33

Consolidate a REST API call into a GraphQL model with an [AppSync](https://aws.amazon.com/appsync/) HTTP resolver.  Here are some instructions.  Don't forget to read the summary no less than five times.

## Weather Example

What is the weather in Anchorage, Alaska? Let's ask [OpenWeatherMap.org](https://openweathermap.org/current#min) in a new way with [GraphQL](https://graphql.org/).

```
# schema.graphql
type Weather {
  zipCode: String!
  temperature: String
}

type Query {
  # getWeatherByZipCode is resolved by https://api.openweathermap.org/data/2.5/weather
  getWeatherByZipCode(zipCode: String!) : Weather
}
```
```graphql
# example graphql client query
query GetWeather {
  getWeatherByZipCode(zipCode: "99504") {
    zipCode
    temperature
  }
}
```
```json
{
  "data": {
    "getWeatherByZipCode": {
      "zipCode": "99504",
      "temperature": "274.48"
    }
  }
}
```

It's cold in Anchorage, Alaska.  OpenWeatherMap.org reports the temperature in Kelvin. 274.48 K degrees is chilly.

## Instructions

Let's simply add exactly one AppSync [HTTP resolver](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-http-resolvers.html) to an [Amplify](https://aws-amplify.github.io/) app.

1. Add one GraphQL API with `amplify add api` and name it however you like
1. Add one `query getWeatherByZipCode` and one `type Weather`
1. Add one request [VTL](http://velocity.apache.org/engine/2.0/vtl-reference.html) for the resolver
1. Add one response [VTL](http://velocity.apache.org/engine/2.0/vtl-reference.html) for the resolver  
1. Add one CloudFormation `AWS::AppSync::DataSource` resource
1. Add one CloudFormation `AWS::AppSync::Resolver` resource
1. Push to the cloud with `amplify push`

**1. Add one GraphQL API**
```
amplify add api
```

**2. Add one request VTL for the query getWeatherByZipCode**

Add one request VTL in a file named `Query.getWeatherByZipCode.req.vtl` in the resolvers directory `amplify/backend/api/<apiname>/resolvers`.  The VTL file will invoke the openweather.org REST API when the amplify app asks AppSync for the weather.  

The request VTL template programs AppSync to get the weather from OpenWeatherMap.org with an HTTP GET.

Contents of `Query.getWeatherByZipCode.req.vtl`:
```
#set( $appid = $util.defaultIfNull($context.args.appid, "<secretopenweathermapapikey>") )
{
    "version": "2018-05-29",
    "method": "GET",
    "params" : {
  		"headers" : {
            "Content-Type": "application/json"
        }
    },
    "resourcePath": $util.toJson("/data/2.5/weather?zip=${ctx.args.zipCode}&appid=${appid}")
}
```

**3. Add one `query getWeatherByZipCode` and one `type Weather`**

```graphql
type Weather {
  zipCode: String!
  temperature: String
}
type Query {
  getWeatherByZipCode(zipCode: String!) : Weather
}
```

**4. Add one response VTL for the query getWeatherByZipCode**

Add one response VTL in a file named `Query.getWeatherByZipCode.res.vtl` in the resolvers directory `amplify/backend/api/<apiname>/resolvers`.  

The response VTL programs AppSync to transform the OpenWeatherMap.org [REST API response](https://samples.openweathermap.org/data/2.5/weather?zip=94040,us&appid=b6907d289e10d714a6e88b30761fae22) into the Weather type in our GraphQL schema.

Contents of `Query.getWeatherByZipCode.res.vtl`:
```
## Raise a GraphQL field error in case of a datasource invocation error
#if($ctx.error)
  $util.error($ctx.error.message, $ctx.error.type)
#end
#if($ctx.result.statusCode == 200)
## If response is 200, return the body.
  ## The response is a JSON string.  Let us parse it.
  #set( $json = $util.parseJson($ctx.result.body))
  ## Cherry pick the temperature field
  $util.toJson({
    "zipCode": $ctx.args.zipCode,
    "temperature": $json.main.temp
  })    
#else
  ## If response is not 200, append error message to response
  $utils.appendError($ctx.result.body, "$ctx.result.statusCode")
#end
```

**5. Add one CloudFormation `AWS::AppSync::DataSource` resource**

Add one `AWS::AppSync::DataSource` resource to the CustomResources.json template in `amplify/backend/api/<apiname>/stack/CustomResources.json`

```json
"WeatherDataSource": {
  "Type": "AWS::AppSync::DataSource",
  "Properties": {
    "ApiId": {
      "Ref": "AppSyncApiId"
    },
    "Name": "Weather",
    "Type": "HTTP",
    "HttpConfig": {
       "Endpoint" : "https://api.openweathermap.org"
    }
  }
}
```

**6. Add one CloudFormation `AWS::AppSync::Resolver` resource**

Add one `AWS::AppSync::Resolver` resource to the CustomResources.json template in `amplify/backend/api/<apiname>/stack/CustomResources.json`

The resource programs AppSync to ask the `WeatherDataSource` for weather when a client invokes the `getWeatherByZipCode(zipCode: String!)` query. 

The `FieldName` property is like glue.  That's kinda why CloudFormation is like a glue gun.
```
"QueryOpenWeatherResolver": {
  "Type": "AWS::AppSync::Resolver",
  "DependsOn" : "WeatherDataSource",
  "Properties": {
    "ApiId": {
      "Ref": "AppSyncApiId"
    },
    "DataSourceName": "Weather",
    "TypeName": "Query",
    "FieldName": "getWeatherByZipCode",
    "RequestMappingTemplateS3Location": {
      "Fn::Sub": [
        "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.getWeatherByZipCode.req.vtl",
        {
          "S3DeploymentBucket": {
            "Ref": "S3DeploymentBucket"
          },
          "S3DeploymentRootKey": {
            "Ref": "S3DeploymentRootKey"
          }
        }
      ]
    },
    "ResponseMappingTemplateS3Location": {
      "Fn::Sub": [
        "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.getWeatherByZipCode.res.vtl",
        {
          "S3DeploymentBucket": {
            "Ref": "S3DeploymentBucket"
          },
          "S3DeploymentRootKey": {
            "Ref": "S3DeploymentRootKey"
          }
        }
      ]
    }
  }
}
```

**7. Push to the cloud with `amplify push`**

```
amplify push
```

## Summary

### Simplified Thinking

It's like wrangling data sources.  Herd sources into exactly one spot to reduce complexity of client side code. Benefit from an increasingly consistent mental model of how an app gets data from the world.  When the weather gets popular, I can simply turn on AppSync caching and make sure I'm not slowing the world down.  

### @http directive, throw my hands up

Just thinking out loud, maybe `@http` should simply be a directive on a field, like `@function`. `@http` would make a REST call where `@function` invokes a lambda. Amplify supports [Lambda custom resolvers](https://aws-amplify.github.io/docs/cli-toolchain/graphql#data-access-patterns) and omits HTTP resolvers.  Don't let that stop you!  It's a new way for an app to access a web service. 

There are other ways to simplify.  What's the best? If there is a better way to simplify, please leave a comment below.