Alamofire
Swift
Networking
API
iOS Development

Chain multiple Alamofire requests

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Chaining Alamofire requests means one network call depends on data from a previous one, such as fetching a user profile first and then loading that user’s orders. The main engineering goal is to keep the flow linear, fail early, and avoid turning networking code into nested callback pyramids.

Start With the Dependency Graph

Before writing code, decide whether the requests are truly sequential. Some chains are unavoidable because request two needs an identifier or token returned by request one. Others can start in parallel once the first call completes.

That distinction affects both readability and performance.

Callback Chaining Works, but It Scales Poorly

The classic Alamofire style uses completion handlers.

swift
1import Alamofire
2
3func fetchUserThenOrders(completion: @escaping (Result<[Order], Error>) -> Void) {
4    AF.request("https://api.example.com/me")
5        .validate()
6        .responseDecodable(of: User.self) { userResponse in
7            switch userResponse.result {
8            case .failure(let error):
9                completion(.failure(error))
10            case .success(let user):
11                AF.request("https://api.example.com/users/\(user.id)/orders")
12                    .validate()
13                    .responseDecodable(of: [Order].self) { ordersResponse in
14                        completion(ordersResponse.result)
15                    }
16            }
17        }
18}

This is acceptable for one or two steps, but it gets harder to maintain as the chain grows.

Prefer async and await for New Code

Modern Swift concurrency makes dependent requests read like straight-line code.

swift
1import Alamofire
2
3func fetchUserThenOrders() async throws -> [Order] {
4    let user: User = try await AF.request("https://api.example.com/me")
5        .validate()
6        .serializingDecodable(User.self)
7        .value
8
9    let orders: [Order] = try await AF.request("https://api.example.com/users/\(user.id)/orders")
10        .validate()
11        .serializingDecodable([Order].self)
12        .value
13
14    return orders
15}

This is usually the clearest form because error propagation becomes natural and you do not need nested completion handlers.

Keep Authentication Chains Isolated

A common chain is token refresh followed by a protected resource request. Do not spread token logic across view controllers or screens.

swift
1func fetchProfileWithFreshToken() async throws -> Profile {
2    let token: AuthToken = try await AF.request("https://api.example.com/auth/refresh")
3        .validate()
4        .serializingDecodable(AuthToken.self)
5        .value
6
7    let profile: Profile = try await AF.request(
8        "https://api.example.com/profile",
9        headers: [.authorization(bearerToken: token.value)]
10    )
11    .validate()
12    .serializingDecodable(Profile.self)
13    .value
14
15    return profile
16}

A dedicated networking or auth service makes this flow easier to test and change later.

Run Independent Follow-Up Requests Concurrently

Once the initial dependency is satisfied, later requests may not need to wait on each other.

swift
1func fetchDashboard() async throws -> Dashboard {
2    let user: User = try await AF.request("https://api.example.com/me")
3        .serializingDecodable(User.self)
4        .value
5
6    async let stats: Stats = AF.request("https://api.example.com/users/\(user.id)/stats")
7        .serializingDecodable(Stats.self)
8        .value
9    async let messages: [Message] = AF.request("https://api.example.com/users/\(user.id)/messages")
10        .serializingDecodable([Message].self)
11        .value
12
13    return try await Dashboard(stats: stats, messages: messages)
14}

This keeps the logical dependency while avoiding unnecessary serial latency.

Test the Chain as a Service, Not Inside the UI

Networking orchestration belongs in a service layer, not directly in a view controller. That way you can mock the service in UI tests and mock lower-level clients in unit tests.

Keep the chain’s responsibilities narrow:

  • request ordering
  • decoding
  • error propagation
  • optional retry policy

The UI should mostly react to success or failure, not orchestrate transport details.

Common Pitfalls

The biggest mistake is deeply nesting callbacks when the codebase already supports async and await.

Another issue is treating every sequence as strictly serial even when some follow-up calls could run concurrently.

A third problem is mixing token refresh, decoding, retry, and screen-state updates all in one method, which makes failure handling hard to reason about.

Summary

  • Chain Alamofire requests only where data dependencies actually require it.
  • Callback chaining works, but async and await is usually clearer for new code.
  • Keep token-refresh logic in a dedicated service.
  • Run independent follow-up requests concurrently after the required first step.
  • Put request orchestration in a service layer rather than directly in UI code.

Course illustration
Course illustration

All Rights Reserved.