Skip to content

Add a full working Serverless API example using APIGateway, Lambda, DynamoDB #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Examples/LambdaFunctions/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ let package = Package(
.executable(name: "APIGateway", targets: ["APIGateway"]),
// fully featured example with domain specific business logic
.executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]),
// Full REST API Example using APIGateway, Lambda, DynamoDB
.executable(name: "ProductAPI", targets: ["ProductAPI"]),
],
dependencies: [
// this is the dependency on the swift-aws-lambda-runtime library
// in real-world projects this would say
// .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0")
.package(name: "swift-aws-lambda-runtime", path: "../.."),

// The following packages are required by LambdaAPI
// AWS SDK Swift
.package(url: "https://github.com/soto-project/soto.git", from: "5.0.0-beta.2"),
// Logging
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
],
targets: [
.target(name: "HelloWorld", dependencies: [
Expand All @@ -42,5 +50,12 @@ let package = Package(
.target(name: "CurrencyExchange", dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
]),
.target(name: "ProductAPI", dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
.product(name: "SotoDynamoDB", package: "soto"),
.product(name: "Logging", package: "swift-log"),
]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSLambdaEvents
import class Foundation.JSONEncoder
import class Foundation.JSONDecoder

extension APIGateway.V2.Request {

static private let decoder = JSONDecoder()

public func bodyObject<T: Codable>() throws -> T {
guard let body = self.body,
let dataBody = body.data(using: .utf8)
else {
throw APIError.invalidRequest
}
return try Self.decoder.decode(T.self, from: dataBody)
}
}

extension APIGateway.V2.Response {

private static let encoder = JSONEncoder()

public static let defaultHeaders = [
"Content-Type": "application/json",
//Security warning: XSS are enabled
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE",
"Access-Control-Allow-Credentials": "true",
]

public init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
self.init(
statusCode: statusCode,
headers: APIGateway.V2.Response.defaultHeaders,
multiValueHeaders: nil,
body: "{\"message\":\"\(String(describing: error))\"}",
isBase64Encoded: false
)
}

public init<Out: Encodable>(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
var body: String = "{}"
if let data = try? Self.encoder.encode(object) {
body = String(data: data, encoding: .utf8) ?? body
}
self.init(
statusCode: statusCode,
headers: APIGateway.V2.Response.defaultHeaders,
multiValueHeaders: nil,
body: body,
isBase64Encoded: false
)
}
}
31 changes: 31 additions & 0 deletions Examples/LambdaFunctions/Sources/ProductAPI/Product.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

public struct Product: Codable {
public let sku: String
public let name: String
public let description: String
public var createdAt: String?
public var updatedAt: String?

public struct Field {
static let sku = "sku"
static let name = "name"
static let description = "description"
static let createdAt = "createdAt"
static let updatedAt = "updatedAt"
}
}
103 changes: 103 additions & 0 deletions Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import SotoDynamoDB
import AWSLambdaEvents
import AWSLambdaRuntime
import AsyncHTTPClient
import Logging
import NIO

enum Operation: String {
case create
case read
case update
case delete
case list
}

struct EmptyResponse: Codable {}

struct ProductLambda: LambdaHandler {

typealias In = APIGateway.V2.Request
typealias Out = APIGateway.V2.Response

let dbTimeout: Int64 = 30
let region: Region
let db: SotoDynamoDB.DynamoDB
let service: ProductService
let tableName: String
let operation: Operation
var httpClient: HTTPClient

static func currentRegion() -> Region {
if let awsRegion = Lambda.env("AWS_REGION") {
let value = Region(rawValue: awsRegion)
return value
} else {
return .useast1
}
}

static func tableName() throws -> String {
guard let tableName = Lambda.env("PRODUCTS_TABLE_NAME") else {
throw APIError.tableNameNotFound
}
return tableName
}

init(context: Lambda.InitializationContext) throws {

guard let handler = Lambda.env("_HANDLER"),
let operation = Operation(rawValue: handler) else {
throw APIError.invalidHandler
}
self.operation = operation
self.region = Self.currentRegion()

let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout)
let timeout = HTTPClient.Configuration.Timeout(
connect: lambdaRuntimeTimeout,
read: lambdaRuntimeTimeout
)

let configuration = HTTPClient.Configuration(timeout: timeout)
self.httpClient = HTTPClient(
eventLoopGroupProvider: .shared(context.eventLoop),
configuration: configuration
)

let awsClient = AWSClient(httpClientProvider: .shared(self.httpClient))

self.db = SotoDynamoDB.DynamoDB(client: awsClient, region: region)
self.tableName = try Self.tableName()

self.service = ProductService(
db: db,
tableName: tableName
)
}

func handle(
context: Lambda.Context, event: APIGateway.V2.Request,
callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void
) {
let _ = ProductLambdaHandler(service: service, operation: operation)
.handle(context: context, event: event)
.always { (result) in
callback(result)
}
}
}
109 changes: 109 additions & 0 deletions Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSLambdaEvents
import AWSLambdaRuntime
import Logging
import NIO

struct ProductLambdaHandler: EventLoopLambdaHandler {

typealias In = APIGateway.V2.Request
typealias Out = APIGateway.V2.Response

let service: ProductService
let operation: Operation

func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture<APIGateway.V2.Response> {

switch self.operation {
case .create:
return createLambdaHandler(context: context, event: event)
case .read:
return readLambdaHandler(context: context, event: event)
case .update:
return updateLambdaHandler(context: context, event: event)
case .delete:
return deleteUpdateLambdaHandler(context: context, event: event)
case .list:
return listUpdateLambdaHandler(context: context, event: event)
}
}

func createLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture<APIGateway.V2.Response> {
guard let product: Product = try? event.bodyObject() else {
let error = APIError.invalidRequest
return context.eventLoop.makeFailedFuture(error)
}
return service.createItem(product: product)
.map { result -> (APIGateway.V2.Response) in
return APIGateway.V2.Response(with: result, statusCode: .created)
}.flatMapError { (error) -> EventLoopFuture<APIGateway.V2.Response> in
let value = APIGateway.V2.Response(with: error, statusCode: .forbidden)
return context.eventLoop.makeSucceededFuture(value)
}
}

func readLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture<APIGateway.V2.Response> {
guard let sku = event.pathParameters?["sku"] else {
let error = APIError.invalidRequest
return context.eventLoop.makeFailedFuture(error)
}
return service.readItem(key: sku)
.flatMapThrowing { result -> APIGateway.V2.Response in
return APIGateway.V2.Response(with: result, statusCode: .ok)
}.flatMapError { (error) -> EventLoopFuture<APIGateway.V2.Response> in
let value = APIGateway.V2.Response(with: error, statusCode: .notFound)
return context.eventLoop.makeSucceededFuture(value)
}
}

func updateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture<APIGateway.V2.Response> {
guard let product: Product = try? event.bodyObject() else {
let error = APIError.invalidRequest
return context.eventLoop.makeFailedFuture(error)
}
return service.updateItem(product: product)
.map { result -> (APIGateway.V2.Response) in
return APIGateway.V2.Response(with: result, statusCode: .ok)
}.flatMapError { (error) -> EventLoopFuture<APIGateway.V2.Response> in
let value = APIGateway.V2.Response(with: error, statusCode: .notFound)
return context.eventLoop.makeSucceededFuture(value)
}
}

func deleteUpdateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture<APIGateway.V2.Response> {
guard let sku = event.pathParameters?["sku"] else {
let error = APIError.invalidRequest
return context.eventLoop.makeFailedFuture(error)
}
return service.deleteItem(key: sku)
.map { _ -> (APIGateway.V2.Response) in
return APIGateway.V2.Response(with: EmptyResponse(), statusCode: .ok)
}.flatMapError { (error) -> EventLoopFuture<APIGateway.V2.Response> in
let value = APIGateway.V2.Response(with: error, statusCode: .notFound)
return context.eventLoop.makeSucceededFuture(value)
}
}

func listUpdateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture<APIGateway.V2.Response> {
return service.listItems()
.flatMapThrowing { result -> APIGateway.V2.Response in
return APIGateway.V2.Response(with: result, statusCode: .ok)
}.flatMapError { (error) -> EventLoopFuture<APIGateway.V2.Response> in
let value = APIGateway.V2.Response(with: error, statusCode: .forbidden)
return context.eventLoop.makeSucceededFuture(value)
}
}
}
Loading