When I first published ExtremePackaging, the goal was simple β to prove that highly modular Swift architectures can scale across platforms while keeping build times fast, dependencies isolated, and the mental model crystal clear.
But then came the next step: Could this same architecture host a complete OpenAPI workflow β with generated clients, servers, and full middleware integration β without losing its elegance?
Thatβs how the new reference implementation, swift-openapi-extremepackaging-example, was born.
π§© The Foundation β ExtremePackaging
The original ExtremePackaging repository introduced a clean, layered structure:
- Shared packages for models and protocols
- Independent feature modules for UI, data, and networking
- No cross-package leaks
- Unified Xcode workspace that felt like a monolith but built like microservices
The guiding philosophy: Each feature should be an island, communicating only through well-defined contracts.
That foundation made it ideal for integrating OpenAPI-generated code β which naturally fits into modular boundaries like SharedApiModels, ApiClient, and ApiServer.
π Evolving Toward OpenAPI
The OpenAPI version added an entire new layer of automation and functionality β transforming a static architecture into a living, self-describing API ecosystem.
1. OpenAPI Schema & Code Generation
At the heart of the project is the OpenAPI specification (openapi.yaml) β defining endpoints, models, and responses for a DummyJSON-compatible API.
Newly introduced elements:
- π§± Full schema definitions for
Users,Posts,Products,Todos, andCarts - π§© Error schemas (404, validation, authentication)
- βοΈ Integration with Swift OpenAPI Generator
- π Automatic generation of clients, models, and server stubs
- π¦ Separation of generated code into
SharedApiModels
This turned the architecture into a self-contained API ecosystem β one that can generate, serve, and consume its own endpoints.
2. API Server Implementation
A complete Vapor-based local server (ApiServer) was added to simulate real-world backend behavior:
- 17 endpoints fully implemented from the OpenAPI spec
- Realistic mock data mirroring DummyJSON
- Pagination and validation logic
- Centralized error responses
- π₯οΈ Prefixed logging for easy tracing in the console
The server runs locally at http://localhost:8080, serving as both a mock backend and a test harness for the generated client.
3. Enhanced API Client Architecture
The client evolved from a simple abstraction into a fully concurrent, actor-based networking layer.
Highlights
ApiClientactor manages shared state safely across async contexts- Middleware chain introduced: Logging β Authentication β Correction
- Runtime environment switching between
.production,.local, and.mock - Shared singleton
ApiClientStatestores token, settings, and preferences
Example flow:
try await ApiClient.initializeShared(environment: .production)
let client = ApiClient.shared!
let auth = try await client.login(username: "emilys", password: "emilyspass")
await ApiClient.setToken(auth.accessToken)
let users = try await client.getUsers(limit: 10)
A clear separation between environment configuration and runtime state ensures deterministic, thread-safe behavior.
4. Middleware Integration
The client leverages two reusable middlewares from sibling packages:
- OpenAPILoggingMiddleware
Provides structured, console + JSON logging with full request/response capture. - BearerTokenAuthMiddleware
Manages JWT token injection with a concurrency-safe actor and public operation rules.
Together, they demonstrate the power of middleware chaining in OpenAPI Runtime β clean, modular extensions without inheritance or global state.
5. YAMLMerger β The Key to Structured API Specs
The project uses YamlMerger β a Swift package that merges multiple YAML files into a single combined OpenAPI specification.
If your project doesnβt already include an openapi.yaml, YamlMerger ensures you have one β and helps you maintain a structured, predictable folder layout under Tests/ or Sources/SharedApiModels/schemas/.
π§ Why It Must Be Copied into the Project
YamlMerger cannot simply be added as a SwiftPM dependency for build-time merging because of SPMβs read-only resolution model:
- Swift Package Manager stores dependencies in a cached, read-only location (
.build/checkouts/). - The OpenAPI generator, however, needs write access to output the merged
openapi.yamlfile directly into your source tree. - SPM build scripts are not allowed to write to source folders outside their sandboxed build directory.
β
Solution: Copy the YamlMerger executable directly into your project (e.g. Tools/YamlMerger/) and call it from a pre-build script or CI pipeline.
This guarantees write permissions and makes the tool available to everyone checking out the repo.
π§© What It Does
YamlMerger scans subdirectories (01 β 08) and merges YAML fragments in deterministic order:
- Folders are processed numerically.
__*.yamlfiles merge first within each folder.- Remaining files merge alphabetically.
- The final output is a complete OpenAPI spec, suitable for Swift OpenAPI Generator.
π§± Example Schema Layout
Schema/
βββ 01_Info/
βββ 02_Servers/
βββ 03_Tags/
βββ 04_Paths/
βββ 05_Webhooks/
βββ 06_Components/
βββ 07_Security/
βββ 08_ExternalDocs/
Each folder corresponds to a section of the OpenAPI spec, allowing multiple developers to work on different endpoints, schemas, or components without conflicts.
βοΈ Typical Workflow
# Merge schemas before build
./Tools/YamlMerger merge --input Sources/SharedApiModels/schemas/ --output Sources/SharedApiModels/openapi.yaml
You can run this manually, in a pre-build phase, or as part of CI/CD automation.
π‘ Pro Tip
If your project starts without an openapi.yaml, placing schema fragments in structured folders under Tests/ ensures your API structure remains organized β even before full code generation.
YamlMerger gives your tests (and your teammates) a shared, visual map of your APIβs evolving shape.
6. Test Coverage Expansion
Two new test suites validate both local and production APIs:
- π§ͺ
ApiClientLocalTests.swiftβ 25 tests targeting the local Vapor server - π
ApiClientProductionTests.swiftβ 29 integration tests against DummyJSON API
Tests cover:
- Authentication and token persistence
- Pagination behavior
- Error responses and invalid IDs
- Concurrent request handling
Together they form a 54-test safety net proving both architecture and OpenAPI compliance.
π§ Architecture Snapshot
Packages/
βββ Sources/
β βββ ApiClient/
β βββ ApiServer/
β βββ SharedApiModels/
βββ Tests/
βββ ApiClientTests/
Each target is self-contained β just like in the original ExtremePackaging β but now with full OpenAPI integration, client/server symmetry, and end-to-end testability.
β‘ Key Improvements Over ExtremePackaging
| Area | Before | After |
| API Definition | Manual protocol layer | Generated OpenAPI spec |
| Networking | Custom client | Actor-based client w/ middlewares |
| Server | None | Vapor mock server (17 endpoints) |
| Authentication | Static token | BearerTokenAuthMiddleware |
| Logging | Simple print logs | Structured OpenAPILoggingMiddleware |
| Testing | Minimal unit tests | Full integration tests (54 total) |
| Schema Management | Handwritten | Modular YAML + YamlMerger |
| Tooling | Swift only | Swift + OpenAPI toolchain |
π§ Lessons Learned
- OpenAPI fits perfectly into modular Swift architectures β generated code belongs in its own layer, and SwiftPM makes that separation effortless.
- Actors are the future of shared state β simple, safe, and transparent.
- Middleware > Managers β function composition scales better than class hierarchies.
- Automation beats documentation β with OpenAPI, the spec is the documentation.
π¬ Closing Thoughts
This evolution of ExtremePackaging into a full OpenAPI reference app is more than a demo β itβs a blueprint for modular API-driven development in Swift.
From YAML schemas to live servers and typed clients, everything now exists in one unified, testable ecosystem β powered by Appleβs official OpenAPI tools and guided by the ExtremePackaging philosophy.
π Explore the project: swift-openapi-extremepackaging-example
βArchitecture should scale not by adding layers, but by removing assumptions.β