Services, Storage and Dependency Injection

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Vapor 4.122.2 on Swift 6.4 (the package builds in the Swift 6 language mode with tools version 6.3, as the official template does; Vapor 4.122.2 itself needs tools version 6.0) with swift-nio 2.104.0, checked on 2026-10-17. Every Swift listing on this page was compiled in one SwiftPM package (an executable target, a library target for the provider-style example and a Swift Testing target) with zero warnings, and the six tests shown passed. Vapor 5 is at 5.0.0-beta.4 and is not released yet; this page does not claim anything about its service model beyond what is said in the version notes. The page extends the Bookshelf service of the earlier pages of this section. Swift itself is covered by the Swift Reference; the Extensions and Nested Types, Protocols and Actors, Isolation and Sendable pages explain the language features used below.

What "services" means in Vapor 4

Handlers need things that outlive a request: a settings value, a database pool, an HTTP client, a cache, a repository. Vapor 4 has no dependency-injection container: no registration step by protocol, no make call, no scopes. Instead it gives you two plain Swift mechanisms and one convention:

  • Storage: Application (and Request) carry a typed key-value map, storage, where the key is a Swift type and the value is whatever that key says it is.

  • Extensions: you add a computed property to Application or Request that reads and writes the map, and handlers use that property (req.books, app.auditLog). Everything Vapor itself ships follows the same shape, from app.routes to app.http.client to req.cache.

  • The convention is to program against a protocol, store a factory for the implementation, and replace the factory in tests. This is dependency injection by composition, and it needs no framework.

Application.storage holds typed keys; computed properties on Application and Request read them; handlers use the properties; tests replace the factory

Where it runs in the request

In the request lifecycle (SwiftNIO accepts the connection and parses HTTP, Vapor builds a Request, the responder chain runs the middleware, the router matches a route, the handler runs, the returned value is encoded into the Response), services are configured before the first request, in configure(:) (see Application lifecycle and entrypoint), and read during the handler step, through req.application (every Request holds a reference to the Application that created it) or through a computed property on Request that does so. Middleware reads services the same way, from request.application. A service is therefore shared by every request on every event loop thread at the same time, which is why the storage rules below are about Sendable and locking.

Extending Application with a typed value

Application.storage is a value of the struct Storage (Sources/Vapor/Utilities/Storage.swift). Its keys are types that conform to StorageKey, which has one associated type:

// Vapor 4.122.2
public protocol StorageKey {
    /// The type of the stored value associated with this key type.
    associatedtype Value: Sendable
}

The map is keyed by ObjectIdentifier(Key.self), so a key is just a type name; it is never instantiated. That is why the conventional key is a tiny private struct declared inside the extension that uses it: nobody else can read or overwrite the entry, and two unrelated packages can never collide. The Value: Sendable requirement is deliberate: Application is Sendable and shared by every thread, so what you put in it must be safe to share.

The smallest useful service is a settings value, with a default so that reading before configuration cannot crash:

import Vapor

struct BookshelfSettings: Sendable {
    var libraryName: String
    var maxLoansPerMember: Int
}

extension Application {
    private struct BookshelfSettingsKey: StorageKey {
        typealias Value = BookshelfSettings
    }

    /// Typed, defaulted access to the settings; assign it once in `configure(_:)`.
    var bookshelf: BookshelfSettings {
        get {
            storage[BookshelfSettingsKey.self]
                ?? BookshelfSettings(libraryName: "Bookshelf", maxLoansPerMember: 5)
        }
        set {
            storage[BookshelfSettingsKey.self] = newValue
        }
    }
}

extension Request {
    /// Read-only shortcut for handlers.
    var bookshelf: BookshelfSettings {
        application.bookshelf
    }
}

Three things are worth reading in that listing:

  • The key is private and sits inside extension Application; the property is the only door to the entry.

  • Request.bookshelf forwards to application.bookshelf. A handler therefore writes req.bookshelf.libraryName and never needs to know where the value lives. Vapor does the same for req.logger, req.cache and req.client.

  • BookshelfSettings is a struct whose stored properties are Sendable, so it satisfies Value: Sendable with an explicit conformance. The settings are assigned once in configure(_:) and only read afterwards, which is the safest shape for a stored value.

