Build a BFF with Swift on Swift.cloud

One Step closer to Swift world domination...

Build a BFF with Swift on Swift.cloud

Build a BFF with Swift on Swift.cloud

A Backend For Frontend or BFF can solve a very important issue that Apple platform devs often run into. Too often backends or micro services are written in a language unfamiliar to Swift devs. This isn't a problem though, right? The backend code is maintained by another developer or team so it isn't your problem. But what happens when you only need a tiny bit of data but this requires you to make several network requests that each send back way more data than you need? This can be a problem especially for iOS where users' data is often constrained. Or, instead of getting JSON returning a string, what you really need is an Int? I could go on, but there are a lot of scenarios which could require a change to how or what data you are retrieving from the backend. Everything I am about to talk about could be done in the app code itself, but having a BFF simplifies the app code the app code can focus on things that your users care about, UX/UI.

Enough with the whys, here is the how with Swift Cloud and Swift WASM.

  1. Follow the direction in the compute repo readme to get started
  2. Create a local git repo git init
  3. Create a git repository on Github and follow their instructions for how to get your local repo into this remote repo.
  4. Create an account at https://swift.cloud and follow their instructions to connect your GitHub repo with the swift.cloud service
  5. Code your BFF in Swift

The first thing I like to do (and this is just a personal preference) is I like to find the @main struct and delete it and the entire file it lives in. Then I create a new file named main.swift. Swift 5.7, specifically SE-0343 now supports concurrency in top level code so a @main struct is no longer needed.

In your main.swift file add the following 3 lines of code.

import Compute

let router = Router()
try await router.listen()

Now update your git repo.

git commit -am "Swift code running on WASM"
git push

Congrats, if you properly setup your swift cloud account, you just deployed swift code running on a server! This code doesn't do much at the moment so let's change that.

We will start by creating a new file named Routes.swift and adding a static func to register our routes.

import Compute

struct Routes {
    static func register(_ router: Router) {
        // set routes here
    }
}

Now back in our main.swift file we will register the routes we will create in this file. Our main.swift file will now have 4 lines of code,

import Compute

let router = Router()
Routes.register(router)
try await router.listen()

The Router provided by the Compute package allows for the usual, Get, Post, etc.. but our BFF only needs Get. I'll show you to variations. Let's update our static Register method,

static func register(_ router: Router) {
        router.get("/fetch/:id", fetch)
        router.get("/fetchAll", fetchAll)
}

Now when your app makes an api request to /fetch/<id> and /fetchAll your BFF will know how to respond. Let's implement fetch and fetchAll to fix the compiler errors. We'll start with fetch,

// 1
struct OurItem: Decodable {
	let value1: String
	let value2: Int
}

// 2
struct OurModifiedItem: Encodable {
	let value1: String
	let value2: String
}

// 3
static func fetch(req: IncomingRequest, res: OutgoingResponse) async throws {
		// 4
		guard let id = req.pathParams["id"] else {
	    try await res.status(.badRequest).send(BackendError(message: "This request requires an ID"))
      return
    }
		let url = URL(string: "https://ourAPI.com/fetch/\(id)")
		var fetchRequest = FetchRequest(url)
		// 5
    fetchRequest.cachePolicy = .ttl(10, staleWhileRevalidate: 30)
    // 6
    let item: OurItem = try await fetch(fetchRequest).decode()
    // 7
    let modifiedItem = OurModifiedItem(value1: item.value1, value2: "\(item.value2)")
    // 8
    try await res.status(.ok).send(modifiedItem)
}

What do we have here.

  1. We create a Decodable struct that we will get from our backend's API via a fetch request.
  2. We create an Encodable struct that we will send to our app via this get request.
  3. All requests with the Compute router need to go though a method with this signature, having an IncomingRequest and OutgoingResponse parameters and being async and throwable with throws.
  4. We get our id from the request via this guard statement. If it is absent we respond with a .badRequest status.
  5. Since Swift Cloud is using Fastly behind the scenes, we are able to set a cache policy. We do that here to speed up future requests to this endpoint.
  6. We fetch and decode to this item property.
  7. We then create an OurModifiedItem with the data from OutItem. You'll notice that OurItem has an Int` value2 but our iOS app (the consumer of this BFF API is expecting value2 to be a String).
  8. Finally we send back an .ok status and we send our modifiedItem struct.

FetchAll will be very similar but it will introduce search parameters.

static func fetchAll(req: IncomingRequest, res: OutgoingResponse) async throws {
    // 1
    guard let query = req.searchParams["query"] else {
            try await res.status(.badRequest).send(BackendError(message: "FETCH_ALL_NO_QUERY_ERROR"))
            return
        }
    // 2
		let page = req.searchParams["page"] ?? "0"
		let url = URL(string: "https://ourAPI.com/fetchAll")
		var fetchRequest = FetchRequest(url)
    fetchRequest.searchParams = [
        "query" : query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
        "page" : page,
        "limit"  : "10"
    ]
    fetchRequest.cachePolicy = .ttl(10, staleWhileRevalidate: 30)

    let items: [OurItem] = try await fetch(fetchRequest).decode()
    let modifiedItems = items.map { 
        OurModifiedItem(value1: $0.value1, value2: "\($0.value2)")
    }

    try await res.status(.ok).send(modifiedItems)
}

You should notice that a lot about this route is the same as Fetch above but now we are pulling out search parameters from our request (see comment 1 and 2).

This is all great, but what if we want to test this before deploying? Let's run it locally. This is the most complex part of developing on Swift Cloud at the moment and hopefully this will get simplified in the future, but for now follow these steps and you will be good to go. The following steps assume you have Homebrew installed on your Mac.

  1. Install the Fastly CLI
brew install fastly/tap/fastly
  1. Install Carton
brew install swiftwasm/tap/carton
  1. Use Carton to install the latest build of swift WASM
carton sdk install wasm-5.7.1-RELEASE
  1. Install Swiftenv
brew install kylef/formulae/swiftenv
  1. Use Swiftenv to set the local version of swift to use,
swiftenv local wasm-5.7.1
  1. Finally make a Make file with the following scripts (replacing "<TARGET-NAME>" with the name of your app's target in swift.package),
build: Sources
	swift build -c debug --triple wasm32-unknown-wasi

serve: build
	fastly compute serve --skip-build --file ./.build/debug/<TARGET-NAME>.wasm

With all this in place you can now run make build from the command line to verify that your project builds in the Swift WASM environment and you can run make serve to run the server locally.

If you want to better understand how this is all working via Swift and web assembly read this great post by Andrew Barba - Deploy server side Swift to Fastly.

Maybe you're ready to go beyond just a BFF and you want to develop your entire backend with Swift. This is also possible! If you do that you're going to need a database. Swift Cloud supports any hosted database that has an HTTP API. You can create a MySQL database with Planet Scale and get it running in Swift Cloud using this PlanetScale swift package. Or if you prefer MongoDB, you can use the MongoManager package.

Are BFFs the only use case for server side swift? NO. Is this the only server side Swift code at Barstool Sports? Also, no. Do you want to build Swift on the server? Check out the Barstool Sports jobs page. Do you want to talk more about server side swift or just swift in general? Find me on Twitter @TomRads.