After deciding to join more conferences this year, the very first one was dotSwift. It was held in a great old theater in Paris. It was a half-day conference, but it was better than I guessed. There were good talks about backend development in Swift. After hearing a lot about that and the great performance of Swift, I decided to give it a try. And here comes my first experience, step by step.
There are two major frameworks for the backend in Swift, Kitura and Vapor. I choose Kitura first. Because Kitura’s methods naming was close to the ones that I knew from Node.js. I felt more comfortable about understanding what each method does.
First, setting up the environment was easy because I have Mac and I’m actively developing iOS applications. So, Xcode and other stuff were already set up.
Creating a Swift backend project means creating a new Swift package with the single line command swift package init. This creates a structured new package. But packages are not executable, and I needed an executable project to run my backend. Creating main.swift file under the Sources directory gave me this ability to me. At this point, my current folder structure was like this:
SwiftBackend .gitignore Package.swift |--Sources main.swift |--TestsNow, it was time to arrange Package.swift and add Kitura. Here is the Package.swift file with Kitura added as a dependency:
import PackageDescription
let package = Package( name: "SwiftBackend", dependencies: [ .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 4) ])swift build command installs the dependencies and builds the project. After adding a new dependency, it’s always logical to build the project and see if it works.
The next step is setting up an endpoint. The basic example in Kitura’s tutorial is:
import Kitura
let router = Router()router.get("/") { request, response, next in response.send("Hello, World!") next()}Kitura.addHTTPServer(onPort: 8090, with: router)Kitura.run()I added this code to main.swift file. When I ran the project locally, I should have seen Hello, World! when I enter localhost:8090 via browser (or make a get request via Postman etc.). To run this project, first, I needed to build it via swift build and this command created the executable for me under .build/debug/SwiftBackend . I’ve just run the executable by typing the command line .build/debug/SwiftBackend. At this point, I was able to send a request and see “Hello, World!” text in the browser. But in the console, I didn’t see any logs about these requests.
HeliumLogger came in at that point. It’s a logger component which available as a separate Swift module. I added this module to Package.swift file by adding it as a new dependency. At this point, my Package.swift file was like this:
import PackageDescription
let package = Package( name: "SwiftBackend", dependencies: [ .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 4), .Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", majorVersion: 1, minor: 4) ])And I ran the swift build command again to install HeliumLogger. After that, I needed to import HeliumLogger and use it in main.swift file. It was just one line. My main.swift file became like this:
import Kituraimport HeliumLogger
HeliumLogger.use()
let router = Router()router.get("/") { request, response, next in response.send("Hello, World!") next()}Kitura.addHTTPServer(onPort: 8090, with: router)Kitura.run()After building and running the project again, I was able to see the request logs in the console.
As the next step, I wanted to connect the database to my backend API. I used CouchDB because there is a Kitura-CouchDB package.
After adding the CouchDB package to my Package.swift file, it became like this:
import PackageDescription
let package = Package( name: "SwiftBackend", dependencies: [ .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 4), .Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", majorVersion: 1, minor: 4), .Package(url: "https://github.com/IBM-Swift/Kitura-CouchDB.git", majorVersion: 1, minor: 4) ])Again, I ran the command swift build to install a new package.
While implementing database operations, I wanted to create a structure to not do everything in main.swift file. First, I created a User struct as a model object. My main purpose for database operations was to add this User object to the CouchDB database as a document. Easy model, easy operation. So User struct was like this:
import Foundation
public struct User { let name: String let identifier: String}So, as I said, my main purpose was to add a document to the CouchDB database. I created a DatabaseInteraction struct with one method to achieve that goal. My plan was to add all database operations to this struct. Here is the DatabaseInteraction struct:
import Foundationimport CouchDBimport SwiftyJSON
public struct DatabaseInteraction { var db: Database public init(db: Database) { self.db = db }
func addNewUser(_ user: User, handler: @escaping (String?, String?, JSON?, NSError?) -> ()) { let userDict: [String: Any] = [ "name": user.name, "identifier": user.identifier ]11 collapsed lines
let userJSON = JSON(userDict) db.create(userJSON) { (id, revision, doc, error) in if let error = error { handler(nil, nil, nil, error) return } else { handler(id, revision, doc, nil) } } }}I also separated HTTP request methods into routers. First, I created UserRouter to handle HTTP requests for users and I only wrote one post method to get user data in the body.UserRouter class is like this:
import Foundationimport Kituraimport CouchDBimport SwiftyJSON
public class UserRouter { var db: DatabaseInteraction
public init(db: DatabaseInteraction) { self.db = db }
public func bindAll(to router: Router) { addCreateUser(to: router) }33 collapsed lines
private func addCreateUser(to router: Router) { router.post("/user/", handler: { req, res, next in guard let parsedBody = req.body else { res.status(.badRequest) next() return } switch(parsedBody) { case .json(let jsonBody): let name = jsonBody["name"].string ?? let user = User(name: name, identifier: "\(name.characters.count)") self.db.addNewUser(user) { (id, revision, doc, error) in if let error = error { res.status(.internalServerError) next() } else { res.status(.OK) if let doc = doc { res.send(json: doc) } else { res.send("Something is wrong in the doc") } next() } } default: res.status(.badRequest) next() } }) }}After that, I created the main router to manage separate routing operations from here. Here is the BackendRouter as the main router object:
import Foundationimport Kitura
public class BackendRouter { public let router = Router() var db: DatabaseInteraction public init(db: DatabaseInteraction) { self.db = db router.get("/status") { req, res, callNextHandler in res.status(.OK).send("Everything is working") callNextHandler() }
router.all("*", middleware: BodyParser()) self.routeToUser()7 collapsed lines
}
func routeToUser() { let user = UserRouter(db: self.db) user.bindAll(to: self.router) }}Lastly, I connected all of them in the main.swift file.
import Kituraimport HeliumLoggerimport CouchDB
HeliumLogger.use()
let connProperties = ConnectionProperties( host: "127.0.0.1", // httpd address port: 5984, // httpd port secured: false, // https or http username: "admin", // admin username password: "password"// admin password)
let db = Database(connProperties: connProperties, dbName: "swift_backend_test_db")5 collapsed lines
let databaseInteraction = DatabaseInteraction(db: db)let app = MainRouter(db: databaseInteraction)
Kitura.addHTTPServer(onPort: 8090, with: app.router)Kitura.run()So, let’s see how the overall folder structure looks right now.
SwiftBackend .gitignore |--Sources BackendRouter.swift DatabaseInteraction.swift main.swift User.swift UserRouter.swift |--Tests Package.swiftAfter all coding, it was time to build the API and send a request via Postman. I got success after some trials. (Note: Be careful about connection properties and escaping closures). Of course, there are a lot of things to improve in code. But all these codes were intended to create a working backend API developed with Swift.
In the next post, I made this project testable and Dockerized. There are some points to be careful while making it testable.
As a final step, I’ll try to upload it to the cloud. But my first impression was really good. I should say that I am convinced to work on the backend side of Swift. It’s pretty easy. I’m working with SublimeText instead of Xcode. Thus, I can understand each line I wrote without auto-completion. If you’re working with Swift, you should definitely try to create some backend APIs with Swift.