1. Навіщо URL(string:) повертає URL?
Коли ви вперше бачите URL(string:), може здатися, що Swift просто кепкує: «Я ж дав рядок, чого тобі ще треба?» Але URL — це не просто рядок. Це вже перевірена структура, у якій Foundation гарантує базову коректність формату. Якщо рядок пошкоджений — наприклад, у ньому є пробіли, дивні символи або відсутня схема, — URL може не побудуватися, і Swift чесно повідомляє про це через Optional.
Важливо зрозуміти: Optional — це не «заважає жити», а «вмикає фари в темряві». Особливо це помітно в мережевому коді, де рядок URL часто надходить ззовні: з конфігурації, від користувача, із файла або з аргументів CLI. Якщо поставити !, ви не розв’язуєте проблему — ви переносите її в середовище виконання, де вона вибухне в найневдаліший момент.
Погляньмо на контраст.
import Foundation
let s = "https://example.com/api"
let url = URL(string: s)! // ❌ якщо s раптом зіпсується — усе впаде
print(url)
А ось доросліший варіант — не нудний, а просто з меншим числом сюрпризів:
import Foundation
enum RequestBuildError: Error {
case invalidURLString(String)
}
func makeURL(from s: String) throws -> URL {
guard let url = URL(string: s) else {
throw RequestBuildError.invalidURLString(s)
}
return url
}
Тут ми зробили ключовий крок: помилка побудови URL стала звичайною керованою ситуацією, а не падінням процесу.
Мініконтракт: збирання запиту теж може не вдатися
Коли люди починають писати мережевий код, вони часто думають так: «Ну, запит я точно зберу, а от мережа може впасти». На практиці ламається і те, і те. Неправильний URL, хибний метод, забутий Content-Type, порожнє тіло, яке «чомусь» має бути JSON… Усе це — помилки збирання запиту, і вони мають бути такими ж явними, як і помилки мережі.
Зафіксуймо зручний принцип: усе, що може піти не так до відправлення запиту, має бути виражене в коді через перевірки та валідацію. Або через throws, або через Result. У курсі ми часто використовуємо throws там, де помилка — це саме «не змогли зібрати», і її потрібно підняти вгору по стеку, щоб показати людині повідомлення.
Створімо невеликий тип помилки, який приємно читати в налагоджувачі.
import Foundation
enum RequestBuildError: Error, CustomStringConvertible {
case invalidBaseURL(String)
case invalidPath(String)
var description: String {
switch self {
case .invalidBaseURL(let s):
return "Некоректний базовий URL: \(s)"
case .invalidPath(let s):
return "Некоректний шлях: \(s)"
}
}
}
CustomStringConvertible тут — не розкіш. Це спосіб зробити так, щоб помилка виглядала як людина, а не як “Error Domain=… Code=…”.
2. Метод і заголовки без описок
HTTPMethod: маленький enum проти великих проблем
URLRequest.httpMethod — це рядок. Рядок! Тобто технічно ви можете написати "GE T" або "Get" і довго дивуватися на сервер, який раптом почне відповідати дивно. А він, між іншим, матиме рацію. Тому, навіть якщо URLRequest не змушує нас бути акуратними, ми самі себе змусимо — маленьким enum.
Це типовий прийом Swift: якщо якісь значення мають бути строго обмежені, ми перетворюємо їх на enum. Так компілятор стане вашим прискіпливим другом, який не дає написати дурницю.
import Foundation
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
Якщо ви помилитеся, компілятор не промовчить.
URLRequest як контейнер запиту
Важливо побачити картину цілком. URL — це адреса. Але HTTP‑запит — це не лише адреса. Запит — це адреса + метод + заголовки + (інколи) тіло + налаштування на кшталт таймауту. У Swift усе це зберігається в URLRequest.
Ми поки не запускаємо запит — це буде в наступній лекції через URLSession.dataTask. Зараз ми просто збираємо URLRequest так, ніби завтра цей код потрапить у продакшен, хай і навчальний.
Створімо функцію, яка збирає базовий GET‑запит.
import Foundation
func makeGetRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.get.rawValue
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 10
return request
}
Зверніть увагу на три речі.
- По-перше, var request — це нормально. URLRequest — value type, тож ми спокійно його змінюємо.
- По-друге, Accept: application/json означає: «Сервере, надішли мені JSON».
- По-третє, таймаут — це не «прискорювач інтернету», а запобіжник, щоб не чекати вічність.
Accept vs Content-Type: що ми хочемо отримати і що надсилаємо
Цю плутанину новачки зустрічають так часто, що це вже майже обряд ініціації.
- Accept — це те, що ми хочемо отримати у відповідь.
- Content-Type — це те, що ми надсилаємо в тілі запиту.
Якщо в запиту немає тіла — наприклад, у звичайного GET, — Content-Type часто взагалі не потрібен. Якщо ж ми надсилаємо JSON, зазвичай у POST або PUT, то Content-Type: application/json — це майже обов’язкова частина домовленості із сервером.
У вигляді таблиці це запам’ятовується швидше:
| Заголовок | Сенс по-людськи | Типовий приклад |
|---|---|---|
|
«Сервере, надішли мені ось це» | |
|
«Сервере, я надсилаю тобі ось це» | |
А тепер маленький приклад коду — просто щоб відчути API:
import Foundation
var request = URLRequest(url: URL(string: "https://example.com")!)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
Так, тут !, але це приклад із константним URL у демо. У реальному коді курсу ми так робити не будемо.
3. Тіло запиту та JSON
Чому httpBody — це Data
HTTP‑тіло — це байти. Навіть якщо ви надсилаєте «текст», «JSON» або «картинку», на дроті все одно їдуть байти. У Swift байти — це Data.
Тому URLRequest.httpBody має тип Data?. Не String. Не [String: Any]. Саме Data.
Найбезпечніший і найбільш «свівтівський» шлях перетворити вашу модель на JSON-байти — Codable + JSONEncoder. Саме тут увесь попередній матеріал про Codable починає окуповуватися.
Зробімо невеликий payload для умовного входу в систему — не тому, що ми будуємо авторизацію, а тому, що це знайомий приклад.
import Foundation
struct LoginPayload: Codable {
let username: String
let password: String
}
Тепер збираємо POST‑запит, акуратно кодувавши JSON:
import Foundation
func makeLoginRequest(url: URL, payload: LoginPayload) throws -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(payload)
return request
}
Тут важливий момент: encode може кинути помилку. Рідко, але може. І це нормально — ми й це враховуємо, бо сьогодні наша тема: ніяких «авось».
4. URL без склеювання рядків
Базовий URL + appendingPathComponent
Є одна звичка зі «швидкого прототипування», від якої варто позбутися ще до того, як вона стане частиною характеру: склеювати URL руками.
// ❌ погано: легко помилитися зі слешами
let urlString = base + "/v1" + "/books/" + id
По-перше, ви постійно ловитимете "//" або пропущений "/". По-друге, ви випадково «з’їдатимете» частини шляху. По-третє, це погано читається.
Якщо у вас є базовий URL — наприклад, https://api.example.com, — шлях зручніше додавати через appendingPathComponent. Це не URLComponents — вони будуть пізніше, — а проста й корисна операція саме для шляху.
Зберімо базовий URL безпечно:
import Foundation
func makeBaseURL(_ s: String) throws -> URL {
guard let url = URL(string: s) else {
throw RequestBuildError.invalidBaseURL(s)
}
return url
}
А тепер додамо шлях:
import Foundation
func makePingURL(baseURL: URL) -> URL {
baseURL
.appendingPathComponent("v1")
.appendingPathComponent("ping")
}
Тут приємно те, що шлях виглядає як конструктор LEGO: одразу зрозуміло, з яких деталей він складений.
5. RequestBuilder для LibraryCLI
Мініконструктор запитів
Зараз ми акуратно прив’яжемо тему до нашого застосунку курсу — CLI-утиліти LibraryCLI. Ми не запускаємо мережу сьогодні, але вже можемо підготувати шар, який відповідатиме за збирання запитів. Це сильно спростить наступну лекцію, де ми надсилатимемо запит і оброблятимемо відповідь.
Уявімо, що у нас є віддалений API з такими кінцевими точками:
- GET /v1/ping — перевірка доступності
- POST /v1/books — додати книгу (умовно)
Ми зробимо тип, який зберігає базовий URL і вміє збирати запити.
import Foundation
struct RequestBuilder {
let baseURL: URL
let timeout: TimeInterval
func makePingRequest() -> URLRequest {
let url = baseURL.appendingPathComponent("v1").appendingPathComponent("ping")
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.get.rawValue
request.timeoutInterval = timeout
request.setValue("application/json", forHTTPHeaderField: "Accept")
return request
}
}
Поки це проста версія. Але вже видно головне: «як зібрати URL» і «як зібрати URLRequest» заховано в одному місці, а не розкидано по всьому проєкту.
POST з JSON: приклад «створити книгу»
Тепер зробімо невеликий payload для книги, яку ми хочемо надіслати на сервер. У реальному проєкті це буде DTO/Domain-мапінг, але це пізніше. Зараз просто покажемо, як будується коректний POST.
import Foundation
struct CreateBookPayload: Codable {
let title: String
let author: String
}
І розширімо RequestBuilder:
import Foundation
extension RequestBuilder {
func makeCreateBookRequest(payload: CreateBookPayload) throws -> URLRequest {
let url = baseURL.appendingPathComponent("v1").appendingPathComponent("books")
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue
request.timeoutInterval = timeout
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(payload)
return request
}
}
Зверніть увагу на «симетрію»: якщо ми кладемо JSON у httpBody, ми зобов’язані поставити Content-Type. Це не правило Swift, а правило «ми хочемо, щоб сервер нас зрозумів».
Валідація до відправлення: перевірка здорового глузду
Між «код компілюється» і «код надійний» є невеличка прірва, в яку регулярно падають навіть досвідчені розробники. Тому інколи корисно додати просту валідацію на рівні збирання запиту: не для краси, а щоб ловити помилки ще до мережі.
Наприклад, можна домовитися так: GET/DELETE ми надсилаємо без тіла. POST/PUT — можна з тілом. Це не суворе правило протоколу, а здорове правило проєкту.
Зробімо дуже невелику перевірку:
import Foundation
extension HTTPMethod {
var allowsBody: Bool {
switch self {
case .get, .delete:
return false
case .post, .put:
return true
}
}
}
І використаємо її в збиранні:
import Foundation
enum RequestValidationError: Error {
case bodyNotAllowed(method: HTTPMethod)
}
func validateBody(method: HTTPMethod, body: Data?) throws {
if body != nil && !method.allowsBody {
throw RequestValidationError.bodyNotAllowed(method: method)
}
}
Сенс такої валідації не в тому, що «інакше інтернет зламається», а в тому, що ви захищаєте проєкт від випадкових помилок, коли хтось через місяць додасть httpBody у GET «бо так простіше».
6. Схема: як мислити збирання запиту
Щоб у голові не змішувалися «побудувати», «відправити» і «розпарсити», тримайте дуже просту послідовність. Сьогодні ми зупиняємося на перших кроках.
flowchart TD
A["Рядок/конфіг: baseURL"] --> B["URL(string:) → URL?"]
B -->|guard let| C["baseURL: URL"]
C --> D["Додаємо компоненти шляху"]
D --> E["URLRequest(url:)"]
E --> F["method + headers + timeout"]
F --> G["(за потреби) encode payload → Data"]
G --> H["request.httpBody = Data"]
На наступній лекції з’явиться продовження: URLSession.dataTask і обробка результату.
7. Типові помилки
Помилка № 1: URL(string: ...)! «бо я впевнений».
Проблема не у впевненості, а в тому, що впевненість не є типом у Swift. Рядок URL легко змінюється: конфіг підвантажили з файла, користувач увів пробіл, хтось додав «http:/» замість «http://». Звичка ставити ! перетворює таку ситуацію на креш процесу. Набагато здоровіше змусити помилку стати звичайним значенням керування потоком через guard let + throws, як радить практика розкривати optional одразу там, де він з’являється.
Помилка № 2: плутанина Accept і Content-Type.
Симптом виглядає так: сервер повертає помилку або «не розуміє» тіло, а ви впевнені, що надіслали JSON. Часто виявляється, що ви поставили лише Accept, але забули Content-Type. Лікується простим правилом: Accept — про відповідь, Content-Type — про ваше тіло запиту. Якщо поклали JSON у httpBody, поставте Content-Type: application/json.
Помилка № 3: склеювання URL рядками і танці зі слешами.
Сьогодні все працювало, завтра ви додали ще один сегмент шляху й отримали https://api//v1/books або https://apiv1/books. Такі баги неприємні, бо виглядають майже правильно. У навчальному проєкті краще одразу виробити звичку: базовий URL будуємо один раз через URL(string:), а шлях додаємо через appendingPathComponent.
Помилка № 4: тіло запиту як рядок «і так зійде».
Іноді новачки намагаються зробити request.httpBody = "hello".data(using: .utf8) і надіслати «JSON вручну». Це може працювати, але дуже легко помилитися з екрануванням, лапками та кодуванням. Якщо у вас структура даних, використовуйте Codable + JSONEncoder, тому що це типобезпечно і простіше підтримується.
Помилка № 5: збирання запиту в одному місці з бізнес-логікою.
Коли код «і розбирає команду CLI», «і збирає URL», «і додає заголовки», «і кодує JSON» в одній функції на 80 рядків, він починає ламатися від кожної зміни. Краще виділити маленький RequestBuilder, який відповідає лише за збирання URLRequest. Тоді в наступній лекції ви зможете підключити URLSession без рефакторингу всього проєкту.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