Reading is app.storage[Key.self], which returns Key.Value?; storage.get(Key.self) is the same call and storage.contains(Key.self) tests for presence without reading. Assigning nil removes the entry (and runs its shutdown closure, if it has one).

Reading the services from a handler

Settings are used like any other property; the Bookshelf routes at the end of this page start with:

app.get("library") { req async -> String in
    "\(req.bookshelf.libraryName) (max \(req.bookshelf.maxLoansPerMember) loans)"
}

The compile-time rules: Sendable and StorageKey

Because Value must be Sendable, the compiler rejects a key whose value cannot be shared. In the Swift 6 language mode this is an error, not a warning:

final class Counter {          // not Sendable: a mutable class
    var value = 0
}

extension Application {
    struct CounterKey: StorageKey {
        typealias Value = Counter
    }
}
error: type 'Application.CounterKey.Value' (aka 'Counter') does not conform to the 'Sendable' protocol
note: class 'Counter' does not conform to the 'Sendable' protocol

The ways out are the usual ones, in order of preference: store an immutable value (struct of Sendable fields); store an actor; store a final class that is Sendable because every stored property is a let of a Sendable type or is protected by a lock such as NIOLockedValueBox (AuditLog below); and only as a last resort mark the class @unchecked Sendable after proving the safety by hand. The page Swift concurrency in Vapor shows each of these in depth.

The second compile-time rule is that the key must conform to StorageKey (and, for locks, to LockKey):

error: subscript 'subscript(_:)' requires that 'Missing' conform to 'StorageKey'
error: instance method 'lock(for:)' requires that 'Request' conform to 'LockKey'

Mutable and lazily created services: app.locks

app.storage is not safe for read-modify-write sequences. Both its getter and its setter take a lock, but each only for the instant of the copy, and app.storage[Key.self] = value is a get, a mutation of the copy and a set. Two threads that do "if missing, create and store" at the same time can both create a value, and one of them is silently lost, or can lose other writes made in between. Vapor’s own lazily created services guard this with app.locks (the HTTP Client page shows app.http.client built this way):

  • app.locks.main (also available as app.sync) is one global NIOLock;

  • app.locks.lock(for: Key.self) returns a NIOLock that belongs to the key type, creating it on first use under the main lock; the key must conform to LockKey, which is an empty marker protocol (public protocol LockKey {}).

Vapor’s in-memory cache and its HTTP client use exactly this: the same private struct conforms to both StorageKey and LockKey, and the lazy getter takes the key’s lock around "get, or create and set".

The same pattern for an application service that is created on first use and flushed at shutdown:

import NIOConcurrencyHelpers
import Vapor

final class AuditLog: Sendable {
    private let entries = NIOLockedValueBox<[String]>([])

    func record(_ entry: String) {
        entries.withLockedValue { $0.append(entry) }
    }

    var snapshot: [String] {
        entries.withLockedValue { $0 }
    }

    func flush(to logger: Logger) {
        let pending = entries.withLockedValue { $0 }
        logger.info("Flushing audit log", metadata: ["entries": "\(pending.count)"])
    }
}

extension Application {
    private struct AuditLogKey: StorageKey, LockKey {
        typealias Value = AuditLog
    }

    /// Created on first use. The per-key lock makes "check, create, store" atomic.
    var auditLog: AuditLog {
        locks.lock(for: AuditLogKey.self).withLock {
            if let existing = storage[AuditLogKey.self] {
                return existing
            }
            let created = AuditLog()
            let logger = self.logger
            storage.set(AuditLogKey.self, to: created, onShutdown: { log in
                log.flush(to: logger)
            })
            return created
        }
    }
}

The test auditLogIsCreatedOnceUnderConcurrency reads app.auditLog from 50 concurrent tasks and asserts that all of them received the same instance. Lock for a key only when the service is created lazily or mutated after startup; a value assigned once in configure(_:) and then only read needs no lock, because configuration finishes before the server accepts connections.

Shutdown closures

