ここ数年アプリ開発ばかりですっかりWeb開発から離れてしまっていた最中、久々にオフラインで参加したtry!Swift2024にて非常に刺激的なセッションがあり、Web開発に再び興味を持つきっかけとなりました。
そのセッションとは一日目のSwiftで次世代のウェブサイトを構築しようのセッションと、三日目のワークショップGetting Started with Vaporです。
これらを参考に今回SwiftだけでホスティングサーバーとWebページを構築し、そのWebページを公開するまでを試みました。
そこで3回に分けてSwiftだけでWebサービスを作る方法を紹介していきたいと思います。
- Swiftで始めるバックエンド開発(本記事)
- 静的サイトジェネレータIgniteを用いてWebページを構築する
- Swiftで書かれたWebページをデプロイする
これら全てを体験できるプロジェクトを用意しましたので、もし先んじてみたい方は見てみてください。
https://github.com/gdate/SwiftServerDemo
WebアプリケーションフレームワークVaporについて
それではまず、Swiftで始めるバックエンド開発について見ていきたいと思います。
try!Swift2024の三日目のワークショップGetting Started with VaporにてコアメンバーのTimさんからSwift言語で記述されたWebアプリケーションフレームワークVaporについての手解きを頂いたのでそれを元に紹介したいと思います。
Vaporとは
VaporとはTanner NelsonとLogan Wrightによって作られたSwift言語で記述されたWebアプリケーションフレームワークです。
Swiftでバックエンド、ウェブアプリのAPI、HTTP サーバーを書くことができます。最新のSwift機能とAPIを利用できます。
また、Vaporは以下3つの要素で構成されています。
https://static.brokenhands.io/training/SwiftConnection-Vapor.pdf
Fluent
FluentはVaporのORMライブラリであり、データベースとの対話を簡素化します。Fluentを使用すると、モデルオブジェクトをデータベースの行にマッピングし、CRUD(Create、Read、Update、Delete)操作を行うことができます。
HTTP
HTTPモジュールには、ルーター、ミドルウェア、コントローラーなどが含まれており、これらを使用してHTTPリクエストを受け取り、処理し、適切なレスポンスを返すことができます。
Leaf
LeafはVaporのテンプレートエンジンであり、動的なHTMLやその他のコンテンツを生成するために使用されます。
Vaporを選ぶメリット
それではなぜVaporを選ぶのかについてですが、主に以下のメリットが考えられます。
- Xcodeでのデバッグサポートが受けられる
- 非同期処理(async/await)とノンブロッキングな特性により高いRPSを実現でき、サーバ台数を減らせる可能性が高い
- 少ないメモリリソースで多くのリクエストを処理できる
- Swiftを使っているので型安全
特にアプリメインのエンジニアにとってはXcodeでSwiftを使ってアプリ開発の要領でバックエンドの開発ができるのは非常に大きいメリットではないでしょうか。
Let's try it!
ここからはワークショップのドキュメント手順のままですが、Vaporプロジェクトの使い方および、API開発の入門編をやってみたいと思います。
インストール手順
$ brew install vapor // vaporのインストール
$ vapor new HelloVapor // vapor用プロジェクトの作成
途中でFluentを使うか聞かれるのでYES,SQLiteを選んでください。また、Leafは使わないのでNoを選んでください。
Cloning template...
name: HelloVapor
Would you like to use Fluent (ORM)? (--fluent/--no-fluent)
y/n> y
fluent: Yes
db: SQLite
Would you like to use Leaf (templating)? (--leaf/--no-leaf)
y/n> n
leaf: No
最後にVaporのロゴが表示されたら成功です。
Creating git repository
Adding first commit
**
**~~**
**~~~~~~**
**~~~~~~~~~~**
**~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~~~~~~~~~~~**
**~~~~~~~~~~~~~~~~~~~~~++++~~~**
**~~~~~~~~~~~~~~~~~~~++++~~~**
***~~~~~~~~~~~~~~~++++~~~***
****~~~~~~~~~~++++~~****
*****~~~~~~~~~*****
*************
_ __ ___ ___ ___
\ \ / / /\ | |_) / / \ | |_)
\_\/ /_/--\ |_| \_\_/ |_| \
a web framework for Swift
Project HelloVapor has been created!
Use cd 'HelloVapor' to enter the project directory
Then open your project, for example if using Xcode type open Package.swift or code . if using VSCode
サーバを立ち上げる
続いて作成したプロジェクトに移動してPackage.swiftを開いてください。
$ cd HelloVapor
$ open Package.swift
依存パッケージがダウンロードされるのでしばらく待ちます。
完了したらスキームにHelloVaporが現れるので、Destinationを My Mac にしてビルドします。
コンソールにローカルホストとポートが表示されるので、こちらをブラウザにコピペして貼り付けます。
[ NOTICE ] Server starting on http://xxx.x.x.x:8080
以下がブラウザに表示されたらサーバーの立ち上げに成功です。
It works!
Getリクエスト
「サーバを立ち上げる」にてIt works!がどうやって表示されたのかみていきましょう。
Sources -> App -> routes.swiftを見てみてください。
app.get { req async in
"It works!"
}
こちらにAPIのエントリポイントが登録されています。getの引数にパスやパラメータを設定することができ、ここで設定されたパスにアクセスするとクロージャの中身が実行されます。
ここではgetの引数に何も設定されていないため、ルートにアクセスした際に It works! が表示されます。
パラメータ
パラメータを設定することも簡単です。
// http://localhost:8080/hello/Tim
app.get("hello", ":name") { req -> String in
let name = try req.parameters.require("name")
return "Hello \(name)"
}
これでアクセスすると以下が返却されます。
Hello Tim
パラメータは :(コロン) でラベル付けをしてその入力を処理できるようになります。
Jsonの返却
Jsonをレスポンスとして返却することも非常に簡単です。
// http://localhost:8080/bottles/99
app.get("bottles", ":count") { req -> Bottles in
let count = try req.parameters.require("count", as: Int.self)
return Bottles(count: count)
}
struct Bottles: Content {
let count: Int
}
まず、Bottlesというプロトコルに準拠したstructを定義します。次に戻り値にそのStructを指定してあげて、パラメータから受け取ったcountを使って初期化したものを返してあげればJSONとしてレスポンスを返すことができます。
このパスにリクエストすると以下のようにJSONレスポンスが返ります。
{"count":99}
POSTリクエスト
ここからはブラウザで動作確認が難しいので、PostmanなどのRESTクライアントを用意する必要があります。
JSONの取得
// http://127.0.0.1:8080/bottles/
app.post("bottles") { req -> String in
let bottles = try req.content.decode(Bottles.self)
return "There were \(bottles.count) bottles"
}
先程までgetだった部分をpostに変えています。そして、Bottlesにデコードしてあげるだけでjsonデータをパースし、取得することができます。
Postmanの場合はこのようにしてBodyにJsonをセットしてリクエストします。
以下が返ってきたら成功です。
There were 99 bottles
Fluent
続いてデータベースの操作をしてみましょう。
モデル作成
App -> Modelsの中にUser.swiftを作成します。
import Fluent
import Vapor
final class User: Model, Content {
static let schema = "users"
@ID var id: UUID?
@Field(key: "name") var name: String
@Field(key: "username") var username: String
init() {}
init(id: UUID? = nil, name: String, username: String) {
self.id = id
self.name = name
self.username = username
}
}
プロパティラッパーを使い、識別子やフィールド名を定義します。
ここではUUIDを識別子とし、nameとusernameをフィールドとして定義しています。空のinitがありますが、こちらは必須となっているので消さないようにしましょう。
マイグレーション作成
続いてApp -> Migrationsの中にCreateUser.swiftを作成します。
import Fluent
struct CreateUser: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("users")
.id()
.field("name", .string, .required)
.field("username", .string, .required)
.create()
}
func revert(on database: Database) async throws {
try await database.schema("users").delete()
}
}
先程作成したモデルの情報からテーブルを作成します。prepareはモデルを保存するテーブルを準備するために実行されます。
続いて、configuration.swiftで先程作成したモデルの識別子やフィールドをマッピングし、マイグレーションを実行します。
// configure.swift
app.migrations.add(CreateUser())
try await app.autoMigrate()
Controllerの作成
最後にApp -> Controllersの中にUserController.swiftを作成します。
import Vapor
import Fluent
struct UserController: RouteCollection {
func boot(routes: RoutesBuilder) throws {}
}
Controllerをroute.swiftに登録したら完成です。
// In routes.swift routes(_:)
try app.register(collection: UserController())
CRUD操作
ここまででデータベースの準備が終わったので、Userモデルに対してCRUD操作をしていきます。
Create
先ほど作ったControllerにcreate操作を追加します。
// In UsersController.swift
func boot(routes: RoutesBuilder) throws {
routes.post("api", "users", use: createHandler)
}
func createHandler(req: Request) async throws -> User {
let user = try req.content.decode(User.self)
try await user.save(on: req.db)
return user
}
ここまでできたらビルドをし、Postmanでpostしてみます。
// POST http://localhost:8080/api/users/
{
"name": "Tim",
"username": "timc"
}
成功すれば以下レスポンスが返ります。
{
"name": "Tim",
"id": "518C6A59-E253-419C-9746-43DEAE44F409",
"username": "timc"
}
Read
先程保存したUser情報を読み込んでみましょう。
func boot(routes: RoutesBuilder) throws {
...
routes.get("api", "users", use: getAllHandler)
}
...
func getAllHandler(req: Request) async throws -> [User] {
try await User.query(on: req.db).all()
}
今度はgetなのでブラウザで以下にアクセスするとUser情報が返ってきます。
// http://localhost:8080/api/users/
[{"id":"518C6A59-E253-419C-9746-43DEAE44F409","username":"timc","name":"Tim"}]
Update
続いて保存したユーザー名を変更してみましょう。
func boot(routes: RoutesBuilder) throws {
...
routes.put("api", "users", ":userID", use: updateHandler)
}
...
func updateHandler(req: Request) async throws -> User {
let updatedUser = try req.content.decode(User.self)
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
throw Abort(.notFound)
}
user.name = updatedUser.name
user.username = updatedUser.username
try await user.save(on: req.db)
return user
}
ここまでできたらビルドをし、Postmanでputしてみます。
Tim→Tam
// PUT http://127.0.0.1:8080/api/users/<ID>
{
"name": "Tam",
"username": "tamc"
}
成功すれば以下レスポンスが返ります。
{
"username": "tamc",
"id": "518C6A59-E253-419C-9746-43DEAE44F409",
"name": "Tam"
}
Delete
最後にDelete操作です。
func boot(routes: RoutesBuilder) throws {
...
routes.delete("api", "users", ":userID", use: deleteHandler)
}
...
func deleteHandler(req: Request) async throws -> HTTPStatus {
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
throw Abort(.notFound)
}
try await user.delete(on: req.db)
return .ok
}
ここまでできたらビルドをし、Postmanでdeleteしてみます。
// DELETE http://127.0.0.1:8080/api/users/<ID>
{
"name": "Tam",
"username": "tamc"
}
User情報をgetしてみると削除されていることが確認できるはずです。
http://localhost:8080/api/users/
[]