Swiftで始めるWeb開発(1) Swiftで始めるバックエンド開発

ここ数年アプリ開発ばかりですっかりWeb開発から離れてしまっていた最中、久々にオフラインで参加した[try!Swift2024](https://tryswift.jp/)にて非常に刺激的なセッションがあり、Web開発に再び興味を持つきっかけとなりました。
そのセッションとは一日目の**Swiftで次世代のウェブサイトを構築しよう**のセッションと、三日目のワークショップ**Getting Started with Vapor**です。
これらを参考に今回SwiftだけでホスティングサーバーとWebページを構築し、そのWebページを公開するまでを試みました。
そこで３回に分けてSwiftだけでWebサービスを作る方法を紹介していきたいと思います。

* Swiftで始めるバックエンド開発(本記事) 
* [静的サイトジェネレータIgniteを用いてWebページを構築する](https://qiita.com/gdate/items/241e5b76b009c584e901)
* [Swiftで書かれたWebページをデプロイする](https://qiita.com/gdate/items/b92fe851a714c5da0911)

これら全てを体験できるプロジェクトを用意しましたので、もし先んじてみたい方は見てみてください。
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は以下３つの要素で構成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/a92a3375-fcf9-ac03-4f56-4a0dc48ed78b.png)
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を見てみてください。
```Swift
app.get { req async in
    "It works!"
}
```
こちらにAPIのエントリポイントが登録されています。getの引数にパスやパラメータを設定することができ、ここで設定されたパスにアクセスするとクロージャの中身が実行されます。
ここではgetの引数に何も設定されていないため、ルートにアクセスした際に It works! が表示されます。

### パラメータ
パラメータを設定することも簡単です。
```Swift
// 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をレスポンスとして返却することも非常に簡単です。
```Swift
// 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](https://www.postman.com/)などのRESTクライアントを用意する必要があります。
### JSONの取得
```Swift
// 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をセットしてリクエストします。
![スクリーンショット 2024-04-20 21.43.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/9ffb239c-8d25-cd2b-b2b6-73e872aec467.png)

以下が返ってきたら成功です。

```
There were 99 bottles
```

## Fluent
続いてデータベースの操作をしてみましょう。
### モデル作成
App -> Modelsの中にUser.swiftを作成します。
```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を作成します。
```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で先程作成したモデルの識別子やフィールドをマッピングし、マイグレーションを実行します。
```Swift
// configure.swift
app.migrations.add(CreateUser())
try await app.autoMigrate()
```

### Controllerの作成
最後にApp -> Controllersの中にUserController.swiftを作成します。
```Swift
import Vapor
import Fluent

struct UserController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {}
}
```
Controllerをroute.swiftに登録したら完成です。
```Swift
// In routes.swift routes(_:)
try app.register(collection: UserController())
```

## CRUD操作
ここまででデータベースの準備が終わったので、Userモデルに対してCRUD操作をしていきます。

### Create
先ほど作ったControllerにcreate操作を追加します。
```Swift
// 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情報を読み込んでみましょう。
```Swift
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
続いて保存したユーザー名を変更してみましょう。
```Swift
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操作です。
```Swift
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/
[]
```

# 参考
* https://static.brokenhands.io/training/SwiftConnection-Vapor.pdf
* https://docs.vapor.codes/ja/