storage.set(_:to:onShutdown:) takes an optional @Sendable closure that Vapor calls when the application shuts down (or when you remove the key by assigning nil). app.asyncShutdown() runs the closure of every entry and then clears the map; clear() on its own does not run them. Because set is marked unavailable from asynchronous contexts, use await storage.setWithAsyncShutdown(Key.self, to: value, onShutdown: { value in ... }) when you configure in an async function and the cleanup is itself asynchronous (closing a connection pool, flushing a client): asyncShutdown() prefers the async closure and falls back to the synchronous one. A shutdown closure that throws is caught and logged as a warning; it never stops the other entries from shutting down.

Applications can also hook the lifecycle with a LifecycleHandler (app.lifecycle.use(handler)) that implements willBootAsync, didBootAsync and shutdownAsync; that is the tool for work that is not "one value per key", such as warming a cache at boot. The lifecycle phases are described on the lifecycle page.

Request-scoped storage

Request has its own storage, a Storage value with the same API, that lives only as long as the request. It is how a middleware passes a computed value to the handler without a global: a middleware stores the authenticated user or the request ID, and a computed property on Request reads it back. Vapor’s own authentication cache (req.auth) and req.session are stored this way. Do not put an Application service there, and do not store a Request in app.storage: that leaks the request (and its body and logger) for the life of the process. The Request itself is Sendable and holds req.application and req.logger; see Request and response for the rest.

The repository pattern: protocol, factory, req.books

Settings are data. Behaviour (read the catalogue, create a book) is where dependency injection pays: handlers should depend on what a book store does, not on how (memory, Fluent, a remote service). The pattern has four parts:

  1. a protocol that describes the capability and is Sendable;

  2. one or more implementations;

  3. a factory stored in app.storage that builds an implementation for a given request;

  4. a computed property on Request that calls the factory.

import Vapor

struct Book: Content, Equatable {
    var id: UUID?
    var title: String
    var author: String
}

protocol BookRepository: Sendable {
    func all() async throws -> [Book]
    func find(_ id: UUID) async throws -> Book?
    func create(title: String, author: String) async throws -> Book
}

actor InMemoryBookRepository: BookRepository {
    private var books: [UUID: Book] = [:]

    func all() async throws -> [Book] {
        books.values.sorted { $0.title < $1.title }
    }

    func find(_ id: UUID) async throws -> Book? {
        books[id]
    }

    func create(title: String, author: String) async throws -> Book {
        let book = Book(id: UUID(), title: title, author: author)
        books[book.id!] = book
        return book
    }
}

/// How the application builds a repository for a request.
struct BookRepositoryFactory: Sendable {
    let make: @Sendable (Request) -> any BookRepository
}

extension Application {
    private struct BookRepositoryFactoryKey: StorageKey {
        typealias Value = BookRepositoryFactory
    }

    var bookRepositoryFactory: BookRepositoryFactory? {
        get { storage[BookRepositoryFactoryKey.self] }
        set { storage[BookRepositoryFactoryKey.self] = newValue }
    }
}

extension Request {
    var books: any BookRepository {
        guard let factory = application.bookRepositoryFactory else {
            fatalError("No BookRepository configured. Set app.bookRepositoryFactory in configure(_:).")
        }
        return factory.make(self)
    }
}

Why a factory and not the repository itself? A factory closure receives the Request, so an implementation can take req.db, req.logger or req.client and be constructed per request (a Fluent repository, see Fluent Models, is a thin wrapper around req.db). A shared implementation, like the in-memory actor here, is simply returned from a closure that captured it: BookRepositoryFactory { _ in repository }. The factory is a struct with a single @Sendable closure so that it satisfies Value: Sendable.

fatalError in Request.books mirrors Vapor’s own services (req.cache stops with "No cache configured. Configure with app.caches.use(…​)" when none was chosen): a missing service is a programming error that must show up in the first request of the first test, not a runtime condition to recover from.

The wiring is a few lines of configure(_:) and the handlers know only the protocol:

import BookshelfStore
import Vapor

func configure(_ app: Application) async throws {
    app.bookshelf = BookshelfSettings(libraryName: "City Library", maxLoansPerMember: 3)

    let repository = InMemoryBookRepository()
    app.bookRepositoryFactory = BookRepositoryFactory { _ in repository }

    app.bookStores.use(.inMemory)

    try routes(app)
}
import BookshelfStore
import Vapor

