Overview
Swift as a language has grown from infancy quite rapidly since its release in 2014. The programming language, initially a proprietary language, was released to the community as an open-source project with version 2.2 in late December 2015.
At the heart of the open-source language is the community’s goal for the language:
To create the best available language for uses ranging from systems programming to mobile and desktop apps, scaling up to cloud services.
Swift Language, source: http://swift.org/about/
So what’s the big deal?
Initially Swift only supported the development of desktop and native mobile applications for devices running iOS and Mac OS. However, over the years, support for Unix based architecture and more recently official support for windows in Swift 5.3 (latest at time of writing), has enabled the development of applications and solutions in Swift to reach a much wider audience and developer base.
If you would like to read more on the port of Swift to Windows, I suggest reading through their blog post on the official Swift site.
Why Swift?
So you may be asking, why should I be using Swift for my server-side development? Why is it any better than Java, C#, NodeJS (JS/TS), Python?
Swift has many advantages when developing, some of these include uninitialised variable prevention, overflow checking, and automated memory management thanks to ARC. As well as this, the language design and syntax promote swift (fast) development with high maintainability and readability. Swift being a young language, it still has a lot of ways to go and will continuously see improvements to its performance and feature base.
You can read up on the language itself and look into the pros and cons. But one of the main advantages of building in Swift for server-side and full-stack is the reusability of code across your mobile, web, and server-side developments, this allows for the sharing of business logic, models, and validation across your project. This also enables language familiarity across your codebase to promote work in a cross-functional development team.
Another big advantage is if you are already an experienced iOS developer, the transition to developing APIs and backend services for your application is seamless, with no need to learn a new language.
The transition from other languages such as JavaScript, TypeScript, Kotlin, etc. is also very simple as the language is designed for fast development and it is easy to learn, sharing a lot in common with those aforementioned.
Vapor
Vapor is a web framework built for server-side Swift development. It’s built on Apple’s SwiftNIO: a non-blocking, event-driven architecture written in Swift which encourages type-safe, expressive, and protocol-oriented development.
We will be covering Vapor in this tutorial. Vapor is one of the most used web frameworks for Swift server-side development. However, other options do exist but a lot of them are no longer supported or have died out. If you wish to explore them as well here are a couple: Kitura (IBM no longer supports this) and Perfect.
Prerequisites
Before we dive in and get our hands dirty, you will need to have the following setup on your system:
- Swift > 5.2
- IDE or Text Editor with Swift support (optional)
Setting up Vapor
We will be installing Vapor in the next section of the tutorial using Vapor Toolbox for CLI shortcuts, which is only available for macOS and Linux. If you are on Windows you can still use the Vapor framework but will have to manually set up your project. You can download the API Template from Vapor’s GitHub repository.
To install the Vapor Toolbox, you can use brew or apt-get.
# macOS
brew install vapor
# linux
git clone https://github.com/vapor/toolbox.git
cd toolbox
git checkout <desired version>
make install
# commands
vapor --help
Now that we have Vapor installed, we can use the vapor command. Let’s start by creating our project.
Out of the box, Vapor supports the use of database drivers, we will not be using these in the tutorial so if prompted when creating your new project to install any extra packages you can answer no.
vapor new shopApi
cd shopApi
vapor build
We are creating a new project here named shopApi. In this tutorial, we will be creating a simple shopping app which will display a list of products available to a user.
If you would like to read more about the differences between dynamic linking and static linking you can read further at https://swift.org/package-manager/.
The rest of the tutorial will focus on development using the macOS environment. All code snippets and Vapor CLI commands will carry across to Linux as well; however, running the application will be done through the terminal instead of through Xcode.
Running our Swift application from the command line:
vapor build && vapor run
If you are on macOS you can run:
vapor xcode
This will open the project in Xcode where you can use the normal build and run buttons you are familiar with for mobile development. You can also set breakpoints inside your code to debug your application.
Building your first route
It’s time to get our hands dirty. Let’s start by creating our first route for our basic shopApi.
Endpoints
First, let us define some constants for our endpoints. Create a new Constants.swift file with the following constants.
// Constants.swift
import Vapor
public enum Constants {
public enum Endpoints: String {
// MARK:- Shop Endpoints
case products = "products"
case singleProduct = "productId"
var path: PathComponent {
return PathComponent(
stringLiteral: self.rawValue
)
}
}
}
Here we define two constants for the routes we will be using. You will see later how we use the “:productId” to provide a path template for an endpoint to retrieve a single product.
Routes
Now open the routes.swift
file and replace the existing routes with a new route for the two endpoints: products and products/:productId.
// routes.swift
func routes(_ app: Application) throws {
app.get(Constants.Endpoints.products.path) {
req -> String in
return "All Products"
}
app.get(Constants.Endpoints.products.path,
Constants.Endpoints.singleProduct.path) {
req -> String in
return "Single Product"
}
}
When we define the endpoint for products/:productId, Vapor uses the colon (:) as an identifier for a URL path parameter. We can access this inside the function using the following:
let param = req.parameters.get("productId")
or
# Using type casting
let param = req.parameters.get("productId", as: Int.self)
Now if we run the project (Run in Xcode) you should see the following:
vapor build && vapor run
Building project...
[8/8] Linking Run
Project built.
[ NOTICE ] Server starting on http://127.0.0.1:8080
Navigate in a browser or using Postman to http://127.0.0.1:8080/products
you should see the server respond with “All products”.
Congratulations, you now have your first endpoints setup using Vapor.
Models and business logic
Let’s now explore the reusability of models and business logic across our server-side and application codebase. We discussed earlier that this is one of the main advantages of using Swift across your entire stack.
Create a new file called Product.swift in the Models folder (create this if it does not exist).
In a real-world scenario, this would be developed as part of a common framework or module and imported into the project for code re-usability across your Swift Stack. For simplicity in this tutorial, we will not cover creating a Swift package for use in your project.
// Product.swift
import Foundation
struct Product: Codable {
// MARK:- Properties
private var id: Int
private var name: String
private var price: Double
private var description: String
// MARK:- Lifecycle
init(id: Int, name: String,
price: Double, description: String) {
self.id = id
self.name = name
self.price = price
self.description = description
}
// MARK:- Codeable Methods
func asJsonString() -> String {
let codedProduct = try! JSONEncoder().encode(self)
return String(data: codedProduct, encoding: .utf8)!
}
}
In this product model file, we create a basic data structure for our products and conform it to the Codable type. This allows us to serialise our object both in our server-side and client-side applications.
We also have an instance helper method here to serialise our codeable product to a string for passing as a response body.
This product model will now be useable in both our server-side and client-side applications, and any changes that are made to this data structure in our common framework will be reflected across both applications, reducing the development effort when updating and ensuring alignment between client and server.
Services
Services in Vapor can be registered as a part of the application to act as the business logic layer. Now that we have created our model for the shop, let’s create a service which will serve some dummy products for our shop.
Create a new file called ProductService.swift inside a Services folder (Create this folder if it does not exist).
// ProductService.swift
import Foundation
import Vapor
class ProductService {
var products: [Product] = []
//
// Initialise our products.
// This is a mock that returns hard coded products
//
init() {
// Create some dummy products
let toucan = Product(
id: 1,
name: "Toucan",
price: 50.00,
description: "Famous bird of the zoo"
)
let elephant = Product(
id: 2,
name: "Elephant",
price: 85.00,
description: "Large creature from Africa"
)
let giraffe = Product(
id: 3,
name: "Giraffe",
price: 65.00,
description: "Long necked creature"
)
// Add them to our products array
products.append(toucan)
products.append(elephant)
products.append(giraffe)
}
//
// Filter our products array and get by matching id
//
func getProductById(id: Int) -> Product? {
return products.first(where: { $0.id == id })
}
//
// Return all products
//
func getProducts() -> [Product] {
return products
}
}
In this simple service, we are instantiating 3 products and storing them in our products array. This logic in practice would be replaced with a database implementation to store and access our products. However, for now, we are just hardcoding the values stored in our shop.
There are two methods in this service, one which we will use to return all the products that our store contains, and the other to return a product by its ID. These two methods match up with our current routes.
Registering our service
Now to access our service from our routes or controllers, we must register them in the Vapor application. To do this let’s add this extension to the bottom of our ProductService.swift file.
// MARK:- Services implementation
extension Application {
//
// Register our product service with the Vapor
// application.
//
var productService: ProductService {
.init()
}
}
In Vapor 4, you now register your services as extensions of either the Application or Request objects. This exposes the services as properties and allows for easier use in our routes and controllers.
This allows us to use our ProductService methods by calling:
app.productService.getProducts()
Codable Extension
Lastly, before we hook everything up and can start responding with products for our API, we must write an extension to serialise the list of our products to a JSON string. Open the Product.swift file and at the end of the file add the following extension.
// Product.swift
struct Product: Codable {
...
...
}
extension Array {
typealias CodableArray = [Product]
//
// Encode our array as a Json string.
//
func codableArrayAsJsonString() -> String {
if let array = self as? CodableArray {
let codedArray = try!
JSONEncoder().encode(array)
return String(
data: codedArray,
encoding: .utf8
)!
}
// This is where we can add some error handling,
// But for now we will just return blank
return ""
}
}
This extension allows us to encode a list of our products to a JSON string for use as the body in a response object.
Putting it all together
Now that we have built our business logic and models, we can now start responding to our client with the products our shop offers. Let’s open the routes.swift file again and modify our /products route.
All Products Route
//
// Register an endpoint for /products
//
app.get(Constants.Endpoints.products.path) {
req -> Response in
// Call our product service to get our products
let products = app.productService.getProducts()
// Return a serialised list of products
return .init(status: .ok,
version: req.version,
headers: [
"Content-Type":
"application/json"
],
body:
.init(string:
products.codableArrayAsJsonString()
))
}
Here we are calling our service that we registered earlier on our application object, and retrieving the list of products our shop offers.
We are then creating a response object which will return the encoded JSON list of our products in the body using our extension: codableArrayAsJsonString()
.
Single Product Route
Let’s add modify our final route, which takes in a url-path parameter for the productId and returns the product if it is found.
//
// Register an endpoint for /products/:productId
//
app.get(Constants.Endpoints.products.path,
":\(Constants.Endpoints.singleProduct.path)"
) {
req -> Response in
// Get our productId from the url-path parameter
let productId = req.parameters.get(
Constants.Endpoints.singleProduct.rawValue,
as: Int.self
) ?? 0
// Call our product service to get the product by id
if let product =
app.productService.getProductById(id: productId) {
return .init(status: .ok,
version: req.version,
headers: [
"Content-Type":
"application/json"],
body:
.init(
string: product.asJsonString()
))
}
// Return no product found
return .init(status: .ok,
version: req.version,
headers: [
"Content-Type":
"application/json"
],
body: .init(string: "No product found."))
}
Here we get the productId from the url-path of the request and use it to call our service with the .getProductById()
method. This returns a single product matching the ID. We then encode the product as a JSON String and set it as the response body.
If no product is found we return a 404 Product not found.
Running the application
Finally, let’s run our application to see our API in its finished state.
If you call the http://127.0.0.1:8080/products
endpoint you should now see the following response:
[
{
"id":1,
"name":"Toucan",
"price":50,
"description":"Famous bird of the zoo"
},
{
"id":2,
"name":"Elephant",
"price":85,
"description":"Large creature from Africa"
},
{
"id":3,
"name":"Giraffe",
"price":65,
"description":"Long necked creature"
}
]
Now, now if you call the single product endpoint with a product id
http://127.0.0.1:8080/products/:productId
For example: /products/1
you should see the following response:
{
"id":1,
"name":"Toucan",
"price":50,
"description":"Famous bird of the zoo"
}
Congratulations, you now have a simple products API built entirely in Swift.
Next: Swift Stack: Become a Full Stack Swift Developer (Part Two)
In the next part, we will look at deploying our Swift server into the cloud using a CI/CD pipeline. We will also be looking at how to write tests and how we can run these as a part of our pipeline.
The source code for this tutorial is available at: https://github.com/justinwilkin/server-side-swift-tutorial-part-1