Введение: Почему Swift подходит для REST API?
Когда речь заходит о серверной разработке, Swift — не первый язык, который приходит на ум. Однако с появлением фреймворка Vapor (и его альтернативы — Kitura), Swift стал полноценным инструментом для создания REST API. Зачем использовать Swift на сервере? Во-первых, это единый язык для iOS, macOS и бэкенда — вы можете переиспользовать модели данных. Во-вторых, Swift — быстрый, безопасный и имеет отличную поддержку асинхронности через async/await.
В этом руководстве мы создадим простой REST API для управления списком задач (Todo list) на Swift с использованием фреймворка Vapor. Вы научитесь настраивать сервер, создавать маршруты, работать с JSON и тестировать API. Весь процесс займёт около 30 минут.
Шаг 1: Установка и настройка проекта
Прежде чем писать код, нужно установить Vapor Toolbox — утилиту командной строки для создания и управления проектами Vapor.
Установка Vapor Toolbox
Откройте терминал и выполните:
brew install vapor
Если у вас нет Homebrew, установите его с brew.sh.
Создание нового проекта
Создадим новый проект с именем TodoAPI:
vapor new TodoAPI -n
cd TodoAPI
Флаг -n означает «без примера» — мы получим чистый шаблон.
Запуск сервера
Откройте проект в Xcode:
open Package.swift
Выберите схему Run и нажмите Cmd+R. Через несколько секунд сервер запустится на http://127.0.0.1:8080. Откройте браузер — вы должны увидеть приветственное сообщение от Vapor.
Шаг 2: Создание модели данных
Наше API будет управлять задачами. Каждая задача имеет ID, название и статус выполнения (выполнено/не выполнено).
Создадим модель Todo. В Vapor модели — это обычные структуры, которые соответствуют протоколу Content (для сериализации в JSON) и Codable.
Создайте файл Sources/App/Models/Todo.swift:
import Vapor
struct Todo: Content, Codable {
var id: UUID?
var title: String
var isCompleted: Bool
init(id: UUID? = nil, title: String, isCompleted: Bool = false) {
self.id = id
self.title = title
self.isCompleted = isCompleted
}
}
Обратите внимание: id — опциональный UUID. Мы будем генерировать его на сервере при создании задачи.
Шаг 3: Создание хранилища и контроллера
Для простоты мы будем хранить задачи в памяти (в массиве). В реальном проекте вы бы использовали базу данных (Fluent ORM), но для 30-минутного демо это избыточно.
Хранилище (In-Memory Store)
Создайте файл Sources/App/Controllers/TodoStore.swift:
import Vapor
final class TodoStore {
private var todos: [Todo] = []
private let lock = NSLock()
func getAll() -> [Todo] {
lock.lock()
defer { lock.unlock() }
return todos
}
func get(id: UUID) -> Todo? {
lock.lock()
defer { lock.unlock() }
return todos.first { $0.id == id }
}
func create(_ todo: inout Todo) -> Todo {
lock.lock()
defer { lock.unlock() }
todo.id = UUID()
todos.append(todo)
return todo
}
func update(id: UUID, with updatedTodo: Todo) -> Todo? {
lock.lock()
defer { lock.unlock() }
guard let index = todos.firstIndex(where: { $0.id == id }) else { return nil }
var mutableTodo = updatedTodo
mutableTodo.id = id
todos[index] = mutableTodo
return mutableTodo
}
func delete(id: UUID) -> Bool {
lock.lock()
defer { lock.unlock() }
guard let index = todos.firstIndex(where: { $0.id == id }) else { return false }
todos.remove(at: index)
return true
}
}
Мы используем NSLock для потокобезопасности — это важно, так как Vapor обрабатывает запросы асинхронно.
Контроллер (TodoController)
Создайте файл Sources/App/Controllers/TodoController.swift:
import Vapor
struct TodoController: RouteCollection {
let store = TodoStore()
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("api", "todos")
todos.get(use: index)
todos.post(use: create)
todos.group(":todoID") { todo in
todo.get(use: show)
todo.put(use: update)
todo.delete(use: delete)
}
}
// GET /api/todos
func index(req: Request) async throws -> [Todo] {
return store.getAll()
}
// GET /api/todos/:id
func show(req: Request) async throws -> Todo {
guard let id = req.parameters.get("todoID", as: UUID.self) else {
throw Abort(.badRequest, reason: "Invalid ID")
}
guard let todo = store.get(id: id) else {
throw Abort(.notFound)
}
return todo
}
// POST /api/todos
func create(req: Request) async throws -> Todo {
var todo = try req.content.decode(Todo.self)
return store.create(&todo)
}
// PUT /api/todos/:id
func update(req: Request) async throws -> Todo {
guard let id = req.parameters.get("todoID", as: UUID.self) else {
throw Abort(.badRequest, reason: "Invalid ID")
}
let updatedTodo = try req.content.decode(Todo.self)
guard let result = store.update(id: id, with: updatedTodo) else {
throw Abort(.notFound)
}
return result
}
// DELETE /api/todos/:id
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("todoID", as: UUID.self) else {
throw Abort(.badRequest, reason: "Invalid ID")
}
guard store.delete(id: id) else {
throw Abort(.notFound)
}
return .noContent
}
}
Шаг 4: Регистрация маршрутов и тестирование
Теперь нужно зарегистрировать контроллер в основном