struct CreateBook: Content {
    var title: String
    var author: String
}

struct BooksController: RouteCollection {
    func boot(routes: any RoutesBuilder) throws {
        let books = routes.grouped("books")
        books.get(use: index)
        books.post(use: create)
    }

    @Sendable func index(req: Request) async throws -> [Book] {
        req.application.auditLog.record("list")
        return try await req.books.all()
    }

    @Sendable func create(req: Request) async throws -> Response {
        let input = try req.content.decode(CreateBook.self)
        let book = try await req.books.create(title: input.title, author: input.author)
        req.application.auditLog.record("create \(book.title)")
        return try await book.encodeResponse(status: .created, for: req)
    }
}

func routes(_ app: Application) throws {
    app.get("library") { req async -> String in
        "\(req.bookshelf.libraryName) (max \(req.bookshelf.maxLoansPerMember) loans)"
    }

    app.get("titles") { req async throws -> [String] in
        try await req.bookStore.titles()
    }

    try app.register(collection: BooksController())
}

BooksController is a RouteCollection, exactly as on the controllers page (Controllers and route collections); the only change is that the handlers call req.books instead of reaching into a global. @Sendable on the handler methods is what lets them be passed to books.get(use:) in Swift 6 mode.

Test doubles

Because the handlers depend on the protocol, a test (see Testing) replaces the factory after configure(_:) ran and before the first request: no mocking library, no reflection. The two doubles below are a stub with fixed data and a fake that always fails, which is the only practical way to test the 503 path without breaking a real dependency:

import BookshelfStore
import Testing
import VaporTesting

@testable import App

/// A test double: a fixed catalogue, no state.
struct StubBookRepository: BookRepository {
    let stored: [Book]
    func all() async throws -> [Book] { stored }
    func find(_ id: UUID) async throws -> Book? { stored.first { $0.id == id } }
    func create(title: String, author: String) async throws -> Book {
        Book(id: UUID(), title: title, author: author)
    }
}

/// A test double that always fails.
struct UnavailableBookRepository: BookRepository {
    func all() async throws -> [Book] { throw Abort(.serviceUnavailable) }
    func find(_ id: UUID) async throws -> Book? { throw Abort(.serviceUnavailable) }
    func create(title: String, author: String) async throws -> Book { throw Abort(.serviceUnavailable) }
}

@Suite struct ServiceTests {
    @Test func settingsAreStored() async throws {
        try await withApp(configure: configure) { app in
            try await app.testing().test(.GET, "/library") { res async in
                #expect(res.status == .ok)
                #expect(res.body.string == "City Library (max 3 loans)")
            }
        }
    }

    @Test func stubReplacesTheRepository() async throws {
        try await withApp(configure: configure) { app in
            let stub = StubBookRepository(stored: [Book(id: nil, title: "Dune", author: "Herbert")])
            app.bookRepositoryFactory = BookRepositoryFactory { _ in stub }
            try await app.testing().test(.GET, "books") { res async throws in
                #expect(res.status == .ok)
                let books = try res.content.decode([Book].self)
                #expect(books.map(\.title) == ["Dune"])
            }
        }
    }

    @Test func failingDoubleProduces503() async throws {
        try await withApp(configure: configure) { app in
            app.bookRepositoryFactory = BookRepositoryFactory { _ in UnavailableBookRepository() }
            try await app.testing().test(.GET, "books") { res async in
                #expect(res.status == .serviceUnavailable)
            }
        }
    }

    @Test func createThenListUsesTheSharedInstance() async throws {
        try await withApp(configure: configure) { app in
            try await app.testing().test(.POST, "books", beforeRequest: { req in
                try req.content.encode(CreateBook(title: "Emma", author: "Austen"))
            }, afterResponse: { res async in
                #expect(res.status == .created)
            })
            try await app.testing().test(.GET, "books") { res async throws in
                #expect(try res.content.decode([Book].self).count == 1)
            }
            #expect(app.auditLog.snapshot == ["create Emma", "list"])
        }
    }

