Serverless FrameworkとTypeScriptでClean ArchitectureライクなREST APIを作ってみる

前回のTerraformでAWSサーバーレスなサービスのインフラ構築をコード化する - Inside Closure - にへろぐではサンプルとして空っぽのAPIを用意しました。せっかくなのでこのサンプルを動くように実装してみました。

何を作ったのか

特にネタがなかったのでTodoを登録・一覧・取得・更新・削除できるだけのREST APIを作成しました。

  • POST /todos : Register todo
  • GET /todos : List todos
  • GET /todos/:id : Get todo
  • PUT /todos/:id : Update todo
  • DELETE /todos/:id : Delete todo

アーキテクチャ

目指したところ

Clean Architecture(日本語訳)の考え方を取り入れたアーキテクチャにしようと思いました。*1

Clean Architectureとはざっくり下記のようにして抽象的な実装(ビジネスロジックなど)を具体的な実装(フレームワークやDBなど)から切り離して保守性を高めようとするアーキテクチャです。

  • システムを4つのレイヤ(Entities, Use Cases, Interface Adapters, Frameworks and Drivers)にわける
  • レイヤ間の依存を具体的なレイヤから抽象的なレイヤへの一方通行にする(Frameworks and DriversInterface AdaptersUse CasesEntities)

レイヤ間の依存と処理/呼び出しの順序が一致しない場合は依存性逆転の原則を駆使するなどしてレイヤ間の依存の方向を整える必要があります。

図の方がわかりやすいです。(原文より)

f:id:nihma:20180916144257j:plain

実際のところ

今回はClean Architectureの原則に全て従うのではなく具体レイヤから抽象レイヤへ一方通行にして抽象レイヤの保守性を上げるという目的/考え方だけを取り入れた構成にしました。原則に全て従うのはお遊びだしめんどくさいため小規模な開発では実装が肥大してやりすぎ感がありデメリットがメリットを上回ると判断したためです。

具体的にはルールを下記のように崩してInterface Adapters, Frameworks and DriversからEntities and Use Casesが一方通行になるところだけ原則に従いました。

  • Interface Adaptersの責務を具体レイヤ(Frameworks and Drivers)と抽象レイヤ(Entities and Use Cases)のマッピングとする。そして、全てのレイヤに依存して良いことにする。

f:id:nihma:20180916151219p:plain

Interface AdaptersUse Casesの間の矢印の向きは全て内向きになってます。*2

これだけでもひとまずServerless FrameworkやLambda、DynamoDBなどをやめたくなってもビジネスロジックは(ほぼ)再利用できるような気がします。たぶん。

実装

コードをGitHubにアップしました。

大まかなディレクトリ構成です。

$ tree -ad -I '.git'
.
├── .circleci ... CircleCI用
├── app       .. アプリケーションコード
│   ├── adapters    ... Interface Adaptersレイヤ
│   │   ├── commands
│   │   ├── databases
│   │   └── http
│   │       ├── requests
│   │       └── responses
│   ├── commands    ... Frameworks and Driversレイヤ(コマンド)
│   ├── databases   ... Frameworks and Driversレイヤ(DynamoDB)
│   ├── entities    ... Entitiesレイヤ
│   ├── http        ... Frameworks and Driversレイヤ(Serverless Framework)
│   │   ├── controllers
│   │   ├── middlewares
│   │   ├── models
│   │   ├── utils
│   │   └── views
│   ├── providers   ... DI関連
│   └── usecases    ... Use Casesレイヤ
│       ├── implementations
│       ├── inputs
│       ├── outputs
│       └── stores
├── dockers    ... docker環境設定
│   ├── dynamodb
│   └── serverless
└── test       ... テストコード
    ├── units
    │   ├── http
    │   │   ├── controllers
    │   │   └── middlewares
    │   └── usecases
    └── utils

33 directories

今回、middleware engineにはmiddyを使ってhttp周りの処理をミドルウェアに切り出しapp/http/middlewaresにまとめました。

aws-serverless-expressserverless-httpでもよかったのですがServerless FrameworkExpressフレームワーク2つを使うことになるのが何となく嫌だったのでミドルウェアに特化しているmiddyを使ってみました。まだバージョンがalphaですので業務では使えないかもですがそこそこ使いやすい気がしました。

あと一応、tslintでtslint-config-standardに準拠するようにとmochaでユニットテストを(できるだけ)書いてクリーンな状態を保つように心がけました。*3

使い方

dockerを使ってローカル環境で実行する方法とAWSで動かす方法があります。

docker環境で動かす

まずリポジトリをcloneします。

$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api

そしてdocker環境のビルドとDynamoDB Localの初期化を行います。

$ npm run docker-build
$ npm run docker-up-dev
$ npm run docker-reset-tables
$ npm run docker-down

最後にdocker環境でserverless-offlineを実行します。

$ npm run docker-up
$ npm run docker-offline

するとAPIが使えます。

# POST /todos
$ curl -X POST http://localhost:3000/v1/todos -H "Content-Type: application/json" --data '{ "text": "foo" }'
# GET /todos
$ curl http://localhost:3000/v1/todos
# GET /todos/:id
$ curl http://localhost:3000/v1/todos/:id
# PUT /todos/:id
$ curl -X PUT http://localhost:3000/v1/todos/:id -H "Content-Type: application/json" --data '{ "text": "bar", "checked": true }'
# DELETE /todos/:id
$ curl -X DELETE http://localhost:3000/v1/todos/:id

DynamoDB Localの管理画面へは下記からアクセスできます。

http://localhost:8000/

AWS環境で動かす

TerraformでAWSサーバーレスなサービスのインフラ構築をコード化する - Inside Closure - にへろぐ使い方の通りです。

ただ今回のコードにはタグv0.0.1を打ってあるのでそこだけは変える必要があります。具体的にはCodeCommitのリポジトリにGitHubのリポジトリをpushする箇所が下記に変わります。

# spa infra repository...
$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-terraform.git sample-spa-infra
$ cd sample-spa-infra
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra
$ cd ..
# spa api repository...
$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-api
$ cd ..
# spa web repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-web.git sample-spa-web
$ cd sample-spa-web
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-web
$ cd ..

aws-sls-spa-sample-terraform.gitaws-sls-spa-sample-api.gitのclone元がv0.0.1になります。

まとめ

今回はServerless FrameworkとTypeScriptでTodoを管理するREST APIのサンプルを作ってみました。

  • Clean Architectureの考え方の理解とやりすぎると実装コストが膨らみそうなことの実感を得ることができました
  • 普通にTypeScriptを書くのはほぼ初めてだったので苦戦するところもありましたが文法が少し分かるくらいにはなれた気がするのでよかったです

*1:ちょうど書籍のClean Architecture 達人に学ぶソフトウェアの構造と設計を読んでいたというだけの理由で

*2:他のレイヤ間も依存性逆転の原則を使って矢印の向きを内向きにすることはできると思いますがレイヤ間マッピングだらけになると思います

*3:心がけただけです