This guide explains the Server layer in application/Sources/Server.
It focuses on two things:
- How the server is assembled and built.
- How the configuration layer feeds that build process.
By the end of this guide, you will:
- Understand the startup flow from
main()to a running HTTP server. - Understand where server configuration is loaded and mapped.
- Understand what each folder in
Sources/Serveris responsible for.
- Assembly starts in
Entrypoint.swift, then callsServerConfigLoader,buildServer, andbuildRouter. AppInfrastructureandAppModuleswire runtime dependencies into use-case builders.Routes+Build.swiftregisters App and Admin OpenAPI handlers plus middleware.- The configuration layer is small and explicit, based on scoped config readers.
Server startup path:
Entrypoint.main
|
v
ServerConfigLoader.load
|
v
buildServer(config)
|
+--> build logger + postgres client + database client
+--> build AppInfrastructure
+--> build AppModules
+--> buildRouter(modules)
|
v
Application.runService
Snippet (Entrypoint.swift):
@main
struct Entrypoint {
static func main() async throws {
let config = try await ServerConfigLoader().load()
let server = try await buildServer(config: config)
try await server.runService()
}
}What this does:
- Loads config first.
- Builds the server application object.
- Starts the service loop.
The Server configuration layer lives in Sources/Server/Environment.
ServerConfig.swift: the in-memory config model used by server build.ServerConfigLoader.swift: reads config values from environment/config files and buildsServerConfig.
Snippet (ServerConfigLoader.swift):
func load() async throws -> ServerConfig {
let reader = try await environmentLoader.loadConfigReader(
defaultEnvironmentFilePrefix: "server"
)
let system = systemConfigLoader.load(reader: reader)
let serverHTTPReader = reader.scoped(to: "server.http")
return .init(
host: serverHTTPReader.string(forKey: "host", default: "127.0.0.1"),
port: serverHTTPReader.int(forKey: "port", default: 8080),
serverName: reader.string(forKey: "serverName"),
system: system
)
}Why this matters:
- Config is centralized and explicit.
- Defaults are visible in one place.
- The build layer receives one typed object (
ServerConfig).
The composition layer wires infrastructure and modules.
AppInfrastructure is the shared dependency bag passed to module builders.
struct AppInfrastructure: Sendable {
let database: any DatabaseClient
let idGenerator: any IDGenerator
}AppModules constructs typed module builders (system, user, auth) from shared infrastructure.
struct AppModules: Sendable {
let system: SystemBackend
let user: UserBackend
let auth: AuthModule
}Each module builder composes use-cases from executors + scopes.
Snippet (SystemBackend.makeAddVariable()):
func makeAddVariable() -> AddVariable {
let transaction = DatabaseTransactionExecutor(
database: infrastructure.database,
scope: { connection in
WriteVariable(
variable: VariableDatabaseRepository(connection: connection)
)
}
)
return .init(
transaction: transaction,
idGenerator: infrastructure.idGenerator
)
}This is the core assembly style used throughout Server.
Routes+Build.swift connects route handlers and middleware.
Main responsibilities:
- Create base router.
- Register shared middleware (logging, CORS).
- Register utility routes (
/health). - Register App and Admin OpenAPI handlers.
Snippet (trimmed):
let appAPI = AppAPI(modules: modules)
try appAPI.registerHandlers(
on: router,
middlewares: [
ErrorMiddleware(),
UnescapeHTTPHeadersMiddleware(),
]
)
let adminAPI = AdminAPI(modules: modules)
try adminAPI.registerHandlers(
on: router,
middlewares: [
ErrorMiddleware(),
UnescapeHTTPHeadersMiddleware(),
]
)This section describes the main server components, their purpose, and how they work together.
Purpose: server-specific config model and loading.
- Converts raw config sources into typed runtime values.
- Keeps startup code clean by isolating config parsing.
How it works (short snippet):
let serverHTTPReader = reader.scoped(to: "server.http")
let host = serverHTTPReader.string(forKey: "host", default: "127.0.0.1")
let port = serverHTTPReader.int(forKey: "port", default: 8080)Purpose: composition of feature module builders.
AppInfrastructure.swift: shared runtime dependencies.AppModules.swift: module builder aggregator.Builders/*.swift: per-feature use-case assembly.
How it works (short snippet):
self.system = .init(infrastructure: infrastructure)
self.user = .init(infrastructure: infrastructure)
self.auth = .init(infrastructure: infrastructure)Purpose: OpenAPI operation handlers.
APIs/App/: app-facing API endpoints.APIs/Admin/: management/admin endpoints.- Subfolders split endpoints by feature (
Auth,User,System, etc.). - API protocols are generated by Swift OpenAPI Generator from OpenAPI definition files (
.yaml/.json). - The OpenAPI definition is the shared contract used by both server and client apps.
How it works (short snippet):
struct AppAPI: APIProtocol {
let modules: AppModules
}Example handler implementation (system.variable get endpoint):
func systemVariableGet(
_ input: Operations.SystemVariableGet.Input
) async throws -> Operations.SystemVariableGet.Output {
let useCase = modules.system.makeGetVariable()
let result = try await useCase.execute(
.init(id: input.path.systemVariableId)
)
return .ok(
.init(
body: .json(map(result))
)
)
}What this handler does:
- Receives the generated OpenAPI operation input type.
- Builds the matching use-case from the module builder.
- Executes business logic using path parameters.
- Maps result DTO into the generated OpenAPI response schema.
- Returns a typed
.okOpenAPI output.
Purpose: cross-cutting request/response behavior.
ErrorMiddleware.swift: maps errors to HTTP responses.UnescapeHTTPHeadersMiddleware.swift: decodes/validates select response headers.AuthMiddleware.swift,SessionSlidingExpirationMiddleware.swift: currently mostly commented scaffolding for future auth/session policy wiring.
How it works (short snippet):
response.headerFields[fieldName] = decodeAndValidate(field)Purpose: error contracts and formatting.
HTTPErrorRepresentable.swift: protocol to map errors to HTTP metadata/content.Errors/Details/*: serializable/public error detail shapes.Errors/Trace/*: nested trace model for debug trace trees.
How it works (short snippet):
try container.encode(code.code, forKey: .code)
try container.encode(message, forKey: .message)ErrorTrace is the debug tree for failures.
It keeps the error type, message, and nested causes, so one top-level failure can show the full chain behind it.
Snippet (Errors/Trace/ErrorTrace.swift, trimmed):
public struct ErrorTrace: Encodable {
public let type: Any
public let logMessage: String
public let children: [ErrorTrace]
}To plug an error into tracing, conform it to ErrorTraceRepresentable.
Example from this codebase (Variable.Error):
extension Variable.Error: ErrorTraceRepresentable {
public var underlyingErrors: [any Error] { [] }
public func trace() -> ErrorTrace {
.init(
type: type(of: self),
logMessage: String(describing: self),
children: underlyingTraces()
)
}
}HTTPErrorRepresentable maps a domain or application error to an HTTP response shape.
It tells middleware which status code, headers, and optional body should be returned.
Protocol shape (trimmed):
protocol HTTPErrorRepresentable: Error {
associatedtype Content: Encodable
var status: HTTPResponseStatus { get }
var headers: HTTPHeaders? { get }
var content: Content? { get }
}Example adaptation for Variable.Error:
extension Variable.Error: HTTPErrorRepresentable {
var status: HTTPResponseStatus { .badRequest }
var content: ServerError.Details? {
.init(
code: .badRequest,
message: "\(self)",
reason: "\(self)"
)
}
}How both pieces work together:
- Your use-case or repository throws
Variable.Error. ErrorTraceRepresentableprovides a trace tree for debug output.HTTPErrorRepresentableprovides response status/body mapping.ErrorMiddlewarereturns JSON or plain text based on request headers.
Purpose: bridge server layer to other contracts.
Adapters/Infrastructure/: concrete server-side implementations for kernel app ports (IDGenerator,PasswordHasher,MailSender).Adapters/Error/: trace adapters for domain/infrastructure error types.Adapters/DTO+Schema/: schema mapping helpers between application DTOs and OpenAPI schema objects.
These adapters connect abstract application contracts to concrete libraries.
Examples in this project:
NanoIDGeneratorimplementsIDGenerator.BcryptPasswordHasherimplementsPasswordHasher.FeatherMailSenderimplementsMailSender.
How it works (short snippet):
struct NanoIDGenerator: IDGenerator {
func generate() -> String { NanoID().rawValue }
}Why this matters:
- Use-cases depend on interfaces, not libraries.
- Server assembly chooses concrete implementations in one place.
- Swapping implementations stays localized to adapters and wiring.
Error adapters normalize different error sources into one traceable shape.
Examples:
System+Variable+Error+Trace.swiftadapts domain variable errors.DatabaseError+Trace.swiftadapts persistence errors.ServerError+Trace.swiftadapts transport/runtime errors.
These adapters make ErrorMiddleware output consistent details even when failures come from different layers.
DTO/schema adapters convert between internal application DTOs and OpenAPI request/response schema types.
Typical mapping responsibilities:
- Convert request schema -> application query/input DTO.
- Convert application detail/list DTO -> response schema.
- Map enums and pagination/sort objects safely.
In this repository, some DTO+schema files are still in-progress or commented out, but the pattern is visible in helper mapping code under APIs/Admin/AdminAPI+Helpers.swift.
Example DTO adapter snippet (schema -> application query DTO):
func map(
_ query: Components.Schemas.SystemVariableListItemSearchQuerySchema
) -> VariableList.Query {
let sort = (query.sort ?? []).map { rule in
let field: VariableList.Query.Sort.Field
switch rule.field {
case .id: field = .id
case .name: field = .name
case .value: field = .value
case .notes: field = .notes
}
return VariableList.Query.Sort(
field: field,
direction: mapSortDirection(rule.direction)
)
}
return .init(
page: map(query.page),
sort: sort,
search: query.filters.search
)
}This adapter keeps controllers thin by moving request-shape conversion into a dedicated mapping function.
HTTP/OpenAPI schema
|
v
Controller mapping helper (schema -> DTO)
|
v
Use-case call (ports only)
|
v
Infrastructure adapter (ID/hash/mail/db)
|
v
Domain/DB result
|
v
Controller mapping helper (DTO -> schema)
|
v
HTTP response
The Server layer is the outer assembly shell.
- It does not own core business rules.
- It owns runtime construction, routing, adapters, and delivery concerns.
- It translates HTTP and infrastructure concerns into module use-case calls.
If you remember this split, server changes stay clean and predictable.