So you want to build your next backend API in Swift? No problem! Perfect has got you covered. Perfect is a collection of libraries to turn your next server-side Swift application into reality. It is blazingly fast and awesome!
So I decided to dive a little bit deeper into the possibilities of Swift and Perfect, and so I quickly decided that an authentication API would be a 'perfect' idea (see what I did there?) to get started. So, what do we need?
- A computer running Linux or macOS
- Swift 3 installed
- A MySQL Server
That's basically all that's needed. Now we need a functional database, so I came up with a SQL table like this:
CREATE TABLE `users` (
`id` int(32) NOT NULL,
`username` varchar(128) NOT NULL,
`password` varchar(128) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`),
ADD UNIQUE KEY `id` (`id`);
INSERT INTO `users` (`id`, `username`, `password`) VALUES
(1, 'user1', '7c12772809c1c0c3deda6103b10fdfa0'),
(2, 'user2', '7c12772809c1c0c3deda6103b10fdfa0');
ALTER TABLE `users`
MODIFY `id` int(32) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
Perfect. Now - for the next part - initialize a new Swift package in an empty folder:
swift package init --type executable
This will generate an empty package, so now it's time to edit our Package.swift
file. You'll need to add 2 repositories to your project, this is how I have done that:
import PackageDescription
let package = Package(
name: "backend-api",
dependencies: [
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2),
.Package(url: "https://github.com/PerfectlySoft/Perfect-MySQL.git", majorVersion: 2)
]
)
Cool. So now it is about time to work on the real deal - programming! I've split the project into two separate swift files
- main.swift : this is for all things related to the web server processing
- DBOperations.swift : this is a provider to execute MySQL (prepared) statements in the form of a function
My main.swift:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
import Foundation
let server = HTTPServer()
var routes = Routes()
server.serverPort = 8000
routes.add(method: .get, uri: "/", handler: {
request, response in
response.status = HTTPResponseStatus.ok
response.setHeader(.contentType, value: "application/json")
response.setBody(string: "{\"status\":\"Backend server is up and running\"}")
response.completed()
})
routes.add(method: .get, uri: "/oauth", handler: {
request, response in
response.setHeader(.contentType, value: "application/json")
let username = request.param(name: "username", defaultValue: "")
let password = request.param(name: "password", defaultValue: "")
if ((username ?? "").isEmpty || (password ?? "").isEmpty) {
response.status = HTTPResponseStatus.ok
response.setBody(string: "{\"status\":\"Invalid request\"}")
} else {
if (username ?? "").characters.count >= 8 || (username ?? "").characters.count <= 24{
if (password ?? "").characters.count == 32 {
if (username ?? "").rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil && (password ?? "").rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil {
response.status = HTTPResponseStatus.ok
let queryResult: [[[String: String]]] = executeQuery(query: "SELECT * FROM users WHERE username = ?", parameters: [username ?? ""])
if (queryResult.count == 1) {
if ((queryResult[0][2]["password"] ?? "") as String == password ?? "") {
response.setBody(string: "{\"status\":\"Authentication request succeeded!\"}")
} else {
response.setBody(string: "{\"status\":\"Invalid request\"}")
}
} else {
response.setBody(string: "{\"status\":\"Invalid request\"}")
}
} else {
response.status = HTTPResponseStatus.ok
response.setBody(string: "{\"status\":\"Invalid request\"}")
}
} else {
response.status = HTTPResponseStatus.ok
response.setBody(string: "{\"status\":\"Invalid request\"}")
}
} else {
response.status = HTTPResponseStatus.ok
response.setBody(string: "{\"status\":\"Invalid request\"}")
}
}
response.completed()
})
server.addRoutes(routes)
do {
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Network error thrown: \(err) \(msg)")
}
My DBOperations.swift:
import MySQL
let sqlHost = "127.0.0.1"
let sqlUser = "app_authentication"
let sqlPassword = "testing01"
let sqlDB = "app_authentication"
func executeQuery(query: String, parameters: [String]? = nil) -> [[[String: String]]] {
var fStore = [[[String: String]]]()
let mysql = MySQL()
let connection = mysql.connect(host: sqlHost, user: sqlUser, password: sqlPassword, db: sqlDB)
guard connection else {
print(mysql.errorMessage())
return [[[String: String]]]()
}
defer {
mysql.close()
}
let fQuery = MySQLStmt(mysql)
_ = fQuery.prepare(statement: query)
if parameters != nil {
for parameter in parameters! {
fQuery.bindParam(parameter)
}
}
_ = fQuery.execute()
let fResults = fQuery.results()
_ = fResults.forEachRow {
e in
var fSubStore = [[String: String]]()
for i in 0..<e.count {
let fFieldName: String? = fQuery.fieldInfo(index: i)!.name
fSubStore.append(["\(fFieldName!)": "\(e[i]!)"])
}
fStore.append(fSubStore)
}
return fStore
}
You need to save both of these files inside the Sources
folder of your package.
To build and run your app, run this in the root of your package directory:
swift build
.build/debug/backend-api
Now it is time to test if everything works:
So: this is how you can build authentication APIs with Swift. Please bare in mind that all code examples given are NOT production ready. I know that it works, but I can definitely already see some points of improvements - too bad that I don't have time to fix that. This demo project is soon to be expected on my GitHub. Thanks for reading and have a nice day ;)