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
Fluent
とVapor
をimportするstruct
ではなく、class
にするModel
とContent
に準拠する- 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
最終的なレスポンスの型
- 今回だとこんな形になる。「
- ここは、最初のサンプルrouterのようにURLの記述を行う
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ライフを!!