Swift on Server - Bootstrapping

If it isn't obvious by the title, this series of posts will be about writing Swift code that runs on the server. Some benefits of the language: a strict type system, memory safety, multithreaded, and easy to read syntax. Its still a young ecosystem for server side development so if you're looking for something with a mature ecosystem you should probably pick a different language/framework for server side development. That being said, it is still very capable for server side development. You can read more about Swift on Server here.

I'm not going to walk through the basics of the language but anytime that something server side specific comes up I'll try to explain what is going on the best I can. You can find directions on installing and language tour along with lots of other resources at Swift.org.

Swift has a package system similar to other languages. Each package can have multiple targets which are just folders of source code and resources. Packages can also have products, which are the final product from the compile process. These can be libraries or executables.

For server side development, you would usually make two products, a library and an executable. The executable is usually just a few lines of code to get the server running from the library. The library is a bulk of your code that has all your business logic and tests. All of these package infomation is stored in a file called Package.swift 🤯. In the console app of your choice just make a new folder and run the following.

mkdir MyProject
cd MyProject
swift package init

You can edit the code in whatever IDE you'd like - personally I use Xcode and VS Code on macOS. You can go here for learning how to setup your IDE of choice. On macOS Xcode is preferred but you can do whatever you want ¯\_(ツ)_/¯.

Once you have the code open in your IDE we're going to modify a few things. First lets update the Package.swift file, we're going to add a dependency on a library called Vapor, set up our targets and products as described above and get rid of the tests.

Vapor is an amazing library for building server side applications in Swift and handles a lot for you including routing, database access, auth, templating, queues, and more.

Package.swift

// swift-tools-version:5.3
import PackageDescription

let package = Package(name: "NoDevExample")

package.platforms = [
    .macOS(.v10_15)
]

package.dependencies = [
    .package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.0.0"))
]

package.targets = [
    .target(name: "Server", dependencies: [
        .product(name: "Vapor", package: "vapor")
    ]),
    .target(name: "Run", dependencies: [
        .target(name: "Server")
    ]),
]

package.products = [
    .library(name: "Server", targets: ["Server"]),
    .executable(name: "Run", targets: ["Run"]),
]

Next our file structure will look like this:

Folder
- README.md
- Package.swift
- Package.resolved
- Sources
    - Run
        - main.swift
    - Server
        - Server.swift

The goal of this configuration is to keep all the business logic in the Server library product and make the Run product as light weight as possible. The two files will contain the following.

Server.swift

import Vapor

public func application() throws -> Application {
    var environment = try Environment.detect()
    try LoggingSystem.bootstrap(from: &environment)
    let application = Application(environment)
    
    // Insert additional configuration here
    
    return application
}

main.swift

import Server

let app = try application()
defer { app.shutdown() }
try app.run()

For Server.swift, we are importing the Vapor library then creating a function that the Run executable will call. We mark the function as public so it can be accessed outside the Server library. By default in Swift all functions and properties are marked as internal so they are not accessible outside the module. Internal is inferred if another modifer is not present.

Inside the application function we are detecting the current environment, so any environment variables and the contents of a .env file can be accessed in your code. This is pretty similar to other web frameworks like Express or Rails. We then start a logging system so application logs can be captured and sent somewhere, currently they are sent to the terminal. You can read more about logging here. We then create the application instance and return the instance from the function.

In main.swift we access this function by importing the Server library, call the function, defer the shutdown, and then run the application. The reason the file is called main.swift is because executable targets require a main file and that is the entry point for the executable. From now on we shouldn't have to modify main.swift and will only be modifying files inside the Server library target.

To run the server you can either run it in your IDE by selecting the "Run" target or you can run it from the console by entering the following:

swift run Run

You'll see a bunch of output about fetching dependencies and compiling but finally you should see:

[ NOTICE ] Server starting on http://127.0.0.1:8080

Congrats! You've bootstrapped a server with Swift. It doesn't really do anything but its a good starting place. All code for this example can be found here.

Subscribe to nodev

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe