ConnectableKit is a Swift package for the Vapor framework that simplifies the response DTOs and JSON structures for API projects.
- Generic JSON structure: The
Connectable
protocol allows you to define a wrapped VaporContent
structs. - Custom HTTPStatus for every responses.
- ErrorMiddleware configurations for handling Vapor's error as ConnectableKit JSON output.
- CORSMiddleware configurations for handling Vapor's CORSMiddleware with ease.
Type | Description | Type |
---|---|---|
status | Five possible cases: information, success, redirection, failure, and error. | ResponseStatus: String |
message | Optional custom message from server. | String? = nil |
data | Generic associated type that represents the data that is being sent as a response. It can be any type that conforms to Vapor's Content protocol, which includes types such as String, Int, and custom structs or classes. | Connectable? = nil |
{
"status": "success",
"message": "Profile fetched successfully.",
"data": {
"id": "EBAD7AA7-A0AF-45F7-9D40-439C62FB26DD",
"name": "Tuğcan",
"surname": "ÖNBAŞ",
"profileImage": "http://localhost:8080/default_profile_image.png",
"profileCoverImage": "http://localhost:8080/default_profile_cover_image.png"
}
}
ConnectableKit can be installed using Swift Package Manager. Simply add the following line to your Package.swift file:
dependencies: [
.package(url: "https://github.com/tugcanonbas/connectable-kit.git", from: "1.0.0")
]
dependencies: [
.product(name: "ConnectableKit", package: "connectable-kit"),
],
To use the ConnectableKit Framework,
In Struct, simply conform that Profile
is Connectable
import ConnectableKit
struct Profile: Model, Connectable {
@ID(key: .id)
var id: UUID
@Field(key: "name")
var name: String
@Field(key: "surname")
var surname: String
@Field(key: "profileImage")
var profileImage: String
@Field(key: "profileCoverImage")
var profileCoverImage: String
}
In Response call .DTO
for responding wrapped generic response.
return .toDTO(_ httpStatus: Vapor.HTTPStatus = .ok, status: ResponserStatus = .success, message: String? = nil) -> Responser<Self>
app.get("/profiles", ":id") { req -> Profile.DTO in
let id = try req.parameters.require("id", as: UUID.self)
let profile = try await Profile.query(on: req.db).filter(\.$id == id).first()!
return profile.toDTO(message: "Profile fetched successfully.")
}
app.post("/profiles") { req -> Profile.DTO in
let profile = Profile(
id: UUID(),
name: "Tuğcan",
surname: "ÖNBAŞ",
profileImage: "http://localhost:8080/default_profile_image.png",
profileCoverImage: "http://localhost:8080/default_profile_cover_image.png"
)
try await profile.save(on: req.db)
return profile.toDTO(.created, message: "Profile fetched successfully.")
}
app.put("/profiles", ":id") { req -> Connector.DTO in
let id = try req.parameters.require("id", as: UUID.self)
let update = try req.content.decode(Profile.Update.self)
let profile = try await Profile.query(on: req.db).filter(\.$id == id).first()!
profile.name = update.name
try await profile.save(on: req.db)
return Connector.toDTO(.accepted, message: "Profile updated successfully.")
}
{
"status": "success",
"message": "Profile updated successfully."
}
public protocol Connectable: Content {
associatedtype DTO = Responser<Self>
func toDTO(_ httpStatus: Vapor.HTTPStatus, status: ResponserStatus, message: String?) -> Responser<Self>
}
public extension Connectable {
func toDTO(_ httpStatus: Vapor.HTTPStatus = .ok, status: ResponserStatus = .success, message: String? = nil) -> Responser<Self> {
let response = Responser(httpStatus, status: status, message: message, data: self)
return response
}
}
import ConnectableKit
Simply call ConnectableKit.configureErrorMiddleware(app)
for default error handling.
ConnectableKit.configureErrorMiddleware(app)
Or, you can use custom error middleware error handling with ConnectableErrorMiddleware.ErrorClosure
.
let errorClosure: ConnectableErrorMiddleware.ErrorClosure = { error in
let status: Vapor.HTTPResponseStatus
let reason: String
let headers: Vapor.HTTPHeaders
switch error {
case let abort as Vapor.AbortError:
reason = abort.reason
status = abort.status
headers = abort.headers
case let customError as CustomError:
reason = customError.localizedDescription
status = customError.httpResponseStatus
headers = [:]
default:
reason = app.environment.isRelease
? "Something went wrong."
: String(describing: error)
status = .internalServerError
headers = [:]
}
return (status, reason, headers)
}
ConnectableKit.configureErrorMiddleware(app, errorClosure: errorClosure)
Error Response Example
Database Error:
{
"status": "error",
"message": "server: duplicate key value violates unique constraint \"uq:users.username\" (_bt_check_unique)"
}
AbortError:
guard let profile = try await Profile.query(on: req.db).filter(\.$id == id).first() else {
throw Abort(.notFound, reason: "User not found in our Database")
}
Response
{
"status": "failure",
"message": "User not found in our Database"
}
If you use ConnectableKitErrorMiddleware
, don't forget to use before all middleware configureations.
See in Vapor's Documentation Error Middleware
import ConnectableKit
ConnectableKit.configureCors(app)
func configureCORS
public static func configureCORS(_ app: Vapor.Application, configuration: Vapor.CORSMiddleware.Configuration? = nil)
The JSend specification, developed by Omniti Labs, outlines a consistent JSON response format for API endpoints. I found the specification to be very helpful in ensuring that API consumers can easily understand and parse the responses returned by the API.
As a result, I have heavily borrowed from the JSend specification for the response format used in this project. Specifically, I have adopted the status field to indicate the overall success or failure of the request, as well as the use of a message field to provide additional context for the response.
While I have made some modifications to the response format to suit the specific needs of this project, I am grateful for the clear and thoughtful guidance provided by the JSend specification.
ConnectableKit is available under the MIT license. See the LICENSE file for more info.