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 "Invalid base URL: \(s)"
case .invalidPath(let s):
return "Invalid path: \(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["Добавляем path components"]
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 без рефакторинга всего проекта.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