How To: REST API in Swift with Vapor RestKit — CRUDs
Basics
While Vapor's Model is something represented by a table in your database, RestKit introduces such thing as ResourceModel.
ResourceModel is a wrap over the Model that is returned from backend API as a response or consumed by backend API as a request.
Actually, there are three types of ResourceModels in Vapor RestKit:
One for output:
protocol ResourceOutputModel: Content {
associatedtype Model: Fields
init(_: Model)
}
and two for input: Update and Patch ResourceModels:
protocol ResourceUpdateModel: Content, Validatable {
associatedtype Model: Fields
func update(_: Model) -> Model
}
protocol ResourcePatchModel: Content, Validatable {
associatedtype Model: Fields
func patch(_: Model) -> Model
}
ResourceModels provide manageable interface for the underlying database models. My implementation was initially inspired by Tibor Bödecs's post about Generic CRUD controllers and a CRUD-Kit library.
The difference is that by design, the libs above allow only single input and output per model. In my implementation each database model can have as many possible inputs and outputs. And here is why.
The problem
When we start with the first version of our backend API we usually have a single output for the model across all endpoints. And that's very convenient both for server and client side.
When our app gets more mature models may become too heavy, require joins with other tables, etc.
At some point we may start to wish some compact version of the model along with full model detailed output.
Furthermore, when the app gets even more mature, we may want to version our model outputs so we could maintain old clients along with new ones.
Vapor RestKit provides a manageable way to maintain Inputs and Outputs versioning in a type-safe manner.
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
CRUD controller creation process is pretty easy and consists of three simple steps.
- Create Inputs, Outputs for your model:
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
}
}
}
2. Using resource controller builder, we can define which operations will be supported by resource controller:
let controller = Todo.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.create(using: Todo.CreateInput.self)
.read()
.update(using: Todo.UpdateInput.self)
.patch(using: Todo.PatchInput.self)
.delete()
.collection(sorting: DefaultSorting.self,
filtering: DefaultFiltering.self)
3. Add controller's methods to Vapor's routes builder:
controller.addMethodsTo(routeBuilder, on: "todos")
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 |
Customize Delete method Output
Luckily RestKit allows to take part in endless disputes about correct response format for DELETE methods and specify any output.
How to change Delete method output
By default, if delete controller is defined the following way and will return the deleted model as response:
let controller = Todo.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.delete()
It's possible to use different output for Delete method, or use default SuccessOutput:
let controller = SuccessOutput<Todo>
.controller(eagerLoading: EagerLoadingUnsupported.self)
.delete()
Member discussion