콘텐츠로 이동

Services

Vapor's Application and Request are built to be extended by your application and third-party packages. New functionality added to these types are often called services.

Read Only

The simplest type of service is read-only. These services consist of computed variables or methods added to either application or request.

import Vapor

struct MyAPI {
    let client: Client

    func foos() async throws -> [String] { ... }
}

extension Request {
    var myAPI: MyAPI {
        .init(client: self.client)
    }
}

Read-only services can depend on any pre-existing services, like client in this example. Once the extension has been added, your custom service can be used like any other property on request.

req.myAPI.foos()

Writable

Services that need state or configuration can utilize Application and Request storage for storing data. Let's assume you want to add the following MyConfiguration struct to your application.

struct MyConfiguration {
    var apiKey: String
}

To use storage, you must declare a StorageKey.

struct MyConfigurationKey: StorageKey {
    typealias Value = MyConfiguration
}

This is an empty struct with a Value typealias specifying which type is being stored. By using an empty type as the key, you can control what code is able to access your storage value. If the type is internal or private, only your code will be able to modify the associated value in storage.

Finally, add an extension to Application for getting and setting the MyConfiguration struct.

extension Application {
    var myConfiguration: MyConfiguration? {
        get {
            self.storage[MyConfigurationKey.self]
        }
        set {
            self.storage[MyConfigurationKey.self] = newValue
        }
    }
}

Once the extension is added, you can use myConfiguration like a normal property on Application.

app.myConfiguration = .init(apiKey: ...)
print(app.myConfiguration?.apiKey)

Lifecycle

Vapor's Application allows you to register lifecycle handlers. These let you hook into events such as boot and shutdown.

// Prints hello during boot.
struct Hello: LifecycleHandler {
    // Called before application boots.
    func willBoot(_ app: Application) throws {
        app.logger.info("Hello!")
    }

    // Called after application boots.
    func didBoot(_ app: Application) throws {
        app.logger.info("Server is running")
    }

    // Called before application shutdown.
    func shutdown(_ app: Application) {
        app.logger.info("Goodbye!")
    }
}

// Add lifecycle handler.
app.lifecycle.use(Hello())

Locks

Vapor's Application includes conveniences for synchronizing code using locks. By declaring a LockKey, you can get a unique, shared lock to synchronize access to your code.

struct TestKey: LockKey { }

let test = app.locks.lock(for: TestKey.self)
test.withLock {
    // Do something.
}

Each call to lock(for:) with the same LockKey will return the same lock. This method is thread-safe.

For an application-wide lock, you can use app.sync.

app.sync.withLock {
    // Do something.
}

Request

Services that are intended to be used in route handlers should be added to Request. Request services should use the request's logger and event loop. It is important that a request stay on the same event loop or an assertion will be hit when the response is returned to Vapor.

If a service must leave the request's event loop to do work, it should make sure to return to the event loop before finishing. This can be done using the hop(to:) on EventLoopFuture.

Request services that need access to application services, such as configurations, can use req.application. Take care to consider thread-safety when accessing the application from a route handler. Generally, only read operations should be performed by requests. Write operations must be protected by locks.