Introduction to Swift on Server

Swift

Introduction to Swift on Server

Why Swift?

To the Swift on Server to quote site:

The goal of the Swift project is to create the best available language for uses ranging from systems programming, to mobile and desktop apps, scaling up to cloud services.

(The goal of the Swift project is to create the best available language for applications ranging from system programming to mobile and desktop applications to cloud services.)

Strict typing, async/await, and actors are key features that make it easier to build and maintain correct programs. Some characteristics that make Swift particularly suitable for server applications are the low resource requirements in terms of processor load and memory, the fast start-up time and the deterministic performance thanks ARC and the lack of JIT.

The system load compared to node.js can be around Up to 90% be less. It also allows iOS developers who are already familiar with Swift to act as backend developers.

Shared DTOs

Another advantage arises in the context of client-server applications. Code can be shared between the server and the client app. For example, by sharing Data Transfer Objects (DTOs), we make our API type-safe. The easiest way to do this is to create a separate Swift package, which can then be imported into both places.

Why Vapor?

There are a variety of frameworks for using Serverside Swift. Steam is currently the most popular. It builds up SwiftNIO on and therefore benefits from improvements made by Apple and the Swift Server Group come. The API is very swift and focuses on simplicity and combinability. Vapor has an excellent Documentation and supports unit testing both in memory and with a live environment.

Example

Let's look at an example of a simple Vapor application with a single endpoint.

import Vapor let app = try Application(.detect()) defer { app.shutdown() } app.get("hello") { req in return "Hello, world." } try app.run()

that's all.

Command line

To create a new project we can use the command line tool vapor which can be installed via homebrew.

vapor new demo

The tool walks us through the setup process and allows us to choose additional frameworks like Fluent, which we'll get to later.

Content

Vapor's Content API is based on Codable. The conformance of a struct with Content allows to use them when decoding requests or encoding responses.

struct User: Content { let username: String let email: String }

In addition to JSON, Vapor supports multipart, URL-encoded form, plain text, and HTML. Speaking of HTML, Vapor even has its own template language with a Swift-inspired syntax called Leaf and there are already Vapor-based ones content management systems. But that should not be the topic of this blog post.

routes

Routes define how HTTP requests are handled. As in the example above, we can add a route by passing in a block of code.

app.get("hello") { req in return "Hello, world." }

Parameters are preceded by a : .

app.get("hello", ":name") { req -> String in let name = req.parameters.get("name")! return "Hello, \(name)!" }

It also supports routing anything (*) and catchall (**) path components.

Via the <strong>integrated datalog</strong> the measuring values can be stored on the humimeter RH5 paper moisture meter and additional data can be added. You also have the possibility to use the Autolog function. This function automatically saves measuring values in adjustable time intervals. content we can access the body of a request.

let user = try req.content.decode(User.self)

The strength of Vapor's routing approach lies in its combinability. For example, we can group routes under a common prefix.

let users = app.grouped("users") users.get { req in /*...*/ } users.post { req in /*...*/ } users.delete(":id") { req in /*...*/ }

Groups can also be used to add middleware for rate limiting or authentication. Later more.

fluent

Fluent is a framework for Object-relational mapping. It supports PostgreSQL, SQLite, MySQL, and MongoDB More.

Model

Objects are described by regular Swift classes using Model are compliant. The keys are defined via property wrappers.

final class User: Model { static let schema = "users" @ID(key: .id) var id: UUID? @Field(key: "username") var username: String @Field(key: "email") var email: String }

Parent/child or sibling relationships can be defined directly in the class, allowing for easier access.

