Last updated 3 min read

How To: REST API in Swift with Vapor RestKit — CRUDs

Writing CRUD Rest APIs is something that gets me bored VERY fast. Here comes RestKit with a vaccine from CRUDs boredom
How To: REST API in Swift with Vapor RestKit — CRUDs
Photo by Chantal & Ole / Unsplash
#Server Side Swift, #Swift, #Software Engineering

ResourceModel DTOs

While Vapor's Model is something represented by a table in your database, RestKit introduces such thing as ResourceModel.

ResourceModel is a DTO. It's a wrap-around of the Model that is returned from the backend API as a response or consumed by the backend API as a request. Simply an input or an output.

There are three kinds of ResourceModels in Vapor RestKit for inputs and outputs:

protocol ResourceOutputModel: Content {
    associatedtype Model: Fields

    init(_: Model)
}

protocol ResourceUpdateModel: Content, Validatable {
    associatedtype Model: Fields

    func update(_: Model) -> Model
}

protocol ResourcePatchModel: Content, Validatable {
    associatedtype Model: Fields

    func patch(_: Model) -> Model
}

ResourceModels is a manageable way to maintain Inputs and Outputs versioning in a type-safe manner.

A database model is not limited to have a single output ResourceModel. This allows us to have:

struct UserV1: ResourceOutputModel {
    
    init(_: User)
}

struct UserV2: ResourceOutputModel {
    
    init(_: User)
}

It also allows us to maintain a number of response model formats serving different purposes:


struct UserCompact: ResourceOutputModel {
    let avatar: URL
    let username: String
   
    init(_: User)
}

struct UserFull: ResourceOutputModel {
    let avatar: URL
    let username: String
	let followedBy: [UserCompact]
	let follows: [UserCompact]
	
    init(_: User)
}

It can be helpful as long the app gets more mature and full response models become too heavy, and start to include joins with other tables, etc.

CRUD for Resource Models

Writing CRUD APIs is something that gets us bored VERY fast. Here comes RestKit with a vaccine from CRUD boredom.

Resource CRUD Controllers

Having a Todo defined as a database model (from the example Vapor template app):

import Fluent
import struct Foundation.UUID

final class Todo: Model, @unchecked Sendable {
    static let schema = "todos"
    
    @ID(key: .id)
    var id: UUID?

    @Field(key: "title")
    var title: String

    init() { }

    init(id: UUID? = nil, title: String) {
        self.id = id
        self.title = title
    }
}


CRUD controller creation process is pretty easy and consists of three simple steps.

  1. Create Inputs, Outputs for your model. Input models may include validations that exactly follow Vapor Docs
extension Todo {
    struct Output: ResourceOutputModel {
        let id: Int?
        let title: String

        init(_ model: Todo, req: Request) {
            id = model.id
            title = model.title
        }
    }
    
    struct CreateInput: ResourceUpdateModel {
        let title: String

        func update(_ model: Todo) throws -> Todo {
            model.title = title
            return model
        }

        static func validations(_ validations: inout Validations) {
            //Validate something
        }
    }
    
    struct UpdateInput: ResourceUpdateModel {
        let title: String

        func update(_ model: Todo) throws -> Todo {
            model.title = title
            return model
        }

        static func validations(_ validations: inout Validations) {
            //Validate something
        }
    }

    struct PatchInput: ResourcePatchModel {
        let title: String?

        func patch(_ model: Todo) throws -> Todo {
            model.title = title ?? model.title
            return model
        }

        static func validations(_ validations: inout Validations) {
            //Validate something
        }
    }
}

  1. Then we define a controller that will use Vapor RestKit's resource controller:
struct TodoController {
    func create(req: Request) async throws -> Todo.Output {
        try ResourceController<Todo.Output>().create(req: req, using: Todo.Input.self)
    }

    func read(req: Request) async throws -> Todo.Output {
        try ResourceController<Todo.Output>().read(req: req)
    }

    func update(req: Request) async throws -> Todo.Output {
        try ResourceController<Todo.Output>().update(req: req, using: Todo.Input.self)
    }

    func patch(req: Request) async throws -> Todo.Output {
        try ResourceController<Todo.Output>().patch(req: req, using: Todo.PatchInput.self)
    }

    func delete(req: Request) async throws -> Todo.Output {
        try ResourceController<Todo.Output>().delete(req: req)
    }

    func index(req: Request) async throws -> CursorPage<Todo.Output> {
        try ResourceController<Todo.Output>().getCursorPage(req: req)
    }
}

3. Add controller's methods to Vapor's routes builder:

app.group("todos") {
    let controller = TodoController()

    $0.on(.POST, use: controller.create)
    $0.on(.GET, Todo.idPath, use: controller.read)
    $0.on(.PUT, Todo.idPath, use: controller.update)
    $0.on(.DELETE, Todo.idPath, use: controller.delete)
    $0.on(.PATCH, Todo.idPath, use: controller.patch)
    $0.on(.GET, use: controller.index)
}

That's it! It will add the following methods to your API endpoint:

HTTP Method Route Result
POST /todos Create new
GET /todos/:todoId Show existing
PUT /todos/:todoId Update existing (Replace)
PATCH /todos/:todoId Patch exsiting (Partial update)
DELETE /todos/:todoId Delete
GET /todos Show list

Further Reading

Vapor RestKit can do much more: nested CRUD controllers for related entities that support all kinds of database relations and all kinds of pagination.

It's capable of building controllers with complicated nested filters and sorting and even eager loading query keys for querying joined models efficiently.

Check out all the features in the repo: Vapor RestKit