Skill: DataSource
Guide for creating DataSources following the Repository pattern.
- •RemoteDataSource: Consumes REST APIs via HTTPClient
- •MemoryDataSource: In-memory storage using actors for thread safety
When to use this skill
- •Create a new RemoteDataSource to consume an API
- •Create a new MemoryDataSource for in-memory caching
- •Create DTOs for API responses
Additional resources
- •For complete implementation examples, see examples.md
File structure
Features/{Feature}/
├── Sources/
│ └── Data/
│ ├── DataSources/
│ │ ├── {Name}RemoteDataSource.swift
│ │ └── {Name}MemoryDataSource.swift
│ └── DTOs/
│ └── {Name}DTO.swift
└── Tests/
├── Data/
├── Fixtures/
│ ├── {name}.json
│ └── {name}_list.json
└── Mocks/
RemoteDataSource Pattern
Contract
protocol {Name}RemoteDataSourceContract: Sendable {
func fetch{Name}(id: Int) async throws -> {Name}DTO
}
Rules: Internal visibility, Sendable, async throws, return DTOs
Implementation
struct {Name}RemoteDataSource: {Name}RemoteDataSourceContract {
private let httpClient: HTTPClientContract
init(httpClient: HTTPClientContract) {
self.httpClient = httpClient
}
func fetch{Name}(id: Int) async throws -> {Name}DTO {
let endpoint = Endpoint(path: "/{resource}/\(id)")
return try await httpClient.request(endpoint)
}
}
DTO (Data Transfer Object)
"A Data Transfer Object is one of those objects our mothers told us never to write. It's often little more than a bunch of fields and the getters and setters for them." — Martin Fowler, PoEAA
DTOs are intentionally anemic - they exist purely to transfer data between systems.
struct {Name}DTO: Decodable, Equatable, Sendable {
let id: Int
let name: String
}
Rules:
- •
Decodable,Equatable,Sendable - •Internal visibility
- •Properties match JSON keys
- •NO behavior - only data (anemic by design)
- •NO
toDomain()methods - mapping belongs in the Repository
Why anemic?
- •DTOs represent the structure of an external system (the API)
- •If we add logic, it couples to the external API structure
- •The Repository is responsible for translating DTOs to Domain models
MemoryDataSource Pattern
Contract
protocol {Name}MemoryDataSourceContract: Sendable {
// MARK: - Single Item (Detail)
func get{Name}Detail(identifier: Int) async -> {Name}DTO?
func save{Name}Detail(_ item: {Name}DTO) async
func delete{Name}Detail(identifier: Int) async
// MARK: - Paginated Results (optional)
func getPage(_ page: Int) async -> {Name}sResponseDTO?
func savePage(_ response: {Name}sResponseDTO, page: Int) async
}
Rules:
- •
async(no throws), return optional for get - •Use
Detailsuffix for single-item methods to distinguish from list operations - •
identifierparameter name (notid) for consistency
Implementation (Actor)
actor {Name}MemoryDataSource: {Name}MemoryDataSourceContract {
private var items: [Int: {Name}DTO] = [:]
private var pages: [Int: {Name}sResponseDTO] = [:]
// MARK: - Single Item
func get{Name}Detail(identifier: Int) -> {Name}DTO? { items[identifier] }
func save{Name}Detail(_ item: {Name}DTO) { items[item.id] = item }
func delete{Name}Detail(identifier: Int) { items.removeValue(forKey: identifier) }
// MARK: - Paginated Results
func getPage(_ page: Int) -> {Name}sResponseDTO? { pages[page] }
func savePage(_ response: {Name}sResponseDTO, page: Int) { pages[page] = response }
}
Rules: Use actor, dictionary storage, no async in signatures
JSON Fixtures
Tests use JSON files replicating real API responses.
Location: Tests/Fixtures/
Naming: snake_case (character.json, character_list.json)
Loading in tests:
Each test file defines a private extension to wrap Bundle.module access:
private extension {Name}DataSourceTests {
func loadJSON<T: Decodable>(_ filename: String) throws -> T {
try Bundle.module.loadJSON(filename)
}
func loadJSONData(_ filename: String) throws -> Data {
try Bundle.module.loadJSONData(filename)
}
}
Import {AppName}CoreMocks for Bundle+JSON helper.
Visibility Summary
| Component | Visibility | Location |
|---|---|---|
| Contract | internal | Sources/Data/DataSources/ |
| Implementation | internal | Sources/Data/DataSources/ |
| DTO | internal | Sources/Data/DTOs/ |
| Mocks | internal | Tests/Mocks/ |
Checklists
RemoteDataSource
- • Create DTO with
nonisolated - • Create Contract with
async throws - • Create Implementation injecting HTTPClientContract
- • Create Mock in
Tests/Mocks/ - • Create JSON fixtures in
Tests/Fixtures/ - • Create tests using JSON fixtures
MemoryDataSource
- • Create Contract with
asyncmethods - • Create
actorImplementation - • Create
final classMock with@unchecked Sendableand call tracking - • Create tests