Hello, Swift ラバーなみなさん。これからラバーのみなさん。
突然ですが、サーバサイドでもSwiftが使えることご存知でしょうか?
実はいくつかのWeb Frameworkがあり、今回はVaporを使って、APIサーバをローカルで立ててみました。
Swiftを愛しすぎて、サーバサイドもSwiftで書きたいと思っているみなさんにピッタリなものですね!(?)
さっそく見ていきましょう!!

Vaporとは

まずはVaporについて、紹介します。
VaporはSwift+Xcodeで構築をするWeb Frameworkです。
今回取り上げるAPIサーバももちろん、Webアプリの作成も可能です。
最近では、Swift Concurrencyにも対応され、細かなメンテナンスがされています。

導入

早速導入をしていきましょう。
今回使うバージョン(2022年9月時点最新バージョン)

  • MacOSX: 12.5.1
  • Xcode: 13.4.1
  • Homebrew: 3.5.10

Vaporインストール

まずは、早速Vaporを導入します。
今回はなにも考えずに最新のstableバージョンをHomebrew経由でインストールします。
Homebrewが入っていない方は先に導入してください)

$ brew install vapor
$ vapor --version
toolbox: 18.5.1

(ここでPackage.resolveがないと言われますが、ここでは気にせず大丈夫です)

プロジェクトの作成

プロジェクトを作成したいディレクトリに移動してください。

$ cd <プロジェクトのディレクトリ>

今回はHomeディレクトリで「Develop/ServerSideSwift」というディレクトリを作成したので、下記のようになりました。

$ cd ~/Develop/ServerSideSwift

実際にプロジェクトを作成します。

$ vapor new <プロジェクト名>

今回は「SampleAPI」というプロジェクト名にしましたので、下記のようにしました。

$ vapor new SampleAPI

以下、SampleAPIの部分は、適宜ご自身で作られたプロジェクト名に置き換えてください。

ここで3つほど質問されます。

  • > Would you like to use Fluent? -> y
    • ORMフレームワークの導入について聞かれていますので、「y」を入力します。
  • > Which database would you like to use -> 1. PostgreSQL
    • 使用するDBを選択します。今回は1のPostgreSQLを選択しました。(後ほど導入します)
  • > Would you like to use Leaf? -> n
    • テンプレート言語 HTMLの作成フレームワークの導入について聞かれています。今回はフロント画面を作成しないので、「n」を入力します。

こんな感じの画面が表示されたらプロジェクトの作成完了です。

プロジェクトディレクトリに移動

$ cd SampleAPI

一度ビルドします。

$ vapor build

結構時間がかかりますので、お茶を入れに行きます。

このときにXcodeにCommandLineToolsが設定されていないと、下記のエラーが発生する場合があります。 エラーが出たら確認してみましょう。

error: unable to find utility "xctest", noa developer a developer tool or in PATH

実際に動かしてみる

前項のプロジェクトディレクトリのままXcodeをVaporで起動します。

$ vapor xcode

立ち上がったら、実際に動かしてみましょう。

デフォルトのポートは8080ですが、変更することもできます。

configure.swift内で以下を記述してください。(5000の部分は任意です。予約済みポートもあるので、いろいろ試してください。)

以下のアクセスURLは、ご自身で指定したポートに読み替えてください。

app.http.server.configuration.port = 5000

今回はデフォルトのポート(8080)で起動します。(Xcode上でcmd + R)


コンソール部分に接続先のURLが表示されますので、そちらにブラウザでアクセスしてみましょう。
「It works!」という画面が出ましたか??


