6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-04-20

ここ数年アプリ開発ばかりですっかりWeb開発から離れてしまっていた最中、久々にオフラインで参加したtry!Swift2024にて非常に刺激的なセッションがあり、Web開発に再び興味を持つきっかけとなりました。
そのセッションとは一日目のSwiftで次世代のウェブサイトを構築しようのセッションと、三日目のワークショップGetting Started with Vaporです。
これらを参考に今回SwiftだけでホスティングサーバーとWebページを構築し、そのWebページを公開するまでを試みました。
そこで3回に分けて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つの要素で構成されています。
image.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を見てみてください。

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をセットしてリクエストします。
スクリーンショット 2024-04-20 21.43.42.png

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

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/
[]

参考

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?