Soto icon

Soto

Using Soto with Vapor 4

When using Soto with Vapor 4 it is best to have a global AWSClient that all routes use. You shouldn't be creating an AWSClient on the fly. Initialization of the client can take time and you have to shutdown the client before it is deleted. You best option is to store a single AWSClient in the Vapor Application. The code below shows how you can extend Application to provide a global AWSClient.

import Vapor

public extension Application {
    var aws: AWS {
        .init(application: self)
    }

    struct AWS {
        struct ClientKey: StorageKey {
            typealias Value = AWSClient
        }

        public var client: AWSClient {
            get {
                guard let client = self.application.storage[ClientKey.self] else {
                    fatalError("AWSClient not setup. Use application.aws.client = ...")
                }
                return client
            }
            nonmutating set {
                self.application.storage.set(ClientKey.self, to: newValue) {
                    try $0.syncShutdown()
                }            
            }
        }

        let application: Application
    }
}

And extend Request to provide access to this AWSClient.

public extension Request {
    var aws: AWS {
        .init(request: self)
    }

    struct AWS {
        var client: AWSClient {
            return request.application.aws.client
        }

        let request: Request
    }
}

Once you have this you can then initialize your client in the configure(_ app: Application) function found in configure.swift. The code below initializes an AWSClient to use the shared HTTPClient that Vapor uses.

app.aws.client = AWSClient(httpClientProvider: .shared(app.http.client.shared))

And then in all your routes you can access the AWSClient as follows

func myRoute(req: Request) async throws -> Response {
    let client = req.aws.client
    let s3 = S3(client: client, region: .useast1)
}

Alternatively you can also include your service structs in the Application as well.

import SotoS3

extension Application.AWS {
    struct S3Key: StorageKey {
        typealias Value = S3
    }

    public var s3: S3 {
        get {
            guard let s3 = self.application.storage[S3Key.self] else {
                fatalError("S3 not setup. Use application.aws.s3 = ...")
            }
            return s3
        }
        nonmutating set {
            self.application.storage[S3Key.self] = newValue
        }
    }
}

And provide access to them through Request:

public extension Request.AWS {
    var s3: S3 {
        return request.application.aws.s3
    }
}

Example

If you have extended your Vapor Application as above and also included an SES (Simple Email Service) service object in the Application in a similar way to the example above with S3 you could write a route to send an email as follows:

final class MyController {
    struct EmailData: Content {
        let address: String
        let subject: String
        let message: String
    }
    func sendUserEmailFromJSON(_ req: Request) async throws -> HTTPStatus {
        let emailData = try req.content.decode(EmailData.self)
        let destination = SES.Destination(toAddresses: [emailData.address])
        let message = SES.Message(body: .init(text: SES.Content(data: emailData.message)), subject: .init(data: emailData.subject))
        let sendEmailRequest = SES.SendEmailRequest(destination: destination, message: message, source: "soto@me.com")
        try await req.aws.ses.sendEmail(sendEmailRequest)
        return .ok
    }
}