SKILL.md
Skill: DataSource
Guide for creating DataSources following the Repository pattern.
Before Creating — Clarify Requirements
CRITICAL: Do NOT assume the API schema or operations. Ask the user before writing any code:
- Operations: Which operations does the contract need and with what parameters? (e.g., paginated list, single item by ID, both, with filters, etc.)
- Entity fields: What fields does the DTO need? For GraphQL, the user can provide the query directly — derive DTOs from its response shape. Otherwise, ask for the fields or API documentation. Never fetch external API docs on your own.
Only proceed to implementation after the user confirms these details.
DataSource Types
| Type | Transport | Contract | Implementation | Error Mapper |
|---|---|---|---|---|
| REST | HTTP | nonisolated protocol: Sendable |
nonisolated struct with HTTPClientContract |
HTTPErrorMapper |
| GraphQL | HTTP/GraphQL | nonisolated protocol: Sendable |
nonisolated struct with GraphQLClientContract |
GraphQLErrorMapper |
| Memory | In-memory | : Actor |
actor with dictionary storage |
— |
| UserDefaults | Local | : Actor |
actor with UserDefaults |
— |
Templates
Each reference contains full templates: contract, implementation, DTO, mock, fixtures, and tests.
- REST API: See [remote-rest.md](references/remote-rest.md)
- GraphQL API: See [remote-graphql.md](references/remote-graphql.md)
- Memory cache: See [local-memory.md](references/local-memory.md)
- UserDefaults: See [local-userdefaults.md](references/local-userdefaults.md)
File Structure
Features/{Feature}/
├── Sources/
│ └── Data/
│ ├── DataSources/
│ │ ├── Remote/
│ │ │ ├── {Name}RemoteDataSourceContract.swift
│ │ │ └── {Name}RESTDataSource.swift (or {Name}GraphQLDataSource.swift)
│ │ └── Local/
│ │ ├── {Name}LocalDataSourceContract.swift
│ │ ├── {Name}MemoryDataSource.swift
│ │ └── {Name}UserDefaultsDataSource.swift # Optional: UserDefaults
│ └── DTOs/
│ └── {Name}DTO.swift
└── Tests/
├── Unit/Data/
│ └── DataSources/
│ ├── Remote/
│ │ └── {Name}RESTDataSourceTests.swift (or {Name}GraphQLDataSourceTests.swift)
│ └── Local/
│ └── {Name}MemoryDataSourceTests.swift (or {Name}UserDefaultsDataSourceTests.swift)
├── Shared/Fixtures/
│ ├── {name}.json
│ └── {name}s_response.json
└── Shared/Mocks/
Key Principles
Contracts
- Internal visibility, separate file from implementation
- Remote contracts:
nonisolated protocol: Sendablewith@concurrenton async methods. Local contracts (Memory, UserDefaults):: Actor - Transport-agnostic: same contract for REST or GraphQL
- Remote DataSource implementations:
nonisolated structwith@concurrenton async methods - Transport clients (
HTTPClientContract,GraphQLClientContract) also use@concurrentfor off-MainActor execution ChallengeNetworkingusesnonisolateddefault isolation — networking types don't neednonisolatedannotations- DataSources only work with DTOs — parameters and return types must be DTOs, never domain objects
- Remote:
@concurrent async throws. Local (Memory, UserDefaults): methods are actor-isolated (implicitlyasyncfrom caller)
Sendable vs Actor contracts: Use
: Actorwhen the DataSource has its own mutable state to protect (Memory, UserDefaults,ImageDiskCacheContract). Use: Sendablewithnonisolatedmethods only for stateless wrappers around thread-safe APIs (e.g.,FileSystemwrappingFileManager). See/concurrencyskill "Actor Reentrancy" section for when and why to choose: Sendable.
DTOs (Data Transfer Objects)
"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.
Rules:
Decodable,Equatable- Internal visibility, properties match JSON keys
- NO behavior — only data (anemic by design)
- NO
toDomain()methods — mapping belongs in the Repository - REST IDs are
Int, GraphQL IDs areString
Error Mapping
DataSources catch transport errors and map them to APIError:
- REST:
HTTPError→APIErrorviaHTTPErrorMapper - GraphQL:
GraphQLError→APIErrorviaGraphQLErrorMapper
Repositories and upper layers only see APIError, never transport-specific errors.
Visibility Summary
| Component | Visibility | Location |
|---|---|---|
| Remote Contract | internal | Sources/Data/DataSources/Remote/ |
| Remote Implementation | internal | Sources/Data/DataSources/Remote/ |
| Local Contract | internal | Sources/Data/DataSources/Local/ |
| Local Implementation | internal | Sources/Data/DataSources/Local/ |
| DTO | internal | Sources/Data/DTOs/ |
| Mocks | internal | Tests/Shared/Mocks/ |
Checklists
RemoteDataSource (REST)
- Create DTO
- Create Contract in
Remote/withasync throws - Create RESTDataSource in
Remote/withHTTPErrorMapper - Create Mock in
Tests/Shared/Mocks/ - Create JSON fixtures in
Tests/Shared/Fixtures/ - Create tests
RemoteDataSource (GraphQL)
- Create DTO (IDs are
String) - Create Contract in
Remote/withasync throws - Create GraphQLDataSource in
Remote/withGraphQLErrorMapper - Create Mock in
Tests/Shared/Mocks/ - Create JSON fixtures in
Tests/Shared/Fixtures/ - Create tests
MemoryDataSource
- Create Contract in
Local/with: Actor - Create
actorImplementation inLocal/ - Create
actorMock with setter methods and call tracking - Create tests (use
awaitfor all mock reads/writes)
LocalDataSource (UserDefaults)
- Create Contract in
Local/with: Actor - Create
actorImplementation inLocal/withprivate let userDefaults - Create
actorMock with setter methods and call tracking - Create
asynctests using customUserDefaultssuite (nonisolated(unsafe)on test property)