はじめに
この記事は、クライアントサイドの実装に特化した言語仕様、アーキテクチャを持っているElmであえてサーバサイド実装をしてみたまとめの記事になります。インフラのことは考えずサーバレスで作っていきたいのでAWS LambdaとServerless Frameworkを使ってみました。かなり荒削りなので質問があればコメント欄、もしくはTwitterで気軽にお声がけいただけるとうれしいです。
なぜElmでサーバサイド?
モチベーションは非常にシンプルです。フロントエンドとバックエンドの両方でElmが使える、それに尽きます。メリットとしては以下が考えられると思います。
- モデルの構造(データ型)に差異が出ない
- APIのインターフェースの定義をフロントエンド側からも参照できる
- 全てがElmである(?)
登場人物
Elm
言わずと知れたHaskellライクな文法を持った言語、アーキテクチャです。
AWS Lambda
AWSが提供する、書いたコードをzipにかためてアップロードするだけでそのコードをクラウド上で実行することが可能になるサービスです。今回はElmが吐き出すJavaScriptのコードを実行するためのインフラとして利用します。サーバレスやAWS Lambdaについて気になる方は以下の記事が分かりやすいので見てみると良いかもしれません。
Serverless Framework
例えばAWS Lambdaである程度の規模のアプリケーションを開発する場合、デプロイやアプリケーションの設定などが複雑になってきます。それを用意にしてくれるアプリケーションフレームワークがServerless Frameworkです。
elm-serverless
ElmでAWS Lambdaを動かすためのライブラリです。以下を内包しています。
- AWS LambdaのNode.jsのコードで受け取ったHTTPリクエストをElmに流し込み、レスポンスをElmから受け取るためのNode.jsのパッケージ
- Node.js側から受け取ったリクエストをElm側で処理し、レスポンスを構築するためのElmパッケージ
elm-serverless の仕組み
今回は非常にシンプルな、Todoの閲覧しかできないアプリ(登録/更新はできません...)の一部を雑に実装してみたので、それを作った流れを追いながらelm-serverlessの仕組みをご紹介します。扱うTodoの型はこんな感じ。今回はDBとの連携が試せてないのでアプリに静的なデータを埋め込んでそれを扱います
dummyTodos : List Todo
dummyTodos =
[ { id = "1", title = "This is todo1", finished = False }
, { id = "2", title = "This is todo2", finished = True }
, { id = "3", title = "This is todo3", finished = False }
]
type alias Todo =
{ id : String
, title : String
, finished : Bool
}
以下がGitHubのリポジトリになります。
https://github.com/otofu-square/serverless-elm-todo-api
環境構築
elm-serverless を始めるには以下のものが必要です。
- Node.js
- yarn(or npm)
- AWSアカウント
それではまずServerless Frameworkをインストールして、プロジェクトを作りましょう。Serverless Frameworkでプロジェクトのボイラープレートを作成する際はcreateコマンドを使います。
$ yarn global add serverless
$ serverless create --template aws-nodejs --path serverless-elm-todo-api
$ cd serverless-elm-todo-api
プロジェクトのディレクトリに移動して、必要なライブラリ群をインストールします。ElmのコンパイルやライブラリのバンドリングはWebpack先生頼りなので、必要なライブラリ
$ yarn add --dev \
elm elm-serverless \ # Elm
serverless-webpack webpack uglifyjs-webpack-plugin elm-webpack-loader # Webpack関連
Elmで使うためのライブラリもインストールしておきます。HTTPリクエストやレスポンスではJSONを簡単に扱いたいのでelm-decode-pipeline
、またURLをパースして取り扱いたいのでurl-parser
を導入しています。
(elm-package
は一度に複数インストールできないのがちょっと面倒ですよね...)
$ yarn elm package install elm-lang/core && \
yarn elm package install elm-lang/http && \
yarn elm package install ktonon/elm-serverless && \
yarn elm package install ktonon/url-parser && \
yarn elm package install NoRedInk/elm-decode-pipeline
だいたい必要なものは以上です。それでは実際にアプリを書いていきましょう。
serverless.yml, webpack.config.jsを書く
今回は横道にそれそうなので割愛します
気になる方は自分のリポジトリにあるserverless.yml, webpack.config.jsを見ていただけるともしかしたら参考になるかもしれません。
エントリーポイントとなるjsファイルを書く
まずはじめに、AWS Lambdaがリクエストを受け付けるエントリーポイントとなるNode.jsのコードを実装します。
https://github.com/otofu-square/serverless-elm-todo-api/blob/master/src/api.js
const elmServerless = require('elm-serverless');
const elm = require('./API.elm');
module.exports.handler = elmServerless.httpApi({
handler: elm.Hello.API,
requestPort: 'requestPort',
responsePort: 'responsePort',
});
このコードでは、どのElmファイルをハンドラとして使用するかを設定し、必要があればElm側にJavaScriptの関数を渡すことができます。渡した関数はElm内で実行することが可能です。今回は特に使用しません。
Elmのハンドラを書く
Elmのハンドラを書いていきます。ここが先程のエントリーポイントのjsとElmを繋ぎこむコードとなるため、jsから受け取ったHTTPリクエストを処理してレスポンスを構築する部分を実装していくことになります。
port module Hello.API exposing (main)
import Serverless
import Conn exposing (..)
import Router exposing (router, urlParser)
main : Serverless.Program () () Route () ()
main =
Serverless.httpApi
{ configDecoder = Serverless.noConfig
, initialModel = ()
, parseRoute = urlParser
, update = Serverless.noSideEffects
, interop = Serverless.noInterop
, requestPort = requestPort
, responsePort = responsePort
, endpoint = router
}
port requestPort : Serverless.RequestPort msg
port responsePort : Serverless.ResponsePort msg
main関数はServerless.Program
型になっており、これはElmのcoreのProgram
が元になっています。具体的にはServerless.httpApi
に対して8つのメンバを持つレコードを渡してあげることで処理を書くことができます。全ての説明は割愛しますが、今回主に実装したのはAWS Lambdaが受け取ったURLをパースするparseRoute
と、レスポンスを構築するendpoint
の部分になります。他のパラメータについては公式のAPI Docに簡単な説明があります。
http://package.elm-lang.org/packages/ktonon/elm-serverless/latest/Serverless
parseRouteの実装
ルーティングの型と、それに対するパーサを書いてあげることでURLのハンドリングが行えるようになります。今回実装したルーティングはGET /todos
とGET /todos/:id
の2つのエンドポイントです。
type Route
= TodoIndex -- GET /todos
| TodoShow String -- GET /todos/:id
...
urlParser : String -> Maybe Route
urlParser =
UrlParser.parseString <|
oneOf
[ map TodoIndex (s "todos") -- GET /todos
, map TodoShow (s "todos" </> string) -- GET /todos/:id
]
...
endpointの実装
endpointではURLをパースした結果をパターンマッチで抽出して、それぞれのエンドポイントに対する処理とレスポンスの組み立てを行います。
router : Conn -> ( Conn, Cmd msg )
router conn =
case
( method conn
, route conn
)
of
( GET, TodoIndex ) ->
respond ( 200, jsonBody <| todosEncoder dummyTodos ) conn
( GET, TodoShow id ) ->
case (getTodoById id dummyTodos) of
Just todo ->
respond ( 200, jsonBody <| todoEncoder <| todo ) conn
Nothing ->
respond ( 404, jsonBody <| errorEncoder <| "Not Found id: " ++ id ) conn
_ ->
respond ( 405, jsonBody <| errorEncoder "Method not allowed" ) conn
JSONの組み立てにはエンコーダを用意してそちらを使っています。
デプロイ
ここまでで大体実装が終わり、デプロイ出来るようになります。AWSアカウントのクレデンシャルを環境変数にセットしてあげた状態で
$ NODE_ENV=production serverless deploy
を実行してあげるとAWS Lambdaに実装したアプリがデプロイされます。お手軽。
デプロイが完了するとエンドポイントのURLが表示されていると思うので、そちらにリクエストすると結果が返ってくるはずです。
さいごに
Elmでサーバサイドがまさか書けるとは思っていませんでしたが、小さなアプリケーションであればElmでフロントエンドもバックエンドも両方書くのはそこまで非現実的ではないなと少し希望を持てました。しかしながら実際にアプリ内からDBを扱ったりHTTPリクエストを飛ばしたりといったことはまだ試せていないため、そこの部分がうまく解決できるかで命運が分かれそうです(elmのaws-sdkのラッパーもありそうだったが使い勝手が微妙そうだった)。あとこれは自分の熟練度の問題ですがファイル分割が結構難しいですね...気を抜くと循環参照になっちゃったりしてました。
時間不足感が溢れ中途半端なところで記事にしてしまったのが少し悔しいので、今後も継続してelm-serverless
は追っていきたいと思います