    @Test func auditLogIsCreatedOnceUnderConcurrency() async throws {
        try await withApp(configure: configure) { app in
            let logs = await withTaskGroup(of: ObjectIdentifier.self) { group in
                for _ in 0..<50 {
                    group.addTask { ObjectIdentifier(app.auditLog) }
                }
                var ids = Set<ObjectIdentifier>()
                for await id in group { ids.insert(id) }
                return ids
            }
            #expect(logs.count == 1)
        }
    }

    @Test func providerStyleStore() async throws {
        try await withApp(configure: configure) { app in
            try await app.bookStores.store.add(title: "Ulysses")
            try await app.testing().test(.GET, "titles") { res async throws in
                #expect(try res.content.decode([String].self) == ["Ulysses"])
            }
        }
    }
}

Points to notice in the test target:

  • withApp(configure: configure) is VaporTesting’s own helper (Sources/VaporTesting/withApp.swift): it makes an application in the .testing environment, runs your configure, runs the test body and always calls asyncShutdown(), even when the body throws. Calling it as plain withApp { ... } skips configure and every route answers 404, which is a confusing failure. A hand-written helper with the same name as this one competes with it in overload resolution (in the scratch package, a first test that called such a helper got 404 for a route that existed), so give your own helper a different name.

  • app.bookRepositoryFactory = BookRepositoryFactory { _ in stub } is the whole injection: the controller code is the production code.

  • The factory is read on every request (req.books calls it each time), which is why replacing it between requests works. A service read once and cached in a local let at startup could not be swapped.

  • The audit log assertion shows that a service created by app.auditLog during one request is the same object in the next, and that the order of recorded entries is the order of the requests.

Run the suite with swift test; a green run prints one line per test and a summary:

swift test
✔ Test settingsAreStored() passed
✔ Test stubReplacesTheRepository() passed
✔ Test failingDoubleProduces503() passed
✔ Test createThenListUsesTheSharedInstance() passed
✔ Test auditLogIsCreatedOnceUnderConcurrency() passed
✔ Test providerStyleStore() passed
✔ Test run with 6 tests in 1 suite passed

(The order of the lines varies, because Swift Testing runs the tests in parallel; each test has its own Application, so they cannot interfere.)

Provider-style packages

When the same service is used by several applications, or you want to publish it, put the abstraction and the wiring in a separate SwiftPM library target. The shape that Vapor and its ecosystem use (app.caches.use(.memory), app.sessions.use(.fluent) (see Sessions and Cookies), app.queues.use(.redis(...)) (see Queues & Background Jobs), app.databases.use(...), see Fluent Overview and Database Drivers and MongoDB) is:

  • an Application extension exposing a small struct, app.bookStores, that holds the Application;

  • a nested Provider struct wrapping a @Sendable (Application) -> Void closure, and static members on it (.inMemory, .fluent, .remote(url:)) that configure the application;

  • use(_:) overloads that store the choice in app.storage under a private key;

  • a computed property on Request for use in handlers.

public import Vapor

/// The abstraction application code depends on.
public protocol BookStore: Sendable {
    func titles() async throws -> [String]
    func add(title: String) async throws
}

/// A trivial in-memory implementation shipped with the package.
public actor InMemoryBookStore: BookStore {
    private var storedTitles: [String]

    public init(titles: [String] = []) {
        self.storedTitles = titles
    }

    public func titles() async throws -> [String] { storedTitles }

    public func add(title: String) async throws { storedTitles.append(title) }
}

extension Application {
    /// `app.bookStores`: the entry point of the package, mirroring `app.caches` and `app.sessions`.
    public var bookStores: BookStores { .init(application: self) }

    public struct BookStores: Sendable {
        /// A named way of configuring the package: `app.bookStores.use(.inMemory)`.
        public struct Provider: Sendable {
            let run: @Sendable (Application) -> Void

            public init(_ run: @escaping @Sendable (Application) -> Void) {
                self.run = run
            }
        }

        private struct Registration: Sendable {
            let store: any BookStore
        }

        private struct Key: StorageKey {
            typealias Value = Registration
        }

        let application: Application

        public func use(_ provider: Provider) {
            provider.run(application)
        }

        public func use(_ store: any BookStore) {
            application.storage[Key.self] = Registration(store: store)
        }

        public var store: any BookStore {
            guard let registration = application.storage[Key.self] else {
                fatalError("No BookStore configured. Configure one with app.bookStores.use(...) in configure(_:).")
            }
            return registration.store
        }
    }
}

