AWS Lambda Tutorial for Swift: Getting Started

Swift is now available in the world of serverless functions via AWS Lambda! Get started deploying your first on-demand serverless function with our AWS Lambda for Swift tutorial. By Ralph Kuepper.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Writing a Swift App for AWS Lambda

Open the starter project in Xcode by clicking on Package.swift. Take note of the following two dependencies in this file:

// 1
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git",   
         .upToNextMajor(from:"0.1.0")),
// 2
.package(url: "https://github.com/swift-server/async-http-client.git", 
         from: "1.0.0")

The two dependencies are:

  1. The application needs the AWS Lambda function to have the official AWS Lambda framework available.
  2. The AsyncHttpClient to fetch a document from a server

Next, take a look at main.swift in Sources ▸ EURCurrencyRate. This file defines three global instances of classes you need in your functions:

//1
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
//2
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)

defer {
  try? httpClient.syncShutdown()
}

Here’s what’s happening in the code above:

  1. JSONEncoder and JSONDecoder will read and write JSON content.
  2. You need HTTPClient to pull the most current exchange rate from an external website. You’re creating a new EventLoop by using the .createNew static value for the eventLoopGroupProvider parameter when initializing your HTTPClient.
Note: AWS Lambda isn’t running on an EventLoop by default and it doesn’t need to.

At line 50, you see the function that AWS Lambda calls through the runtime framework:

Lambda.run { 
  (context, 
   request: APIGateway.V2.Request, 
   callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void) in

The framework defines input and output types you use in the function and return in the form of a callback function. The context of the request is defined as APIGateway.V2.Request, which will tell you what kind of a request this is.

You also see a global convert function, which will create the actual output of the AWS Lambda function on line 46.

You’ll get this function to react to the following two requests:

  • GET: /convert: Take the input in form of query parameters: ?amount=1
  • POST: /convert: Take the input in the form of a JSON body.

Creating Your Function’s Model

Before you write the logic, create the following three types in separate files in EURCurrencyRate:

First, create RateInput.swift for a struct that resembles the input for the POST call:

import Foundation
 
struct RateInput: Codable {
  // 1
  let amount: Double
  // 2
  let currency: String?
}

The two inputs are:

  1. amount: This is the actual amount of money the function converts.
  2. currency: This is optional and will default to USD.

Now create a RateOutput.swift for a struct that represents the output of the conversion:

import Foundation
 
struct RateOutput: Codable {
  // 1
  let inputAmount: Double
  // 2
  let rate: Double
  // 3
  let inputCurrency: String
  // 4
  let outputAmount: Double
  // 5
  let outputCurrency: String
}

The variables in this struct represent:

  1. inputAmount: the original amount provided
  2. rate: the applied conversion rate
  3. inputCurrency: the input currency for inputAmount
  4. outputAmount: the converted amount, using inputAmount and rate
  5. outputCurrency: the desired currency

Finally, create a RateResponse.swift file containing the structure for the response from the API you use to determine the exchange rate:

import Foundation
 
struct RateResponse: Codable {
  var rates: [String: Double]
  var base: String
}

The API returns many rates; the logic will sort out which one to take.

Writing Your Function’s Body

Next you’ll write the business logic. Write the following code in the Lambda.run function in main.swift:

// 1
switch (request.context.http.path, request.context.http.method) {
// 2
case ("/convert", .GET):
  // 3
  let amount = Double(request.queryStringParameters?["amount"] ?? "0.0") ?? 0
  let desiredCurrency = request.queryStringParameters?["currency"] ?? "USD"
  // 4
  convert(amount: amount, desiredCurrency: desiredCurrency, callback: callback)
// 5
case ("/convert", .POST):
  // 6
  if let data = request.body?.data(using: .utf8), 
    let input = try? jsonDecoder.decode(RateInput.self, from: data) {
      convert(
        amount: input.amount, 
        desiredCurrency: input.currency ?? "USD", 
        callback: callback)
  } else {
    // 7
    callback(.success(APIGateway.V2.Response(statusCode: .badRequest)))
  }
default:
  // 8
  callback(.success(APIGateway.V2.Response(statusCode: .notFound)))
}

Here’s what the code above does:

  1. Checks the context of this call. Remember AWS Lambda functions are often called internally and not always by a user and a browser.
  2. If it’s a GET call to /convert, this case applies.
  3. Since it’s a GET call, convert the query parameters into variables that you can pass to the convert function.
  4. Call the convert function and pass the callback along.
  5. If it’s a POST call, this case applies.
  6. Instead of parsing the variables from the URL, you decode the data from the request body into the RateInput struct. If this succeeds, call the convert function as well.
  7. If, for some reason, like invalid JSON, the body doesn’t parse, return a 400: Bad Request HTTP code.
  8. If it’s a call to a different path or a different type of HTTP call, return a 404: Not Found HTTP code.

Both functions then call the global convert function to calculate the output and return it to the runtime.

Write the following code in the convert function:

// 1
httpClient.get(url: "https://api.exchangeratesapi.io/latest").whenComplete { 
  result in
  switch result {
  case .failure(let error):
    // 2
    callback(.failure(error))
  case .success(let response):
    // 3
    if let data = response.body, response.status == .ok {
      let data = try? jsonDecoder.decode(RateResponse.self, from: data)
      if let data = data {
        // 4
        for currency in data.rates.keys where currency == desiredCurrency {
          // 5
          let rate = Double(data.rates[currency] ?? 0)
          // 6
          let newAmount = rate * amount
          // 7
          let output = RateOutput(
            inputAmount: amount,
            rate: rate,
            inputCurrency: "EUR",
            outputAmount: newAmount,
            outputCurrency: desiredCurrency)
          // 8
          if let data = try? jsonEncoder.encode(output) {
            // 9
            callback(.success(APIGateway.V2.Response(
              statusCode: .ok,
              multiValueHeaders: ["content-type": ["application/json"]],
              body: String(decoding: data, as: Unicode.UTF8.self)
            )))
          }
        }
      }
      callback(.success(APIGateway.V2.Response(statusCode: .badRequest)))
    } else {
      callback(
        .success(APIGateway.V2.Response(statusCode: .internalServerError))
      )
    }
  }
}

Here’s what’s happening in the code above:

  1. You call an API that returns conversion rates.
  2. If this fails, you return an error right away, as you can’t calculate anything.
  3. At this point, you at least have a response from the API, so now you parse it into the previously defined RateResponse struct.
  4. You loop through the different rates to find the one specified in the input.
  5. Convert the value to a double.
  6. Calculate the new amount.
  7. Form the output as the RateOutput struct.
  8. Convert this output to JSON.
  9. Call the callback function with the JSON string and a 200: OK HTTP code.

This function is the heart of your program. Once you write this, you can start compiling! :]