A minimal, Swift 6 concurrency-compliant networking framework designed specifically for tinyTCA applications. tinyAPI provides a lightweight, type-safe approach to API communication with built-in support for async/await, local JSON mocking, and seamless TCA integration.
- Swift 6.0+ with strict concurrency enabled
- SwiftUI framework
- iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+
- tinyTCA for state management integration
⚠️ Important: This framework requires Swift 6 strict concurrency mode and is designed to work seamlessly with tinyTCA's Feature pattern.
- 🎯 TCA-First Design: Built specifically for tinyTCA's Feature pattern
- ⚡ Swift 6 Ready: Full compliance with Swift 6 strict concurrency
- 🔄 Async/Await: Modern networking with async/await throughout
- 🛡️ Type Safety: End-to-end type safety with Codable support
- 🧪 Mock Support: Local JSON file loading for testing and previews
- 📱 RequestState: Built-in state management for API call lifecycle
- 🎛️ Dependency Injection: Easy switching between live and mock implementations
The heart of tinyAPI is the TinyAPIClient that handles all network communication:
let apiClient = TinyAPIClient()
// Simple GET request
let users = try await apiClient.get(
from: "https://api.example.com",
path: "/users",
as: [User].self
)
// POST with body
let newUser = try await apiClient.post(
to: "https://api.example.com",
path: "/users",
body: CreateUserRequest(name: "John", email: "john@example.com"),
as: User.self
)Define your API endpoints using the TinyAPIEndpoint protocol:
enum UserEndpoint {
case list
case create(CreateUserRequest)
case detail(id: Int)
case update(id: Int, user: User)
case delete(id: Int)
}
extension UserEndpoint: TinyAPIEndpoint {
var baseURL: String { "https://api.example.com" }
var path: String {
switch self {
case .list: return "/users"
case .create: return "/users"
case .detail(let id): return "/users/\(id)"
case .update(let id, _): return "/users/\(id)"
case .delete(let id): return "/users/\(id)"
}
}
var method: HTTPMethod {
switch self {
case .list, .detail: return .GET
case .create: return .POST
case .update: return .PUT
case .delete: return .DELETE
}
}
var body: Data? {
switch self {
case .create(let request):
return try? JSONEncoder().encode(request)
case .update(_, let user):
return try? JSONEncoder().encode(user)
default:
return nil
}
}
var headers: [String: String]? { nil }
var queryItems: [URLQueryItem]? { nil }
}tinyAPI includes RequestState<T> that perfectly integrates with tinyTCA's state management:
struct UserListFeature: Feature {
struct State: Sendable, Equatable {
var users: RequestState<[User]> = .idle
var selectedUser: User?
}
enum Action: Sendable {
case loadUsers
case usersResponse(Result<[User], TinyAPIError>)
case selectUser(User)
}
static var initialState: State { State() }
static func reducer(state: inout State, action: Action) throws {
switch action {
case .loadUsers:
state.users = .loading
case .usersResponse(.success(let users)):
state.users = .success(users)
case .usersResponse(.failure(let error)):
state.users = .failure(error.localizedDescription)
case .selectUser(let user):
state.selectedUser = user
}
}
static func effect(for action: Action, state: State) async throws -> Action? {
let apiClient = APIClientDependency.live.client
switch action {
case .loadUsers:
do {
let users = try await apiClient.request(UserEndpoint.list, as: [User].self)
return .usersResponse(.success(users))
} catch let error as TinyAPIError {
return .usersResponse(.failure(error))
} catch {
return .usersResponse(.failure(.networkError(error.localizedDescription)))
}
default:
return nil
}
}
}Use RequestState directly in your SwiftUI views:
struct UserListView: View {
@StoreState private var state: UserListFeature.State
init(store: Store<UserListFeature>) {
self._state = StoreState(store)
}
var body: some View {
NavigationView {
VStack {
switch state.users {
case .idle:
Text("Tap to load users")
case .loading:
ProgressView("Loading users...")
case .success(let users):
List(users, id: \.id) { user in
VStack(alignment: .leading) {
Text(user.name).font(.headline)
Text(user.email).font(.caption).foregroundColor(.secondary)
}
.onTapGesture {
$state.send(.selectUser(user))
}
}
case .failure(let error):
Text("Error: \(error)")
.foregroundColor(.red)
}
}
.navigationTitle("Users")
.toolbar {
Button("Load") {
$state.send(.loadUsers)
}
}
}
}
}tinyAPI includes a powerful mock system that loads local JSON files automatically:
// Mock client maps endpoints to JSON files:
// GET /users → mock_get_users.json
// POST /users → mock_post_users.json
// GET /users/1 → mock_get_users_1.json
let mockClient = MockTinyAPIClient()
let users = try await mockClient.request(UserEndpoint.list, as: [User].self)Create these files in your app bundle:
mock_get_users.json
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com"
}
]mock_post_users.json
{
"id": 999,
"name": "New User",
"email": "new@example.com"
}Switch between live and mock implementations easily:
struct APIClientDependency {
let client: any APIClientProtocol
static let live = APIClientDependency(client: TinyAPIClient.live)
static let mock = APIClientDependency(client: MockTinyAPIClient.demo)
static let preview = APIClientDependency(client: MockTinyAPIClient.preview)
static let testing = APIClientDependency(client: MockTinyAPIClient.testing)
}Add tinyAPI to your project using Xcode:
- File → Add Package Dependencies
- Enter the repository URL:
https://github.com/yourusername/tinyAPI - Choose your version requirements
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/yourusername/tinyAPI", from: "1.0.0"),
.package(url: "https://github.com/roberthein/tinyTCA", from: "1.0.0")
]struct PostsFeature: Feature {
struct State: Sendable, Equatable {
var posts: RequestState<[Post]> = .idle
}
enum Action: Sendable {
case loadPosts
case postsResponse(Result<[Post], TinyAPIError>)
}
static var initialState: State { State() }
static func reducer(state: inout State, action: Action) throws {
switch action {
case .loadPosts:
state.posts = .loading
case .postsResponse(.success(let posts)):
state.posts = .success(posts)
case .postsResponse(.failure(let error)):
state.posts = .failure(error.localizedDescription)
}
}
static func effect(for action: Action, state: State) async throws -> Action? {
switch action {
case .loadPosts:
do {
let posts = try await APIClientDependency.live.client.get(
from: "https://jsonplaceholder.typicode.com",
path: "/posts",
as: [Post].self
)
return .postsResponse(.success(posts))
} catch let error as TinyAPIError {
return .postsResponse(.failure(error))
}
default:
return nil
}
}
}Use different mock configurations for previews:
#Preview("Loading") {
UserListView(store: .preview(UserListFeature.State(users: .loading)))
}
#Preview("Success") {
let users = [
User(id: 1, name: "Preview User", email: "preview@example.com")
]
UserListView(store: .preview(UserListFeature.State(users: .success(users))))
}
#Preview("Error") {
UserListView(store: .preview(UserListFeature.State(users: .failure("Network error"))))
}- Use enums to represent all API endpoints for a feature
- Include request data as associated values
- Keep endpoint logic focused and simple
- Use
RequestState<T>for all API call states - Handle loading, success, and error states explicitly
- Keep state mutations in the reducer only
- Perform all API calls in the effect function
- Always return an action with the result
- Handle both success and error cases
- Use dependency injection for testability
- Use
TinyAPIErrorfor structured error information - Provide meaningful error messages to users
- Log detailed errors for debugging
- All network calls are async and don't block the main thread
- JSON decoding happens off the main thread
- State updates are batched efficiently
- Mock system has configurable delays for realistic testing
tinyAPI is built from the ground up for Swift 6 strict concurrency:
- All types conform to
Sendablewhere required - No data races between network calls and state updates
- Proper actor isolation for UI updates
- Full async/await support throughout
func testUserLoading() async throws {
let feature = UserListFeature.self
let mockClient = MockTinyAPIClient.testing
// Test loading state
var state = feature.initialState
try feature.reducer(state: &state, action: .loadUsers)
XCTAssertEqual(state.users, .loading)
// Test success response
let users = [User(id: 1, name: "Test", email: "test@example.com")]
try feature.reducer(state: &state, action: .usersResponse(.success(users)))
XCTAssertEqual(state.users, .success(users))
}func testRealAPIIntegration() async throws {
let client = TinyAPIClient.live
let users = try await client.get(
from: "https://jsonplaceholder.typicode.com",
path: "/users",
as: [User].self
)
XCTAssertFalse(users.isEmpty)
}Contributions are welcome! Please ensure all code:
- Maintains Swift 6 strict concurrency compliance
- Includes appropriate tests for both live and mock implementations
- Follows tinyTCA architectural patterns
- Includes proper error handling
This framework is designed to complement tinyTCA and follows similar architectural principles. Special thanks to the tinyTCA project for inspiration on minimal, type-safe architecture patterns.
This entire framework, including its name, tagline, implementation, documentation, README, examples, and even this very disclaimer, was entirely generated by artificial intelligence. This is a demonstration of AI-assisted software development and should be thoroughly reviewed, tested, and validated before any production use.
tinyAPI is available under the MIT license. See LICENSE file for more info.