extension Application.BookStores.Provider {
    public static var inMemory: Self {
        .init { app in
            app.bookStores.use(InMemoryBookStore())
        }
    }
}

extension Request {
    /// `req.bookStore`: what handlers use.
    public var bookStore: any BookStore {
        application.bookStores.store
    }
}

The application’s configure(_:) then has one line, app.bookStores.use(.inMemory), and swapping the implementation is a change to that line. Another package can add its own static member (a remote(url:) provider, say) in an extension of Application.BookStores.Provider without touching this one, which is why the Provider initialiser is public. Notes for library authors:

  • With InternalImportsByDefault enabled (the official template does), a public API that mentions Vapor types needs public import Vapor, as the first line of the listing shows.

  • Keep stored configuration in a Sendable value, as Registration does, and let use(_:) take the finished service (here an any BookStore) so that no closure captures the Application.

  • Expose the provider through app.<name> and req.<name>, so that users write the same two spellings as for every Vapor service.

From Vapor 2 Providers and Vapor 3 Services to this model

The older books describe two earlier mechanisms. In Vapor 2 a Provider was added to the Droplet (addProvider) and configured through JSON files in a Config/ folder; in Vapor 3 services were registered in a Services value, resolved with make(_:) from a Container (the application, a request or a worker), and a Provider protocol had register(_:) and didBoot(_:) hooks. Vapor 4 removed the container: registration is app.storage plus an extension property, resolution is calling that property, and "provider" survived only as a naming convention (the struct above). The configuration files of Vapor 2 are replaced by environment variables and typed settings, covered in Configuration and Environment (a typed AppConfig kept in app.storage).

Common errors and pitfalls

  • Registering after the server started. Assigning storage from a handler works but races with other requests. Configure in configure(_:), or guard with app.locks as in AuditLog.

  • Forgetting to configure. fatalError("No BookRepository configured...") in the first request. Configure in configure(_:), and in tests call withApp(configure: configure).

  • Mutating a stored struct and expecting the store to change. var s = app.bookshelf; s.maxLoansPerMember = 1 changes a copy; assign app.bookshelf = s back.

  • A non-Sendable value (compile error shown above). Prefer an actor or an immutable struct over @unchecked Sendable.

  • Holding a lock across an await. NIOLock.withLock takes a synchronous closure on purpose; do the asynchronous work outside it, or use an actor.

  • Bare String or Int values. Keys are distinguished by key type, so two keys with the same Value type do not clash, but a String entry hides what it is. Wrap the value in a named struct.

  • Using Request as a service locator. A global computed property on Request that reads from app.storage is fine; a property that builds a new connection pool per request is not, because it ignores the fact that a request is short-lived and the pool is not.

  • Capturing the request in a long-lived closure. A factory stored on the application must not capture a Request; it receives one as a parameter.

Version notes and where the books are outdated

  • In 4.122.2 StorageKey.Value is required to be Sendable and shutdown closures are @Sendable, so a key whose value is a plain mutable class compiles in Swift 5 mode (at most with a warning) but fails in the Swift 6 language mode. The asynchronous setWithAsyncShutdown and asyncShutdown() belong to the async/await API of Vapor 4; the synchronous shutdown() is marked unavailable from async contexts. The release in which each requirement first appeared was not verified for this page.

  • Vapor 2 (Patel) used Providers and the Config/ folder; Vapor 3 (the Kodeco-era books) used Services, Container.make and register(_:) on Provider; none of these exists in Vapor 4 and none of the code compiles.

  • The consulted books use EventLoopFuture in repository protocols (func all() -> EventLoopFuture<[Book]>); with async/await the protocol methods are async throws, as in this page.

  • The official 4.0 services page shows only the extension-plus-StorageKey basics; the protocol-and-factory pattern and the test doubles here are the community convention, which is why the page gives them as examples and not as an API.

  • Vapor 5 is at 5.0.0-beta.4 and is not released; whether Application.storage stays in its present form was not verified for this page. Treat the concept (a typed registry on the application plus protocol-based injection) as the durable part, and review What’s New, Upgrading and Vapor 5 before migrating a large set of providers.