All Articles
CategoryBackend
Reading Time
21 min read
Published
2023-12-10
Word Count
1,748words

Grab a coffee — this one is a deep dive!

The Server-Side Swift Ecosystem: Vapor, Hummingbird, and Swift on Server

Summary

Swift on Linux, SwiftNIO fundamentals, a Vapor vs Hummingbird comparison, database integration, Docker deployment, and sharing models between client and server.

  • Swift is officially supported on Ubuntu, Amazon Linux, and CentOS; SwiftNIO provides event-driven networking.
  • Vapor has been mature since 2016 and includes the Fluent ORM; Hummingbird, since 2021, is minimal and faster.
  • Benchmark: Hummingbird 120K req/s (1.4ms p99), Vapor 85K req/s (2.1ms p99), Express 42K req/s.
  • SSWG packages: SwiftNIO/AsyncHTTPClient/SwiftLog are Graduated, PostgresNIO/RediStack are at Sandbox level.
The Server-Side Swift Ecosystem: Vapor, Hummingbird, and Swift on Server

Swift isn't just for iOS/macOS — it also runs on Linux and has a strong ecosystem for server-side development. The same language, the same types, on both client and server. In this guide we'll take a deep look at the Swift on Server ecosystem.

💡 Quick Note: The Swift Server Work Group (SSWG) is the official group, sponsored by Apple, that governs the server-side Swift ecosystem.

Table of Contents


Swift on Linux

Swift is officially supported on Ubuntu, Amazon Linux, and CentOS:

bash
1# Installing Swift on Ubuntu
2wget https://download.swift.org/swift-5.9.2-release/ubuntu2204/swift-5.9.2-RELEASE/swift-5.9.2-RELEASE-ubuntu22.04.tar.gz
3tar xzf swift-5.9.2-RELEASE-ubuntu22.04.tar.gz
4export PATH=$PWD/swift-5.9.2-RELEASE-ubuntu22.04/usr/bin:$PATH
5swift --version

Platform Differences

Feature
macOS
Linux
Foundation
Apple Foundation
swift-corelibs-foundation
Dispatch
libdispatch
libdispatch
Objective-C
✅ Available
❌ Not available
UIKit/AppKit
✅ Available
❌ Not available
Crypto
CryptoKit
swift-crypto
async/await

SwiftNIO Fundamentals

SwiftNIO is Apple's event-driven networking framework. It's inspired by Netty:

swift
1import NIO
2 
3// Simple echo server
4let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
5let bootstrap = ServerBootstrap(group: group)
6 .childChannelInitializer { channel in
7 channel.pipeline.addHandler(EchoHandler())
8 }
9 .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
10 
11let channel = try bootstrap.bind(host: "0.0.0.0", port: 8080).wait()
12print("Server running on \(channel.localAddress!)")
13try channel.closeFuture.wait()
14 
15class EchoHandler: ChannelInboundHandler {
16 typealias InboundIn = ByteBuffer
17 typealias OutboundOut = ByteBuffer
18 
19 func channelRead(context: ChannelHandlerContext, data: NIOAny) {
20 // Send the incoming data straight back
21 context.write(data, promise: nil)
22 }
23 
24 func channelReadComplete(context: ChannelHandlerContext) {
25 context.flush()
26 }
27}

Vapor vs Hummingbird

Feature
Vapor
Hummingbird
Maturity
Since 2016
Since 2021
Community
Large
Growing
ORM
Fluent (built-in)
External
WebSocket
✅ Built-in
✅ Plugin
Templating
Leaf
Mustache
Performance
High
Very high
Size
Large framework
Minimal, modular
Learning curve
Medium
Low
swift
1// Hummingbird minimal server
2import Hummingbird
3 
4let app = HBApplication(configuration: .init(address: .hostname("0.0.0.0", port: 8080)))
5app.router.get("/hello") { _ in
6 "Hello from Hummingbird!"
7}
8try await app.start()

