Making a serverless Swift function with Fastly and Upstash
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
Fastly’s Compute@Edge is a service which allows you to build and deploy serverless applications at the edge. These so called edge functions are applications which are deployed to a number of regions across the world so that they are as close to users as possible. On top of the benefits which serverless computing provides, such as not having to maintain the server infrastructure, edge functions are extremely quick and have very low latency.
I have been wanting to try this service out for a while as I believe edge functions are the future of server side applications. I recently came across the awesome work from Andrew Barba, who has made a runtime which allows developers to write Fastly Compute@Edge functions in Swift. In this article we’ll go through how to use the runtime to make a URL shortener service.
Creating a Swift package
Let’s get started by creating an executable Swift package:
swift package init --type executable --name URLShortener
The command above will create an empty Swift package with no dependencies and an executable target.
Next, we need to define the Compute package as a dependency to the URLShortener
target and an executable product called URLShortener
:
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "URLShortener",
platforms: [.macOS(.v11)],
products: [
.executable(name: "URLShortener", targets: ["URLShortener"])
],
dependencies: [
.package(url: "https://github.com/swift-cloud/Compute", from: "2.8.0")
],
targets: [
.executableTarget(
name: "URLShortener",
dependencies: ["Compute"]
)
]
)
Now that Compute
is available to the URLShortener
target, we can modify its entry point (main.swift
) to handle incoming requests. The simplest form of a Fastly Compute@Edge handler consists of an awaited call to onIncomingRequest
, a method from the Compute library which takes in a closure with two parameters (a request and a response).
This closure provides an async
context, which means that structured concurrency can be used within it. The request parameter provides context on the incoming event and the response parameter is responsible for handling the edge function’s return data and status codes.
import Compute
try await onIncomingRequest { request, response in
try await response.status(200).send("Hello World!")
}
Local development
To run Swift code in Fastly’s Compute@Edge service we must first compile the URLShortener
executable product to WebAssembly. To do so, we can use SwiftWasm’s fork of the Swift official toolchain, which can be installed using swiftenv as follows:
swiftenv install "https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.7.1-RELEASE/swift-wasm-5.7.1-RELEASE-macos_$(uname -m).pkg"
After the download completes, we can tell swiftenv to use the newly downloaded wasm-5.7.1
toolchain in the current directory:
swiftenv local wasm-5.7.1
If you would like to learn more about installing and managing multiple Swift toolchains, please refer to this article in my blog.
We can now build the Swift package and generate a WebAssembly file from the URLShortener
executable product using swift build
:
swift build -c debug --triple wasm32-unknown-wasi
If the build command succeeds, a new file called URLShortener.wasm
should appear in the /.build/debug
directory.
To run the edge function locally we can now use the compute serve
command from the Fastly CLI and give it the generated URLShortener.wasm
file:
# Install the Fastly CLI if needed
brew install fastly/tap/fastly
# Run a development server
fastly compute serve --skip-build --file ./.build/debug/URLShortener.wasm
The Fastly CLI should output the URL for the local server it has spun up (e.g. http://127.0.0.1:7676
). If we now make a GET
request to that URL, we will be greeted with a response of "Hello World!"
and status code of 200
.
Note that whenever you make any changes to your application you will have to kill the server, re-build and start the server again to get the latest changes.
Creating an Upstash redis database
URL shorteners work by storing a set of entries in a database as key/value pairs. Whenever a request is received with a path that matches one of these keys (the shortened name of the URL), then the service immediately redirects to the URL stored in the value for the matched entry. For example, for a database with an entry of key newsletter
and value https://polpiella.dev/newsletter
, any calls to https://domain/newsletter
should redirect to https://polpiella.dev/newsletter
.
A good option for a URL shortener edge function’s database is Upstash. Upstash is a serverless redis data platform which, similarly to Fastly’s Compute@Edge service, can be deployed globally so that data is always as close to the user as possible.
After signing up to Upstash, creating a new redis database is straightforward:
- Navigate to the console.
- Click on the ‘Create Database’ button.
- Give the database a name and select ‘Global’ as the deployment region. This will deploy the database to multiple regions around the world.
Now that the database is ready, we can add an entry of key newsletter
and value https://polpiella.dev/newsletter
through Upstash’s CLI by using a redis SET
command:
Environment variables
To query the new Upstash database we’ve just created, we’ll use Swift Cloud’s Upstash library, which interfaces with Upstash’s REST API under the hood. We will need to provide the library with a REST API token and the database’s endpoint from Upstash’s console. We don’t want to hardcode these values as strings in our code for security reasons and we should make them available to our edge function as environment variables.
To do so, let’s create a file called secrets.json
and add the following content to it:
{
"REDIS_HOST_NAME": "your_database_host_name_here",
"REDIS_REST_TOKEN": "your_redis_rest_token_here"
}
The two values above can be copied from the database’s view in Upstash:
We need to map this file to a dictionary which Fastly can understand through the edge function’s configuration. Create a fastly.toml
file and add the following contents to it:
language = "swift"
manifest_version = 2
[local_server]
[local_server.dictionaries]
[local_server.dictionaries.secrets]
file = "secrets.json"
format = "json"
The configuration file above creates a dictionary called secrets
on the local server with the contents of the secrets.json
file.
The ConfigStore object from the Compute Swift package is responsible for retrieving data for any specific dictionaries it can find. In this case, it should retrieve the values for the secrets
dictionary and return an internal error (status code 500
) if it can’t.
import Compute
try await onIncomingRequest { request, response in
let secrets = try ConfigStore(name: "secrets")
guard let upstashHostName = secrets.get("REDIS_HOST_NAME"),
let upstashToken = secrets.get("REDIS_REST_TOKEN") else {
try await res.status(500).write("Missing secrets...")
return
}
try await response.status(200).send("Hello World!")
}
Building the package and running the server again should still work in the same way as it did before.
You should not commit the file with your secrets on it to source control as it will be used for local development only. Once you deploy the function, you will need to create a dictionary called
secrets
with the same key-value pairs as before in either Fastly or Swift Cloud (see the Deploy section below for more information). I would highly recommend you add thesecrets.json
file to your.gitignore
to prevent it from ever being committed.
Retrieving path parameters
Before retrieving a URL for a given key, we need to find out which URL the user has asked for using the shortened name. As I said earlier, we need to get the first path parameter from the request’s URL and use it to query Upstash for a destination to redirect to. Furthermore, we only want to listen for routes with a single path parameter on them and return a 404
not found error in any other case.
Compute provides a routing mechanism very similar to Vapor’s routing-kit which allows us to implement the logic we need for the URL shortener service. The app can define routes and provide specific handlers for each of these through a Router
instance. If you come from a web development background, this is very similar to how frameworks such as hono or express work.
The URL shortener API will have a single route and will only listen for 'GET'
request on routes with a single path component:
import Compute
// 1
let router = Router()
// 2
router.get("/:key") { request, response in
// 3
let secrets = try ConfigStore(name: "secrets")
guard let upstashHostName = secrets.get("REDIS_HOST_NAME"),
let upstashToken = secrets.get("REDIS_REST_TOKEN"),
let key = request.pathParams.get("key") else {
try await response.status(500).write("Missing secrets...")
return
}
// 4
do {
// Hardcoded for now...
let redirectPath = "https://polpiella.dev/newsletter"
try await response.redirect(redirectPath, permanent: true)
} catch {
try await response.status(404).write("Could not find link for a key with name: \(key)")
}
}
// 5
try await router.listen()
The new routing mechanism requires a bit of a rewrite as it’s no longer using the onIncomingRequest
function and makes use of a Router
instance instead. Let’s take a closer look at the changes to main.swift
:
- Create an instance of
Router
where the available paths will be defined. - Define a route with
GET
method and a single path parameter called key. This key can be retrieved through therequest.pathParams
property. - Retrieve all secrets and the
key
path parameter and return with a server error if any of these values are missing. - If possible, redirect to a URL (which is hardcoded for now) or fail with a not found error if not. For SEO reasons, the API should do a permanent redirect, which will set the status code to
308
. - Finally, make the router listen to incoming requests.
Retrieving a value from Upstash
Now that all secrets are available and routing is set up, the retrieved key from the request’s path can be used to get values from Upstash through Swift Cloud’s Upstash library. Let’s add it as a dependency to URLShortener
:
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "URLShortener",
platforms: [.macOS(.v11)],
products: [
.executable(name: "URLShortener", targets: ["URLShortener"])
],
dependencies: [
.package(url: "https://github.com/swift-cloud/Compute", from: "2.8.0"),
.package(url: "https://github.com/swift-cloud/Upstash", branch: "main")
],
targets: [
.executableTarget(
name: "URLShortener",
dependencies: ["Compute", "Upstash"]
)
]
)
The Upstash library can then be imported and used to retrieve the URL from a given key:
import Compute
let router = Router()
router.get("/:key") { request, response in
// 3
let secrets = try ConfigStore(name: "secrets")
guard let upstashHostName = secrets.get("REDIS_HOST_NAME"),
let upstashToken = secrets.get("REDIS_REST_TOKEN"),
let key = request.pathParams.get("key") else {
try await response.status(500).write("Missing secrets...")
return
}
let client = RedisClient(hostname: upstashHostName, token: upstashToken)
do {
let redirectPath: String = try await client.get(key)
try await res.redirect(redirectPath, permanent: true)
} catch {
try await response.status(404).write("Could not find link for a key with name: \(key)")
}
}
try await router.listen()
Let’s add two more values to the Upstash database (blog: https://polpiella.dev
, gh: https://github.com/polpielladev
) and test the implementation works:
Deploying
I won’t go into too much detail on how to deploy the edge function as Andrew Barba has created a delightful blog post explaining thoroughly how to do so. In a nutshell, there are two ways to deploy a Swift Fastly Compute@Edge function:
- Using Swift Cloud. This is by far the easiest way of deploying the edge function. It allows you to connect a GitHub repo and handles all the building and deploying for you on every push to a specific branch. You must proceed with caution as it is still on beta and some functionalities, such as setting custom domains, are not yet available.
- Using Fastly. You can deploy directly to Fastly, but it requires some extra work. Andrew Barba’s blog post shows you an example of a GitHub action which deploys the function on every push to main.
I made a template!
I decided to put together a template repository to make it easier for me to start developing a new Fastly edge function with Swift.
This was completely inspired by the demo project Andrew Barba put together for the Serverside Swift Conference, so all credit to him, I just collated a lot of the information and put together a template. If you’d like a template with more examples, the Swift Cloud starter-kit template is also available.