Alamofire
URLRequestConvertible
Swift
iOS Development
Networking

Proper usage of the Alamofire's URLRequestConvertible

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

URLRequestConvertible is Alamofire's protocol for safely constructing URLRequest values. The right way to use it is to model endpoint details in one place and make asURLRequest() build a complete, valid request without performing unrelated work.

What The Protocol Is For

At a high level, URLRequestConvertible exists so Alamofire can ask your type for a request object in a consistent way.

The core contract is simple:

swift
func asURLRequest() throws -> URLRequest

That means your type should know enough to build the request, or throw if the request cannot be constructed.

A Good Endpoint Enum Pattern

A common and effective pattern is an enum where each case represents one endpoint.

swift
1import Foundation
2import Alamofire
3
4enum APIRouter: URLRequestConvertible {
5    case listUsers(page: Int)
6    case userDetail(id: Int)
7    case createUser(name: String, email: String)
8
9    private var baseURL: URL {
10        URL(string: "https://api.example.com")!
11    }
12
13    private var method: HTTPMethod {
14        switch self {
15        case .listUsers, .userDetail:
16            return .get
17        case .createUser:
18            return .post
19        }
20    }
21
22    private var path: String {
23        switch self {
24        case .listUsers:
25            return "/users"
26        case .userDetail(let id):
27            return "/users/\(id)"
28        case .createUser:
29            return "/users"
30        }
31    }
32
33    func asURLRequest() throws -> URLRequest {
34        var request = URLRequest(url: baseURL.appendingPathComponent(path))
35        request.method = method
36        request.timeoutInterval = 20
37
38        switch self {
39        case .listUsers(let page):
40            return try URLEncoding.default.encode(request, with: ["page": page])
41        case .userDetail:
42            return request
43        case .createUser(let name, let email):
44            return try JSONEncoding.default.encode(request, with: [
45                "name": name,
46                "email": email,
47            ])
48        }
49    }
50}

This pattern keeps method, path, and parameter encoding together, which is exactly what URLRequestConvertible is good at.

What Should Go Inside asURLRequest()

asURLRequest() should be responsible for request construction only:

  • choosing the URL
  • setting the HTTP method
  • adding headers
  • encoding query parameters or JSON bodies
  • throwing when the request cannot be built correctly

It should not perform network calls, token refresh flows, or response parsing.

Those belong elsewhere.

Keep Authentication Separate

A common mistake is shoving every authentication concern into the router enum. For simple static headers that can be acceptable, but dynamic auth is usually cleaner with a RequestInterceptor.

swift
1import Alamofire
2
3final class AuthInterceptor: RequestInterceptor {
4    let tokenProvider: () -> String?
5
6    init(tokenProvider: @escaping () -> String?) {
7        self.tokenProvider = tokenProvider
8    }
9
10    func adapt(
11        _ urlRequest: URLRequest,
12        for session: Session,
13        completion: @escaping (Result<URLRequest, Error>) -> Void
14    ) {
15        var request = urlRequest
16        if let token = tokenProvider() {
17            request.headers.add(.authorization(bearerToken: token))
18        }
19        completion(.success(request))
20    }
21}

This keeps endpoint modeling and auth adaptation from getting tangled together.

Using The Convertible Type

Once the endpoint type conforms to URLRequestConvertible, request code becomes much cleaner.

swift
1let session = Session(interceptor: AuthInterceptor(tokenProvider: { "sample-token" }))
2
3session.request(APIRouter.listUsers(page: 1))
4    .validate()
5    .responseData { response in
6        switch response.result {
7        case .success(let data):
8            print("Received bytes:", data.count)
9        case .failure(let error):
10            print("Request failed:", error)
11        }
12    }

The caller does not need to know how the URL, method, and encoding were assembled.

Make Errors Meaningful

Because asURLRequest() can throw, use that power when request construction can genuinely fail. Invalid base URLs, broken path assumptions, or malformed encodings should fail early there rather than producing mysterious runtime behavior later.

That said, avoid gratuitous throwing from logic that should have been represented safely in the type system from the start.

Test The Request Builder

One of the best reasons to use URLRequestConvertible is testability. You can assert that an endpoint builds the right request without hitting the network.

Examples of useful tests:

  • method is correct
  • URL path is correct
  • headers exist
  • query or JSON encoding is correct

That gives you confidence before the request ever leaves the app.

Common Pitfalls

The most common mistake is putting too much responsibility into asURLRequest(). It should build requests, not orchestrate the whole networking layer.

Another mistake is using the wrong encoder, such as URL encoding a JSON body endpoint.

Developers also sometimes hardcode tokens or user-specific state inside the router type, which makes testing and reuse harder.

Finally, do not bypass the protocol by building ad hoc requests everywhere else in the codebase. The point of URLRequestConvertible is centralization and consistency.

Summary

  • 'URLRequestConvertible is for safe, centralized URLRequest construction.'
  • 'asURLRequest() should build and return the request, or throw if it cannot.'
  • An endpoint enum is a strong, common pattern for using the protocol.
  • Keep authentication and retry behavior separate from request construction when possible.
  • Test the generated requests so bugs are caught before runtime integration.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.