Как написать REST API на Swift за 30 минут: практическое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему 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: Регистрация маршрутов и тестирование

Теперь нужно зарегистрировать контроллер в основном

Блоги

Рекомендаций по книгам