Database Integration

swift
1// PostgreSQL (Fluent + Vapor)
2app.databases.use(.postgres(
3 hostname: Environment.get("DB_HOST") ?? "localhost",
4 port: 5432,
5 username: "vapor",
6 password: "secret",
7 database: "myapp"
8), as: .psql)
9 
10// MongoDB (MongoKitten)
11import MongoKitten
12let db = try await MongoDatabase.connect(to: "mongodb://localhost/myapp")
13let users = db["users"]
14let results = try await users.find(["age": ["$gte": 18]]).decode(User.self).allResults()

Structured Concurrency on Server

swift
1// Server-side async/await
2func handleRequest(req: Request) async throws -> Response {
3 async let user = fetchUser(id: req.userId)
4 async let orders = fetchOrders(userId: req.userId)
5 async let recommendations = fetchRecommendations(userId: req.userId)
6 
7 let dashboard = try await DashboardResponse(
8 user: user,
9 orders: orders,
10 recommendations: recommendations
11 )
12 return Response(status: .ok, body: .init(data: try JSONEncoder().encode(dashboard)))
13}

Docker and Deployment

dockerfile
1# Multi-stage build - small final image
2FROM swift:5.9-jammy as build
3WORKDIR /app
4COPY Package.* ./
5RUN swift package resolve
6COPY . .
7RUN swift build -c release --static-swift-stdlib
8 
9FROM ubuntu:22.04
10RUN apt-get update && apt-get install -y libcurl4 && rm -rf /var/lib/apt/lists/*
11COPY --from=build /app/.build/release/App /usr/local/bin/
12EXPOSE 8080
13CMD ["App", "serve", "--hostname", "0.0.0.0", "--port", "8080"]
yaml
1# docker-compose.yml
2version: '3.8'
3services:
4 app:
5 build: .
6 ports: ["8080:8080"]
7 environment:
8 - DB_HOST=db
9 - DATABASE_URL=postgres://vapor:secret@db:5432/myapp
10 depends_on: [db]
11 db:
12 image: postgres:16-alpine
13 environment:
14 POSTGRES_USER: vapor
15 POSTGRES_PASSWORD: secret
16 POSTGRES_DB: myapp
17 volumes: ["pgdata:/var/lib/postgresql/data"]
18volumes:
19 pgdata:

Serverless Swift with AWS Lambda

swift
1import AWSLambdaRuntime
2 
3@main
4struct MyHandler: SimpleLambdaHandler {
5 func handle(_ event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response {
6 let name = event.queryStringParameters?["name"] ?? "World"
7 return APIGatewayV2Response(
8 statusCode: .ok,
9 body: "Hello, \(name)!"
10 )
11 }
12}

Sharing Models Between Client and Server

swift
1// SharedModels SPM package
2// The iOS app and the Vapor backend use the same models
3public struct CreateOrderRequest: Codable, Sendable {
4 public let items: [OrderItem]
5 public let shippingAddress: Address
6 
7 public struct OrderItem: Codable, Sendable {
8 public let productId: UUID
9 public let quantity: Int
10 }
11 
12 public struct Address: Codable, Sendable {
13 public let street: String
14 public let city: String
15 public let country: String
16 }
17}

Monitoring and Logging

swift
1import Logging
2 
3let logger = Logger(label: "com.myapp.server")
4logger.info("Request received", metadata: ["path": "\(req.url.path)"])
5logger.error("Database error", metadata: ["error": "\(error)"])
6 
7// Structured logging integrated with Grafana/ELK

Error Handling Strategies

Error handling in server-side Swift is critical in production applications:

swift
1// Custom error type
2enum AppError: AbortError {
3 case userNotFound
4 case invalidInput(String)
5 case databaseError(String)
6 case unauthorized
7 case rateLimited
8 
9 var status: HTTPResponseStatus {
10 switch self {
11 case .userNotFound: return .notFound
12 case .invalidInput: return .badRequest
13 case .databaseError: return .internalServerError
14 case .unauthorized: return .unauthorized
15 case .rateLimited: return .tooManyRequests
16 }
17 }
18 
19 var reason: String {
20 switch self {
21 case .userNotFound: return "User not found"
22 case .invalidInput(let detail): return "Invalid input: \(detail)"
23 case .databaseError(let detail): return "Database error: \(detail)"
24 case .unauthorized: return "You are not authorized"
25 case .rateLimited: return "Too many requests, please wait"
26 }
27 }
28}
29 
30// Global error middleware
31struct ErrorHandlerMiddleware: AsyncMiddleware {
32 func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
33 do {
34 return try await next.respond(to: request)
35 } catch let error as AppError {
36 request.logger.warning("App error: \(error.reason)")
37 let body = ErrorResponse(error: true, reason: error.reason)
38 let response = Response(status: error.status)
39 try response.content.encode(body)
40 return response
41 } catch {
42 request.logger.error("Unexpected error: \(error)")
43 let body = ErrorResponse(error: true, reason: "An unexpected error occurred")
44 let response = Response(status: .internalServerError)
45 try response.content.encode(body)
46 return response
47 }
48 }
49}
50 
51struct ErrorResponse: Content {
52 let error: Bool
53 let reason: String
54}

SSWG Package Ecosystem

Packages approved by the Swift Server Work Group and considered production-ready:

Package
Use
SSWG Level
SwiftNIO
Event-driven networking
Graduated
AsyncHTTPClient
HTTP client
Graduated
SwiftLog
Structured logging
Graduated
SwiftMetrics
Monitoring metrics
Graduated
swift-crypto
Cryptography
Graduated
PostgresNIO
PostgreSQL driver
Sandbox
RediStack
Redis client
Sandbox
MongoSwift
MongoDB driver
Sandbox
Soto
AWS SDK for Swift
Incubating
swift-distributed-actors
Distributed computing
Incubating
swift
1// Monitoring with SwiftMetrics
2import Metrics
3import Prometheus
4 
5// Prometheus metrics
6let requestCounter = Counter(label: "http_requests_total", dimensions: [("method", ""), ("path", "")])
7let requestDuration = Timer(label: "http_request_duration_seconds")
8 
9// Usage in middleware
10struct MetricsMiddleware: AsyncMiddleware {
11 func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
12 let startTime = DispatchTime.now()
13 requestCounter.increment(dimensions: [
14 ("method", request.method.rawValue),
15 ("path", request.url.path)
16 ])
17 
18 let response = try await next.respond(to: request)
19 
20 let duration = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
21 requestDuration.record(duration)
22 
23 return response
24 }
25}

Production Checklist

Checklist to work through before taking a server-side Swift application to production:

Item
Description
Priority
TLS/HTTPS
Nginx reverse proxy or built-in TLS
Critical
Rate limiting
Per-IP request limit
Critical
CORS settings
Define allowed origins
High
Health check
Liveness/readiness via a /health endpoint
High
Structured logging
JSON format, log level configuration
High
Graceful shutdown
Catch SIGTERM, finish in-flight requests
Medium
Database pool
Configure the connection pool size
Medium
Environment config
Read secrets from .env
Critical
Monitoring
Prometheus + Grafana or Datadog
High
Backup
Database backup strategy
Critical

Performance Benchmarks

Framework
Req/sec (JSON)
Latency p99
Memory
Vapor
85K
2.1ms
45MB
Hummingbird
120K
1.4ms
25MB
Actix (Rust)
180K
0.8ms
15MB
Express (Node)
42K
4.8ms
120MB
Django (Python)
8K
22ms
180MB

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Reader Reward

Congratulations! Since you read this article all the way through, I have a special gift for you:

Sources:

Tags

#server-side-swift#Vapor#Hummingbird#Swift NIO#Linux#Docker#backend
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share