Skip to content

Lab 13: Calling External REST APIs

Description

To support different currencies for the coffee order's total price, we'll call to an external currency conversion API using Spring's RestTemplate. This will be done in two steps:

  1. Create a stub service that returns a hard-coded value, to make sure it's integrated properly.

  2. A real, RestTemplate-based implementation that calls out to the API service.

Goals

  • Learn about using stubs to ease development
  • Understand the basics of RestTemplate
  • See how @Profile can be used to switch among services used
  • Learn about handling HTTP query parameters with @RequestParam

a. Stub Service

You'll create a Stub version of the currency conversion service. Remember that a stub returns a hard-coded value.

  1. Create a new interface named CurrencyConversionService in the domain package with the following method:

    int convertToBritishPound(int amount);
    

    Note that this is a domain service, so has no annotations.

  2. Create a new package, com.tedmyoung.coffeekiosk.adapter.out.currency and in that package, create a class named StubCurrencyConversionService that implements CurrencyConversionService and returns a hard-coded value of 1234.

  3. As this is an adapter service, add Spring's @Service annotation to the stub implementation.

b. Add Query Parameter

To support optionally returning the price in a different currency, we'll add support for a query parameter.

Open the CoffeeOrderController class and make the following changes:

  1. Inject the CurrencyConversionService interface as an additional parameter of the existing constructor and save it as a final instance field.

  2. Change the coffeeOrder() method to accept an optional Query Parameter by changing the method signature to:

    public ResponseEntity<CoffeeOrderResponse> coffeeOrderInfo(
        @PathVariable("id") long orderId,
        @RequestParam(value = "currency", defaultValue = "usd") String currency) {
    

    This will assign any incoming Query Parameter (e.g., ?currency=gbp) to the currency variable.

    The defaultValue="usd" means that if no query parameter is sent, currency will default to "usd".

c. Failing Test

Not all tests with Spring controllers have to use Spring support, we can write a unit test that instantiates the controller directly and test it as if Spring weren't involved.

  1. Create a new CoffeeOrderControllerTest class and add the following test:

    @Test
    public void getWithPoundQueryParamConvertsPriceToPounds() throws Exception {
      // Given: an order is in the repository
      CoffeeOrderRepository coffeeOrderRepository = new InMemoryCoffeeOrderRepository();
      // empty coffee order is fine as the price will be ignored for this test
      CoffeeOrder coffeeOrder = new CoffeeOrder("Spring", LocalDateTime.now());
      coffeeOrder.setId(12L); // need to have an id so we know we get an existing order
      coffeeOrderRepository.save(coffeeOrder);
    
      CoffeeOrderController controller =
          new CoffeeOrderController(coffeeOrderRepository, new StubCurrencyConversionService());
    
      // When: we do a GET with "gbp" currency
      CoffeeOrderResponse response = controller.coffeeOrder(coffeeOrder.getId(), "gbp").getBody();
    
      // Then: price will always be 1234
      assertThat(response.getTotalPrice())
          .isEqualTo("1234");
    }
    
  2. This test should fail, but all other tests should still pass.

d. Use the Service

In the CoffeeOrderController class, implement the following behavior in the coffeeInfo method:

  • If currency parameter is "gbp", then:

    1. Take the totalPrice from the coffee order (after it's retrieved from the repository)

    2. Use the CurrencyConversionService to convert the price into Pounds

    3. Store the converted price into the CoffeeOrderResponse as the price.

If implemented properly, the tests should pass.

e. Try It Out

Try it out using the browser, curl, or Postman, both with and without the query parameter of ?currency=gbp:

f. Increase Prices

To make the examples work better (since we're working with whole numbers), change the pricing in the CoffeeItem.price() method so that it's multiples of 100. Instead of 1, 2, and 3, change to 100, 200, and 300.

g. Real Currency Conversion Service

Now you will implement the CurrencyService so it will call out to the remote API. Refer to the example below as needed for the later sections.

The Weather API Example

This is the code and response DTO from the Weather example:

Learning from examples is great, but don't just blindly copy-n-paste...

// instantiate a RestTemplate instance
RestTemplate restTemplate = new RestTemplate();
// create a Template URL for the API
String weatherUrl = "https://basic-weather.herokuapp.com/api/zip/{zip}";

// put the variables in a Map
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("zip", "94404");

// call out to the remote API and get back a DTO
WeatherResponse response =
    restTemplate.getForObject(weatherUrl, 
                              WeatherResponse.class,
                              uriVariables);

This is the DTO that's used above:

public class WeatherResponse {
  private String location;
  private String updated;
  private Float temp;
  private String condition;
  // getters and setters go here
}

h. DTO for Response

Create a DTO ConvertedCurrency that represents the returned JSON. The properties must match the JSON key names below, i.e., it will have 2 properties: the currency string, and the converted result as a double.

The JSON returned from this API looks like:

{
  "currency": "GBP",
  "converted": 71.00
}

i. Using RestTemplate

You'll call out to the remote API like this:

https://jitterted-currency-conversion.herokuapp.com/convert?from=USD&to=GBP&amount=100
  1. Create a new class, HttpCurrencyConversionService in the com.tedmyoung.coffeekiosk.adapter.out.currencyconversion package.

    This class will implement the CurrencyConversionService interface, using the remote API to do the conversion.

  2. Since you'll now have two @Services implementating the same interface, you'll need to use the Spring Profile feature to select among the implementations:

    • Add @Profile("dev") as an annotation for the StubCurrencyConversionService class
    • Add @Profile("prod") as an annotation for the HttpCurrencyConversionService class
    • In the application.properties file, add a new line: spring.profiles.active=prod
  3. In the convertToBritishPound method, create a URI Template string for the remote API:

    • The base part of the URI is http://jitterted-currency-conversion.herokuapp.com/convert
    • There are three query parameters:
      1. from - the source currency, e.g., USD
      2. to - the converted currency, e.g., GBP
      3. amount - the amount to convert, e.g., 10

    Example

    To convert 100 in USD to GBP, the URI would look like this:

    http://jitterted-currency-conversion.herokuapp.com/convert?from=USD&to=GBP&amount=100
    

    Think about what parts of the string need to become template variables. A URI Template Variable looks like {amount}.

  4. Instantiate a RestTemplate and use the getForObject() method, passing in these 3 parameters:

    1. The URI template string from above
    2. ConvertedCurrency.class
    3. The query parameters in the map
  5. Calling getForObject() returns an instance of ConvertedCurrency, so take the converted value (which is a double) and return it as an int (you can just cast it or use the intValue() method).

j. Try It Out

Start the application and try it with the browser, curl, or Postman, both with and without the query parameter of ?currency=gbp:

What happens if you change the profile in the application.properties file to dev?