Server-Side Swift… Served From The Client-Side
Hosting a Vapor Server From Your Native App Takes User Experiences Beyond a Platform
It’s WWDC16 and I (as a lucky attendee) am taking in all the Apple goodness of the week. So many awesome sessions to attend, special events, networking… something that I sorely miss now that WWDC has gone virtual.
One of the sessions catches my eye. IBM will be presenting instead of an Apple Engineer on something called Kitura. I listen in and am blown away how Swift (still a relatively new language and phenomenon) was branching beyond just Apple apps and into Server-Side development.
That session stuck with me well after WWDC. I even wrote a blog article on it later that year and then another article exactly three years later (seriously, look at the dates, they’re both October 14!). Server-Side Swift has never left my radar and has been a fun thing to run back to from time to time.
Present Day Eureka
Now, in 2025, a personal project I’m working on led to the idea of attaching a Vapor server… wait for it… as a feature within an app. It’s not unheard of, for sure. But I also haven’t personally seen or attempted it. While most Vapor tutorials, docs, and it’s CLI point to running as a stand-alone app, all the necessary parts come in Swift Packages.
Thus begins our journey into understanding how we can integrate Server-side Swift into our Client-Side apps, unlocking opportunities for your app you might not have considered possible, before.
Cold Cut Server
For the sake of this article, I want to build a modern Deli Counter Ticker macOS app. My app monitors which number is currently being served and has a way to display it on a screen that can be setup by the counter.
However, in case customers want to stray from the counter, it would be nice if (while on the store WiFi) they could scan a QR code that takes them to a site on their mobile device (iPhone or gasp Android). The site would display the number currently being served in real-time with live updates so they can see when their number is coming up or has been called.
If my app could serve this page, complete with a web-socket that broadcasts the current number, this could ease congestion by the counter and allow my customers to feel a bit freer to continue shopping for their goods. Ultimately, everyone could have a potentially happier experience.
Note: This is going to be more of a POC project, so don’t expect the most amazing looking code, the best practices, or sexiest UI. We have hungry customers and we wanna make sure they get their meats!
Cutting to the Meaty Part
Let’s breeze through the setup of our new macOS App. Go ahead and create one (I called mine DeliCounter).
Note: For those who’ve got cuts to slice, the completed project can be found on Github.
After your project opens, go to the project file and to our app target. We’ll first want to edit our App Sandbox:
While we’re still here, go to the project. We’ll need to pull in the package dependencies we will need for our server feature:
Create a new file called DeliCounterObservable.swift and place the following code in there:
import Observation
@Observable
class DeliCounterObservable {
var currentCustomerServed: Int = 0
private func broadcastToCustomers(text: String) {
for client in sockets {
client.send(text)
}
}
func nextCustomer() {
self.currentCustomerServed = self.currentCustomerServed + 1
}
func previousCustomer() {
self.currentCustomerServed = self.currentCustomerServed - 1
}
}
Go to ContentView and rename the file, the view, and references to the view to DeliCounterView. We’ll build our view to look like this:
import SwiftUI
struct DeliCounterView: View {
@State var dco: DeliCounterObservable = DeliCounterObservable()
var body: some View {
VStack {
Image(systemName: "fork.knife.circle")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Deli Now Serving:")
Text(String(dco.currentCustomerServed))
HStack {
Button("Previous") {
dco.previousCustomer()
}
Button("Next") {
dco.nextCustomer()
}
}
}
.padding()
}
}
Let’s pause for a second: What we’ve just done is made a very minimalist app that achieves the basic look and feel of a deli counter… counter. The deli could display this on a screen and probably get by. But why stop there?
Therefore, lets create one more new file called DeliCounterServer.swift. It’s in here we’ll build our server and begin to hook it up to our client-side app.
Making that Cheddar
We start by creating a simple struct DeliCounterServer: Sendable
. We make this Sendable because the fine folks at Vapor have already optimized for Swift 6 concurrency and we want to ensure we stay in bounds with that.
If you’ve ever used the Vapor CLI to spin up a new project, you can see the building blocks of app, configure, and routes. We’ll combine all of those into our new struct:
import Vapor
public struct DeliCounterServer : Sendable{
public func startAsync(// TODO) async throws {
let app = try await Application.make(.debug)
// TODO
}
private func configure(// TODO) async throws {
// TODO
}
private func routes(// TODO) throws {
// TODO
}
}
We’re also going to make an early assumption: we will hold references to web-socket connections in DeliCounterObservable. The main reason is because it will make broadcasting counter changes simple and local to where the value actually resides. In order to add and remove sockets from our server to our Observable’s collection, we’ll take in two closures:
public struct DeliCounterServer : Sendable{
public var addWS: @Sendable (WebSocket)->Void
public var removeWS: @Sendable (WebSocket)->Void
//...
}
Going back to our functions, the biggest difference from what we’re building from a CLI-server is that, instead of a @main starter function, we’ll be creating a basic async function to be called from our client. Again, we’re not looking to create a standalone server app, but instead a feature to be summoned:
public func startAsync() async throws {
let app = try await Application.make(.testing)
do {
try await configure(app)
try await app.execute()
try await app.asyncShutdown()
} catch {
app.logger.report(error: error)
try? await app.asyncShutdown()
throw error
}
}
In our routes, we will define two of them: one to host a web page that utilizes a connection to a web-socket, and another that serves a web-socket when called by the page. In the latter, we’ll also utilize our closures to add and remove sockets back in our Observable:
private func routes(_ app: Application) throws {
app.get { _ -> Response in
var headers = HTTPHeaders()
headers.add(name: .contentType, value: "text/html")
return Response(status: .ok, headers: headers, body: .init(string: htmlPage))
}
app.webSocket("deli-counter-socket") { req, ws in
addWS(ws)
ws.onClose.whenComplete { _ in
removeWS(ws)
}
}
}
Lastly, in our configure we simply define the hostname (0.0.0.0 as default) and port (8080 as default) and call our routes function:
private func configure(_ app: Application) async throws {
app.http.server.configuration.hostname = "0.0.0.0"
app.http.server.configuration.port = 8080
try routes(app)
}
We now have our basic server setup. The tricky part is to now hook it up to our client. Conversely, we’ll also need to hook our client side, real-time data to our server so our shoppers can stay informed.
Note: In the route that serves the HTML, you’ll notice I pass in a string called htmlPage. That’s because locally I copy and pasted HTML into a string. That’s WAY too big for me to add to this article, but you can find that and some other helper functions directly at the file on Github.
Stacking Our Sandwich
In order for changes to be broadcast, in realtime to our clients, we need to connect our web-sockets to the source of truth. In our app, the current number being served is found as an observed property in our DeliCounterObservable. Therefore, we need to add in there:
Something that posts the latest number to all current web-socket connections.
A container that holds all the current connections.
Ways to add and remove connections.
So first, let’s add a container for our web-sockets.
@ObservationIgnored
private var sockets: [WebSocket] = []
Now let’s add add and remove functions:
@Sendable func addWS(_ ws: WebSocket) {
sockets.append(ws)
ws.send(String(currentCustomerServed))
}
@Sendable func removeWS(_ ws: WebSocket) {
sockets.removeAll { $0 === ws }
}
Lastly, let’s create our broadcaster:
private func broadcastToCustomers(text: String) {
for client in sockets {
client.send(text)
}
}
And add calls to our broadcaster to where we increment and decrement our counter:
func nextCustomer() {
self.currentCustomerServed = self.currentCustomerServed + 1
broadcastToCustomers(text: String(self.currentCustomerServed))
}
func previousCustomer() {
self.currentCustomerServed = self.currentCustomerServed - 1
broadcastToCustomers(text: String(self.currentCustomerServed))
}
Completing our Sandwich
Now that our Observable and Server-Side-Server are prepped, it’s time to combine our ingredients in the view to make our Deli Counter Sandwich!
All it takes is adding a task modifier to our view:
.task {
let server = DeliCounterServer(addWS: dco.addWS, removeWS: dco.removeWS)
do {
try await server.startAsync()
} catch {
fatalError(error.localizedDescription)
}
}
If we run our application, we’ll initially see our macOS GUI. But if we open a browser and type in our machines hostname and port (e.g. http://192.168.86.20:8080/
), we should also see our counter web page:
As we increase and decrease the counter on the app GUI, the changes should appear in our browser! Yummy!
You’ve Been Served
This was certainly a quick and sloppy project (much like how I make my sandwiches). But now that you know it’s possible to host a server from your app, it kind of makes you wonder…
And, yes, you can do this from an iOS app, too.
The possibilities are as endless as there are sandwich creations! Granted, some should probably never be made, but nothing beats a good sandwich served right to you 😉.
Note: One last time, you can find the completed code on Github.