Park API — Server-Side Swift With Hummingbird
Special thanks to Tibor Bödecs for his patience and guidance while writing this tutorial
Server-side Swift has been available since the end of 2015. The idea was behind the development that you can use the same language for RESTful APIs, desktop and mobile applications. With the evolution of the Swift language, the different Swift web frameworks got more robust and complex.
I was happy to read Tib’s excellent article about a new HTTP server library written in Swift, Hummingbird. I immediately liked the modularity concept, so I created a tutorial to show its simplicity.
We will build a swift server running on SQLite database to store playgrounds around the city with names and coordinates. A simple JSON response will look like this:
[
{
"latitude": 50.105848999999999,
"longitude": 14.413999,
"name": "Stromovka"
},
{
"latitude": 50.0959721,
"longitude": 14.4202892,
"name": "Letenské sady"
}, {
"latitude": 50.132259369,
"longitude": 14.46098423,
"name": "Žernosecká - Čumpelíkova"
}
]
The project will use FeatherCMS’s own Database Component.
Step 1. — Init the Project
mkdir parkAPI && cd $_
swift package init --type executable
This creates the backbone of our project. One of the most important files and initial points of our project is the Package.swift, the Swift manifest file. Here you can read more about it.
Step 2. — Create the Folder Structure
We need to follow certain guidelines about folder structure. Otherwise, the compiler won’t be able to handle our project. In the picture below, you can find the simplest structure, which follows the Hummingbird template.
.
├── Package.swift
├── README.md
└── Sources
└── parkAPI
├── App.swift
└── Application+configure.swift
We will add the Tests folder later, when we will have something to test.
Step 3. — Run the Server
Before we can run our server, we need to add two packages to the Package.swift file:
- Hummingbird
- Swift Argument Parser
Using .executableTarget the @main will be the entry point of our application, and we can rename main.swift to App.swift. Paul Hudson wrote a short article about it.
import PackageDescription
let package = Package(
name: "parkAPI",
platforms: [
.macOS(.v12),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-argument-parser",from: "1.2.0"),
],
targets: [
.executableTarget(
name: "parkAPI",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
],
swiftSettings: [
.unsafeFlags(
["-cross-module-optimization"],
.when(configuration: .release)
)
]
)
]
)
Define the hostname and port in the App.swift.
import ArgumentParser
import Hummingbird
@main
struct App: ParsableCommand {
@Option(name: .shortAndLong)
var hostname: String = "127.0.0.1"
@Option(name: .shortAndLong)
var port: Int = 8080
func run() throws {
let app = HBApplication(
configuration: .init(
address: .hostname(hostname, port: port),
serverName: "parkAPI"
)
)
try app.configure()
try app.start()
app.wait()
}
}
One last thing that remained before we could run our application was to define the route in the Application+configuration.swift.
import Hummingbird
import HummingbirdFoundation
public extension HBApplication {
func configure() throws {
router.get("/") { _ in
"The server is running...🚀"
}
}
}
Run the first Hummingbird server by typing:
swift run parkAPI
Step 4. Create API Response
Our server will be accessible on the following routes by using different HTTP methods.
- GET – http://hostname/api/v1/parks: Lists all the parks in the database
- GET – http://hostname/api/v1/parks/:id: Shows a single park with given id
- POST – http://hostname/api/v1/parks: Creates a new park
- PATCH – http://hostname/api/v1/parks/:id: Updates the park with the given id
- DELETE – http://hostname/api/v1/parks/:id: Removes the park with id from database
Step 4.1 Add database dependency
Our server will use the SQLite database to store all data, so we must add the database dependency to our manifest file. This will allow the server to communicate with the database.
The updated Package.swift file will look like this:
import PackageDescription
let package = Package(
name: "parkAPI",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-argument-parser",from: "1.2.0"),
// Database dependency
.package(url: "https://github.com/feathercms/hummingbird-db", branch: "main")
],
targets: [
.executableTarget(
name: "parkAPI",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
// Database dependencies
.product(name: "HummingbirdDatabase", package: "hummingbird-db"),
.product(name: "HummingbirdSQLiteDatabase", package: "hummingbird-db"),
],
swiftSettings: [
.unsafeFlags(
["-cross-module-optimization"],
.when(configuration: .release)
)
]
)
]
)
Step 4.2 Use concurrency in App.swift
Add async to function run() and await to app.configure.
import ArgumentParser
import Hummingbird
@main
struct App: AsyncParsableCommand, AppArguments {
@Option(name: .shortAndLong)
var hostname: String = "127.0.0.1"
@Option(name: .shortAndLong)
var port: Int = 8080
func run() async throws {
let app = HBApplication(
configuration: .init(
address: .hostname(hostname, port: port),
serverName: "Hummingbird"
)
)
try await app.configure()
try app.start()
app.wait()
}
}
We need to use AsyncParsableCommand and AppArguments protocols.
Step 4.3 Create a database configuration file
Add a new file, DatabaseSetup.swift, under a new Database folder under Source/parkAPI/.
import Hummingbird
import HummingbirdSQLiteDatabase
extension HBApplication {
func setupDatabase() async throws {
// Name and location of the SQLite database file
let path = "./hb-parks.sqlite"
services.setUpSQLiteDatabase(
path: path,
threadPool: threadPool,
eventLoopGroup: eventLoopGroup,
logger: logger
)
// Create the database table
try await db.execute(
.init(unsafeSQL:
"""
CREATE TABLE IF NOT EXISTS parks (
"id" uuid PRIMARY KEY,
"latitude" double NOT NULL,
"longitude" double NOT NULL,
"name" text NOT NULL
);
"""
)
)
}
}
Step 4.4 Call the setupDatabase function
Add try await setupDatabase() to Application+configure.swift.
import Hummingbird
import HummingbirdFoundation
public protocol AppArguments {}
public extension HBApplication {
func configure() async throws {
// Setup the database
try await setupDatabase()
// Set encoder and decoder
encoder = JSONEncoder()
decoder = JSONDecoder()
// Logger
logger.logLevel = .debug
// Middleware
middleware.add(HBLogRequestsMiddleware(.debug))
middleware.add(HBCORSMiddleware(
allowOrigin: .originBased,
allowHeaders: ["Content-Type"],
allowMethods: [.GET, .OPTIONS, .POST, .DELETE, .PATCH]
))
router.get("/") { _ in
"The server is running...🚀"
}
// Additional routes are defined in the controller
// We want our server to respond on "api/v1/parks"
ParkController().addRoutes(to: router.group("api/v1/parks"))
}
}
Step 4.5 Create the park model
Add the Park.swift under /Source/parkAPI/Models.
import Foundation
import Hummingbird
struct Park: Codable {
let id: UUID
let latitude: Double
let longitude: Double
let name: String
init(id: UUID, latitude: Double, longitude: Double, name: String) {
self.id = id
self.latitude = latitude
self.longitude = longitude
self.name = name
}
}
extension Park: HBResponseCodable {}
Step 4.6 Create the park controller
The Controller receives input from the users, then processes the user’s data with the help of Model and passes the results back. Add ParkController.swift to a new Controllers folder under Source/parkAPI/.
import Foundation
import Hummingbird
import HummingbirdDatabase
extension UUID: LosslessStringConvertible {
public init?(_ description: String) {
self.init(uuidString: description)
}
}
struct ParkController {
// Define the table in the databse
let tableName = "parks"
// The routes for CRUD operations
func addRoutes(to group: HBRouterGroup) {
group
.get(use: list)
}
// Return all parks
func list(req: HBRequest) async throws -> [Park] {
let sql = """
SELECT * FROM parks
"""
let query = HBDatabaseQuery(unsafeSQL: sql)
return try await req.db.execute(query, rowType: Park.self)
}
}
In the controller file, define the table of the database you want to use. Ideally, it is the same as you defined in the DatabaseSetup.swift file.
Use HBRouterGroup to collect all routes under a single path.
GET — all parks
Start with listing all elements: .get(use: list) Where get refers to GET method and use to the function where you describe what is supposed to happen if you call that endpoint.
The list() function returns with the array of Park model.
GET — park with {id}
Show park with specified id: .get(“:id”, use: show).
func show(req: HBRequest) async throws -> Park? {
let id = try req.parameters.require("id", as: UUID.self)
let sql = """
SELECT * FROM parks WHERE id = :id:
"""
let query = HBDatabaseQuery(
unsafeSQL: sql,
bindings: ["id": id]
)
let rows = try await req.db.execute(query, rowType: Park.self)
return rows.first
}
POST — create park
Create a new park: .post(options: .editResponse, use: create).
func create(req: HBRequest) async throws -> Park {
struct CreatePark: Decodable {
let latitude: Double
let longitude: Double
let name: String
}
let park = try req.decode(as: CreatePark.self)
let id = UUID()
let row = Park(
id: id,
latitude: park.latitude,
longitude: park.longitude,
name: park.name
)
let sql = """
INSERT INTO
parks (id, latitude, longitude, name)
VALUES
(:id:, :latitude:, :longitude:, :name:)
"""
try await req.db.execute(.init(unsafeSQL: sql, bindings: row))
req.response.status = .created
PATCH — update park with {id}
Update park with specified id: .patch(“:id”, use: update)
func update(req: HBRequest) async throws -> HTTPResponseStatus {
struct UpdatePark: Decodable {
var latitude: Double?
var longitude: Double?
var name: String?
}
let id = try req.parameters.require("id", as: UUID.self)
let park = try req.decode(as: UpdatePark.self)
let sql = """
UPDATE
parks
SET
"latitude" = CASE WHEN :1: IS NOT NULL THEN :1: ELSE "latitude" END,
"longitude" = CASE WHEN :2: IS NOT NULL THEN :2: ELSE "longitude" END,
"name" = CASE WHEN :3: IS NOT NULL THEN :3: ELSE "name" END
WHERE
id = :0:
"""
try await req.db.execute(
.init(
unsafeSQL:
sql,
bindings:
id, park.latitude, park.longitude, park.name
)
)
return .ok
}
As in the DatabaseSetup.swift file, we defined that none of the table columns can be NULL, we need to check that the request contains all values of only some of them and update the columns respectively.
DELETE — delete park with {id}
Delete park with specified id: .delete(“:id”, use: deletePark)
func deletePark(req: HBRequest) async throws -> HTTPResponseStatus {
let id = try req.parameters.require("id", as: UUID.self)
let sql = """
DELETE FROM parks WHERE id = :0:
"""
try await req.db.execute(
.init(
unsafeSQL: sql,
bindings: id
)
)
return .ok
}
}
Our final folder structure looks like this:
.
├── Package.swift
├── README.md
└── Sources
└── parkAPI
├── App.swift
├── Application+configure.swift
├── Controllers
│ └── ParkController.swift
├── Database
│ └── DatabaseSetup.swift
└── Models
└── Park.swift
Step 5: Run the API Server
swift run parkAPI
You can reach the server on http://127.0.0.1:8080.
Summary
I was impressed by how easily and quickly I could build a working API server using Hummingbird and FeatherCMS’s Database Component. Building and running the project took very minimal time compared to Vapor. I highly recommend trying the Hummingbird project if you want something light and modular on the server-side Swift.
You can find the source code here.
The original article was published here.
You can find out how the PostgreSQL database is used here.
Park API — Server side Swift with Hummingbird was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.