How To: REST API in Swift with Vapor RestKit — CRUDs
#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.
- 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
}
}
}
- 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
Comments