次にURLに/helloを追加してみましょう。
(例:http://localhost:8080/hello
表示が"Hello, world!"に変わりましたでしょうか??


これはroutes.swiftに記述されているルーティング通りになります。

いまだとWeb画面なので、試しに一つREST APIを作成してみます。
適当に以下の構造体をroutes.swiftのファイル内に記述してみます。

struct Foo: Content {
    var name: String
}

routes.swiftのfunc routes(_ app: Application) throws {}のメソッド内に以下を記述してみます。

app.get("foo") { req async -> Foo in {
    return .init(name: "Foo Bar")
}

もう一度Runしてみましょう。
そこでURLに/foo(http://localhost:8080/foo)を追加してブラウザでアクセスしてみましょう。
APIレスポンスが表示されます。

curlで試してみたい場合は、以下をターミナルに貼り付けてアクセスしてみてください。

curl "http://localhost:8080/foo"

このようにContent Protocolに準拠した構造体を返すだけで、簡単にREST APIを作ることができました。

MEMO

Content ProtocolとはVapor内で定義されているもので、中を見てみると、Codableに準拠しているので、勝手にEncode, Decodeを行ってくれます。
[public protocol Content: Codable, RequestDecodable, ResponseEncodable, AsyncRequestDecodable, AsyncResponseEncodable {}]

簡単にとはいっても、そのままの構造体を返すケースなんてそんなにないので、DBも使用したサンプルを作ってみましょう。

実際にAPIを作成していく

DB環境設定

前述の通り、今回はPostgreSQLを使用します。
今回はなにも考えずに最新バージョンを入れていきます。

$ brew install postgresql
$ psql --version
psql (PostgreSQL) 14.5

このときにMacのPythonが2系だと、下記のエラーが発生する場合があります。

エラーが出てみたら、Pythonのバージョンを確認してみてください。

Error: The 'brew link step did not complete successfully

起動してみます。

$ brew services start postgresql
Successfully started `postgresql` (label: homebrew.mxcl.postgresql)

PostgreSQLの中に入ってみましょう。

$ psql postgres

ここからDBの作成などなど行っていきます。

DB作成

まずはサンプルプロジェクトに使用するDBの作成をします。

postgres=# create database <DB名>;

(*終端にセミコロンをつけないと、発火しないので忘れないように)

今回は「sample_db」としましたので下記のような感じになります。

postgres=# create database sample_db;


作成できたか見てみましょう。

postgres=# \l

(バックスラッシュ+Lです)
先程作成したDBが表示されていれば成功です。


次にユーザーを作成します。

postgres=# create user <ユーザー名>;

「CREATE ROLE」が表示されれば成功です。

こちらは「sample_user」にしました。

postgres=# create user sample_user;


パスワードを設定します。

postgres=# \password sample_user;

2回入力があるので、任意のものを入力します。


ユーザーが作成できているか確認しましょう。

postgres=# \du

作成したユーザー(PostgreSQLではロールって言うみたいです)を確認します。
control + DでPostgreSQLから抜けちゃいましょう。
さてVaporに戻ります。

VaporのDB設定

Xcodeプロジェクトでconfigure.swiftを開いてください。
app.database.use(.postgres(...), as: .psql)というDB設定の記述があります。
業務ではEnvironmentに記述をして管理をすると思いますが、今回はサンプルなのでNil coalescing operator(??のやつ)の部分を変更していきます。

  • hostname→今回はローカルなのでlocalhostのまま
  • port→これもこのまま
  • username→先程作成したユーザー名に変更
  • password→先程作成したユーザーに対するパスワードに変更
  • database→先程作成したDB名に変更

私の場合、こんな感じになっています。

app.databases.use(.postgres(
    hostname: Environment.get("DATABASE_HOST") ?? "localhost",
    port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber,
    username: Environment.get("DATABASE_USERNAME") ?? "sample_user",
    password: Environment.get("DATABASE_PASSWORD") ?? "password",
    database: Environment.get("DATABASE_NAME") ?? "sample_db"
), as: .psql)

下準備はできました。
これからようやくコードを書いていきます。
(DB設定も一応コード??)

Migrationsの作成

Migrationsディレクトリ内にDBに対するテーブル作成の構造体を作成します。

先にコードです。

import Fluent

struct CreateUserTable: AsyncMigration {
    // MARK: Table Name
    static let user: String = "user"
    
    // MARK: Column Name
    static let name: FieldKey = "name"
    static let birthday: FieldKey = "birthday"
    
    // MARK: Prepare
    func prepare(on database: Database) async throws {
        try await database.schema(CreateUserTable.user)
            .id()
            .field(CreateUserTable.name, .string, .required)
            .field(CreateUserTable.birthday, .string)
            .create()
    }
    
    // MARK: Revert
    func revert(on database: Database) async throws {
        try await database.schema(CreateUserTable.user).delete()
    }
}

POINT

  • Fluentをimportする
  • 構造体に対して、AsyncMigration Protocolを適合する
  • prepare()revert()を実装する

見ていきましょう。

MEMO

 今回くらいの規模だと不必要ですが、文字列ベタ書きはタイポの危険性があるので、`static let`でテーブルとカラム名を宣言しています。
 カラム名の型は`FieldKey`を設定してください。

こちらはデータベースに対して、テーブルを作成する実装になります。
schemaにテーブル名を指定し、カラムの記述、最後にcreate()を記載します。


Auto IncrementなID設定や、Not Null設定なども設定できるので、ここはいろいろ試してみてください。


今回はnameとbirthdayの2つを設定してみました。

次にfunc revert(on database: Database) async throws {}ですが、こちらはDBの変更を戻す場合(transaction的な??)に使用するようです。
今回は使用しないですが、一旦上記のように書いています。 migrationが書けたら、configure.swiftに戻り、migration対象にテーブルを追加します。
先程のDB設定の下に記述します。

app.database.use(.postgres(...), as: .psql)`

// MARK: Migration
app.migrations.add(CreateUserTable())

Migrationを行いましょう。

$ vapor run migrate
...
The following migration(s) will be prepared:
+ App.CreateUserTable on default
Would you like to continue?
y/n> y

作成できたか見てみましょう。
一気に行きます。

$ psql postgres
postgres=# \c <先程作ったDB名>      // DBに接続する
You are now connected to database "sample_db" as user "<Admin User名>".
<接続したDB名>=# \dt               // テーブルを見るコマンド
                 List of relations
 Schema |        Name        | Type  |    Owner
--------+--------------------+-------+-------------
 public | user               | table | sample_user

という流れで、最後に「user」テーブルの作成が確認できていればOKです。
失敗していた場合、DBの削除→マイグレーションのやり直しが手っ取り早いです。

drop database sample_db;

テーブルの作成も確認できたので、Modelの作成Routerの作成に入ります。

Modelの作成

Vaporプロジェクトに戻り、Modelsディレクトリ内にUserModel.swiftというファイルを作ります。
今回は以下のように記述しました。

import Fluent
import Vapor

// classである必要がある
final class UserModel: Model, Content {
    static let schema: String = CreateUserTable.user

    @ID(key: .id)
    var id: UUID?

    @Field(key: CreateUserTable.name)
    var name: String

    @Field(key: CreateUserTable.birthday)
    var birthday: String

    init() {}

    init(id: UUID? = nil, name: String, birthday: String) {
        self.id = id
        self.name = name
        self.birthday = birthday
    }
}

POINT

  • FluentVaporをimportする
  • structではなく、classにする
  • ModelContentに準拠する
  • schemaに対し、該当のテーブル名を記述する
  • テーブルのカラムに合わせて実装する

見ていきましょう。
schemaの宣言に先程のmigrationファイルで宣言したstatic変数を代入しています。
タイポがなくなりますね!(ベタ書きでも大丈夫)

次に各パラメータへの実装を行います。

基本は見ればわかる形ですが、ポイントとして

  • 今回のIDはユニークIDになるので、UUIDを代入する形にする
  • nilなのは、外部からの入力はしなくていいように。値はDBで勝手に入る
  • nameはString型で単純に外から値を受け付けるようにする

できたらrouterを作っていきます。

Router

Register User

外部からのPostを受け取り、DBに保存するRouterを作成します。
本来はもっとバリデーションとか、ロジックを外部化するといった配慮が必要ですが、サンプルなので許してください。

// MARK: Register User
app.post("user") { req async throws -> UserModel in
    let data = try req.content.decode(UserModel.self)
    let user = UserModel(id: data.id,
                         name: data.name,
                         birthday: data.birthday)
    try await user.save(on: req.db)
    return user
}

POINT

  • app.post("user") { req async throws -> UserModel in
    • ここは、最初のサンプルrouterのようにURLの記述を行う
      • 今回だとこんな形になる。「http://localhost:8080/user
      • reqはリクエスト、-> UserModel最終的なレスポンスの型
  • let param = try req.content.decode(UserModel.self)
    • こちらは受け取った値をUserModel型にデコードしている
    • デコードに失敗すると、throwされる
    • オリジナルなバリデーションを作ることもできる
  • let user = UserModel(id: param.id, name: param.name, birthday: param.birthday)
    • こちらは新しくUserModel型の変数を作成している
    • 実際にはpramとuserの間でゴニョゴニョ処理をするであろう
    • また、paramはそのままリターンすることができない
  • try await user.save(on: req.db)
    • ここでDBへの保存処理を行う
  • return user
    • ここでAPIリクエストに対してのレスポンスを返す

Request

実際に叩いてみましょう。
今回はGoogle Chrome拡張の「Talend API Tester」で叩いてみます。
メソッドは「POST」、URLには「http://localhost:8080/user」、bodyは以下の形式でリクエストを行います。

{
  "name": "piyo",
  "birthday": "20220101"
}


成功するとこんな感じで返却されます。

Fetch All Users

次に登録したユーザーを全件取得するAPIを作ってみます。
こちらはもっと簡単です。

// MARK: Fetch All Users
app.get("user") { req -> [UserModel] in
    return try await UserModel.query(on: req.db).all()
}

.all()がすべてを物語っています。
Modelに定義したschemaをもとにDBにアクセスして全件取得してくれます。

叩いてみましょう。
メソッドは「GET」、URLには「http://localhost:8080/user」でリクエストを行います。

このように配列で登録したユーザーが返却されれば成功です。

まとめ

はじめて触る方でもAPI作ることができましたか??
Swiftが大好きな人がAPIを作る必要性がある際に導入を検討してみてもいいかもしれません!
私としても普段はiOSアプリの開発を行っていますが、もうちょっと深堀りしてみたい気持ちになりました。
もしかしたら、続編が出るかもしれません笑
興味があれば、ぜひ一度使ってみてください!

それでは、素敵なSwiftライフを!!



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる