This is my presentation from TechBeats #3 hosted by Applause about Server-Side Swift framework called Vapor.
Swift is a great language and possibility of using it also in backend is a huge benefit for any iOS developer out there. Using Vapor is a seamless experience. With this framework creating advance APIs by iOS developer is as easy as writing simple iOS app.
https://www.meetup.com/TechBeats-hosted-by-Applause/events/254910023/
3. Swift
• Multi-paradigm
• Statically typed
• Fast
(https://benchmarksgame-team.pages.debian.net/benchmarksgame/faster/swift.html)
• Safe
• Easy to understand
• Open Source
• Community
5. Swift NIO
SwiftNIO is a cross-platform asynchronous event-driven network application
framework for rapid development of maintainable high performance protocol
servers & clients.
https://www.youtube.com/watch?v=QJ3WG9kRLMo
7. Check compatibility
eval "$(curl -sL check.vapor.sh)
And the output should look something like this
⚠ Xcode 10 support hasn't been tested yet.
ℹ Xcode 10 should be compatible with Vapor 2.
ℹ Xcode 10 should be compatible with Vapor 3.
⚠ Swift 4.2 support hasn't been tested yet.
ℹ Swift 4.2 should be compatible with Vapor 2.
ℹ Swift 4.2 should be compatible with Vapor 3.
8. First App
vapor new TestApp
cd TestApp
vapor build
vapor run
http://localhost:8080/hello/
24. Async code in Vapor
Concept of Future
func getHelloMessages(_ req: Request) throws -> [HelloMessage] {
func getHelloMessages(_ req: Request) throws -> Future<[HelloMessage]> {
func imaginary(_ req: Request) throws -> Future<[Story]> {
let client = try req.make(Client.self)
let url = "https://stories-fetcher.herokuapp.com/hnproxy/topStories/"
return client.send(.GET, to: url)
.flatMap(to: [Story].self, { (response) -> EventLoopFuture<[Story]> in
return try response.content.decode([Story].self)
})
}
25. flatMap()
• Use it when Promise closure returns a Future
• When you want to unpack value and work with raw data
26. map()
• When Promise returns anything else than Future
• To map result from long operation in Future
27. transform
• When you don’t care about result data only want to know
about result status (success/failure)
func imaginary(_ req: Request) throws -> Future<HTTPStatus> {
let client = try req.make(Client.self)
let url = "https://stories-fetcher.herokuapp.com/hnproxy/topStories/"
return client.send(.GET, to: url).transform(to: HTTPStatus.noContent)
}
28. Future - multiple features
• To wait for multiple Futures to end
func imaginary(_ req: Request) throws -> Future<HTTPStatus> {
let client = try req.make(Client.self)
let url = "https://stories-fetcher.herokuapp.com/hnproxy/topStories/"
return map(to: HTTPStatus.self,
try req.content.decode(Story.self),
client.send(.GET, to: url)) { (parameter, response) -> (HTTPStatus) in
return HTTPStatus.noContent
}
}
31. Chaining futures
• flatMap and map can be chained
func imaginary(_ req: Request) throws -> Future<HTTPStatus> {
let client = try req.make(Client.self)
let url = "https://stories-fetcher.herokuapp.com/hnproxy/topStories/"
return client.send(.GET, to: url)
.flatMap(to: [Story].self, { (response) -> EventLoopFuture<[Story]> in
return try response.content.decode([Story].self)
}).flatMap(to: HTTPStatus.self, { (_) -> EventLoopFuture<HTTPStatus> in
return req.future(.ok)
})
}
32. always
func imaginary(_ req: Request) throws -> Future<HTTPStatus> {
let _ = try req.content.decode(Story.self).always {
print("This code will execute always")
}
return req.future(.ok)
}
When you need something to execute always, no matter the
result
33. wait
• Mostly not usable in code 💥
func imaginary(_ req: Request) throws -> Future<HTTPStatus> {
let client = try req.make(Client.self)
let url = "https://stories-fetcher.herokuapp.com/hnproxy/topStories/"
let stories = try client.send(.GET, to: url).flatMap(to: [Story].self)
{ (response) -> EventLoopFuture<[Story]> in
return try response.content.decode([Story].self)
}.wait()
return req.future(.ok)
}
34. Fluent
• Mapping between objects with CRUD support
• Querying data
• Relationships
• Session live cache
• Custom queries
• Transaction support
• Migrations
• Numerous database types support
36. How to add persistence
//configure.swift
let sqlite = try SQLiteDatabase(storage: .memory)
var databases = DatabasesConfig()
databases.add(database: sqlite, as: .sqlite)
services.register(databases)
37. Defining your first model
import FluentSQLite
struct HelloMessage: Content {
var id: Int?
let message: String
}
extension HelloMessage: Model {
typealias Database = SQLiteDatabase
typealias ID = Int
static var idKey: IDKey = HelloMessage.id
}
38. Is it really all?
//HelloMessage.swift
extension HelloMessage: Migration {}
//configure.swift
var migrations = MigrationConfig()
migrations.add(model: HelloMessage.self, database: .sqlite)
services.register(migrations)
43. Vapor CLI Tool
• vapor cloud login
• vapor cloud deploy
• Configure repo (ssh/https)
• Create Project
• Select Organisation
• Name the project
• Provide application Slug (https://<slug>.vapor.cloud)
• Setup Environment
• DB selection
• Build type (incremental, update, clean)
44. Leaf Templates
// Package.swift
.package(url: "https://github.com/vapor/leaf.git", from: “3.0.0-rc")
.target(name: "App", dependencies: ["FluentSQLite", "Vapor", “Leaf”])
//configure.swift
try services.register(LeafProvider())
config.prefer(LeafRenderer.self, for: ViewRenderer.self)
vapor update
Updating [Done]
Changes to dependencies usually require Xcode to be regenerated.
Would you like to regenerate your xcode project now?
y/n> y
Generating Xcode Project [Done]
Select the `Run` scheme to run.
Open Xcode project?
y/n> y
Opening Xcode project...
45. Rendering page
• Create index.leaf view in Resources/Views directory
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello Leaf</title>
</head>
<body>
<h1>Hello Leaf</h1>
</body>
</html>
• Add new route for index and inside render index view
router.get("index") { (req) -> Future<View> in
return try req.view().render("index")
}
46. Injecting context
struct IndexContext: Encodable {
let title: String
}
<title>#(title)</title>
router.get("index") { (req) -> Future<View> in
let context = IndexContext(title: "Home Sweet Home")
return try req.view().render("index", context)
}
47. More about Leaf
• Conditions with #if {} else if {} else {}…
• Loops #for…in…{}
• Content embedding with #embed #set
• Custom tag operations like
• Date formatting with #date
• Text formatting with #capitalize and #lowercase
• Many more
51. Auth
Conformance
extension User: BasicAuthenticatable {
static var usernameKey: IDKey = User.name
static var passwordKey: IDKey = User.password
}
extension User: PasswordAuthenticatable {
static var usernameKey: IDKey = User.name
static var passwordKey: IDKey = User.password
}
extension User: TokenAuthenticatable {
typealias TokenType = UserToken
}
struct UserToken: Model {
var id: Int?
var string: String
var userID: User.ID
var user: Parent<UserToken, User> {
return parent(.userID)
}
}
52. Make Auth Work
// configure.swift
try services.register(AuthenticationProvider())
let basicMiddleware = User.basicAuthMiddleware(using: BCryptDigest())
let guardAuthMiddleware = User.guardAuthMiddleware()
let protectedRouter = router.grouped([basicMiddleware, guardAuthMiddleware])
protectedRouter.get { req in
return "It works only in auth"
}
54. OAuth 2.0
• Imperial (https://github.com/vapor-community/
Imperial.git)
• Create and setup project in Google console
• Create dedicated RouteCollection controller to handle
Google login
• Add env variables with Google client id and secret
55. Testing
.testTarget(name: "AppTests", dependencies: ["App"])
Change project Scheme for Testing
final class AppTests: XCTestCase {
func testNothing() throws {
// add your tests here
XCTAssert(true)
}
static let allTests = [
("testNothing", testNothing)
]
}
56. App in tests
override func setUp() {
var config = Config.default()
var services = Services.default()
var env = Environment.testing
try! App.configure(&config, &env, &services)
app = try! Application(config: config,
environment: env,
services: services)
}
57. Endpoint test
func testHelloMessage() throws {
let request = HTTPRequest(method: .GET,
url: "/hello")
let wrapper = Request(http: request, using: app)
let responder = try! app.make(Responder.self)
let response = try! responder.respond(to: wrapper).wait()
let message = String(data: response.http.body.data!, encoding: .utf8)
XCTAssertEqual(message, "Hello, world!", "Wrong response from /hello")
}