final class Planet: Model { // ... @Parent(key: "star_id") var star: Star } final class Star: Model { // ... @Children(for: \.$star) var planets: [ planets] }

We could also contact the object Content Adapt to use it directly in our API, but since we share a common DTO package it's not necessary in our case.

Migration

Migrations can be thought of as version control for the database. Within a migration we can set up or change one or more schemes. In addition, we implement an inverse function to be able to undo the changes.

struct Create: AsyncMigration { func prepare(on database: Database) async throws { try await database.schema("users") .id() .field("username", .string, .required) .field("email", .string, .required) .unique(on: "username") .create() } func revert(on database: Database) async throws { try await database.schema("users").delete() } }

We then add this migration to the application's migration list (app.migrations.add), which allows us to invoke it from the command line.

vapor run migrate

It is important to repeat this after every schema change. Alternatively, the migration can also be carried out automatically at the start using the --auto-migrate flags or try await app.autoMigrate().

use of objects

To add something to the database, we create an object of our class and call the create(on:) Function specifying a database.

let user = try User( username: dto.username, email: dto.email ) try await user.create(on: req.db)

Query

Fluent's Query API is reminiscent of Swift's Collection API. For example, to search for a user with a specific name, we can run the following query.

let user = try await User.query(on: req.db) .filter(\.$username == "myuser") .first()

If we already know the ID of the object, we can use it directly find use method.

let user = try await User.find(id, on: req.db)

Fluent supports a variety of operators for the filter function. For example, we can filter for all users whose name begins with an “m”.

let users = try await User.query(on: req.db) .filter(\.$username =~ "m") .all()

Fluent also supports joins, chunking, aggregates, and More.

Middleware

Middleware can be injected between the client and a vapor route handler. A middleware can perform operations on incoming requests and outgoing responses. Typical use cases for a middleware are rate limiting or user authentication.

authentication

Fluent comes with built-in middleware to handle user authentication. To use them, we conform to the User- class too ModelAuthenticatable, by providing KeyPaths to the username and password hash, and the body of the function verify(password:) to implement. For crypto, we'll use the one that comes with Vapor Bcrypt.

final class User: Model { // ... @Field(key: "password_hash") var passwordHash: String } extension User: ModelAuthenticatable { static let usernameKey = \User.$username static let passwordHashKey = \User.$passwordHash func verify (password: String) throws -> Bool { try Bcrypt.verify(password, created: self.passwordHash) } }

When creating new users, we store the password hash in the object. Bcrypt does the salting automatically, so we don't have to worry about it.

let user = try User( username: dto.username, email: dto.email, passwordHash: Bcrypt.hash(dto.password) ) try await user.create(on: req.db)

Now we can insert the middleware into our route.

let passwordProtected = routes.grouped(User.authenticator()) passwordProtected.post("login") { req -> String in let user = try req.auth.require(User.self) // ... }

When a client makes a request (and provides the correct credentials), we can use auth access the user object.

let user = try req.auth.require(User.self)

Vapor also supports user tokens and JWT's.

In practice

To test Vapor extensively, I created a demo project. I settled on the idea of ​​a seat reservation tool. It allows users to login and create or join locations, where they can then reserve a spot for one or more days, or see their colleagues' reservations. The server offers a total of 19 API endpoints, uses both user/password and user token authentication, performs rate limiting, has custom query validation, and stores the data in a SQLite database. The entire project is just over 1000 lines of code.

In addition, I have developed a client app for iOS, iPadOS and macOS. Sharing the DTOs between the server and the client app turned out to be very convenient. The power of extensions in Swift allowed me to tailor the DTOs to the situation at hand, such as this Content-Log on the server.

extension DTO.User: Content {}

Since the DTOs already Codable are, I didn't have to add anything else.

Conclusion

As an iOS developer, I found Vapor amazingly easy to learn. The excellent documentation with many examples is very helpful. The database framework and the built-in support for authentication make even complicated applications possible with manageable effort.

So my recommendation is to try Vapor with us when building a backend for a new mobile app. But Vapor can also be a good choice for larger backend applications thanks to its very excellent performance characteristics.

Melvin Gundlach

Author

Melvin Gundlach
Advanced developers