最終更新: a few seconds agoRemove Netlify-related code from main (grafted, HEAD)

API の実装

言語・フレームワーク

言語は TypeScript、ランタイムのターゲットは Node.js 10 以降 とする。いわゆる ES2015 ~ ES2017 (Arrow function, let/const, classes, object spread...) 機能のほとんどおよび async/await は標準で含まれているので、そのまま利用する。 標準のNode.jsでまだ含まれていないECMAScript の機能はBabel 7.x を使って実現する。

  • ES6 modules (import, export, default)
  • Dynamic import (import(lib).then(mod => ...))

HTTP フレームワークとして Koa (Express.js の開発者によって作られた後継プロジェクトで、async/await を多用するモデルのもの)を利用する。 すべての web 周りの機能は Koa の middleware として分割して実装する。 可能な限り middleware レベルで作業を分割する。

モジュールはいわゆる ES6 modules を利用する。webpack の設定ファイルなどの一部を除いて commonjs のモジュールは直接利用はしない。

Koa のミドルウェア階層とエンドポイント登録

基本的なミドルウェア階層

  • errorHandler
  • cors
  • body-parser / multer
  • OAuth2 authenticate
  • router
    • [globalProvilegeChecker]
    • [projectPrivilegeChecker]
    • [seriesPrivilegeChecker] (未実装)
    • メインルート (handleGet, etc.)
errorHandler
ミドルウェアの最上位に位置し、これより下流のあらゆる場所で発生するエラーを捕捉する。これにはクライアント側のエラー (4xx) と JavaScript のランタイムエラー (5xx) の両方を含む。すべてのエラーが必ず正しい JSON 形式にフォーマットされてから返るよう保証する。
cors
すべてのリクエストに CORS ヘッダーを付加する。
body-parser / multer
body-parser は JSON 形式の HTML のリクエストボディをパースし、ctx.request.body にその結果を割り当てる。multermulti-part/form-data 形式のリクエストボディをパースする。無効な JSON 文字列が渡された場合や制限サイズを超えるリクエストが発生した場合、この段階レベルで正しくエラーを返す。
authenticator
OAuth2 トークンによるユーザ認証を行う。認証が成功した場合 ctx.user に現在のログインユーザ情報を割り当てる。
router
いわゆるルータ。URL 文字列をそれぞれのハンドラへと対応づける。

API の各エンドポイントは YAML によるマニフェストファイルで指定する。アプリケーションは起動時に src/api ディレクトリ配下にある *.yaml ファイルを走査し、そこに記載しているルールの通りにルータに route を登録していく。認証関係などのルールを YAML で一定のフォーマットで記載することで、バグの混入をできるだけ防ぐ。

依存性注入

ユニットテストを実現しやすくするため、プログラム外部と何かを入出力するような部分や実装を差し替えられそうな部分については ServiceLoder を使って dependency injection を行うこと。

// logger.js
export const logger = new Logger();

// someRoute.js
import { logger } from './logger';

export async function handleGet(ctx) {
  // logger の実装を差し替えられないのでテスト時に困る
  logger.log('something happened.');
}
// createLogger.js
export const createLogger(options) {
	return new Logger(options);
}

// createApp.js
import { createLogger } from './createLogger';
const logger = createLogger(options);
const app = createApp({ logger });

// someRoute.js
export function handleGet({ logger }) {
	export async function(ctx, next) {
		logger.log('something happened.');
	}
}

API の規約

「いわゆる普通で自然な JSON API」を目指す。

認証

すべての API は OAuth2 の Token を付けて呼び出すことで認証する。

トークンは ID とパスワードの組による login API を通じて発行される。これによって発行されたトークンの有効時間は 60 分とする。(つまり無操作時間がこの時間続くと強制的にログアウト)

他のサーバアプリケーションを認可するための恒久的なトークンも利用可能。

Cookie は使用禁止とし、すべて API トークンベースの認証とする。

リクエストの形式

リクエストボディ

ほぼすべての API へのリクエスト発行は Content-Type: application/json の JSON 形式で行う(後述する例外あり)。それ以外の Content-Type (XML とか application/x-www-form-urlencoded とか)でリクエストが来た場合、問答無用で 415 Unsupported Media Type を返す。

文字コードは UTF-8 とし、それ以外は受け付けない。他のエンコーディングが明示的に指定されたリクエストが来た場合、問答無用で 400 Bad Request を返す。

リクエストのサイズは 1MiB が上限(body-parser 作成時にハードコードされている)。

以下の API へのリクエスト発行に関しては Content-type が application/json 以外となる。

  • 認証トークン発行(OAuth2 準拠のため urlencoded 形式を用いる)
  • シリーズアップロード(DICOM 画像を受け付ける)

ただし例外的に application/octet-stream を受け付ける API もある。

レスポンスボディ

レスポンスボディが存在しない場合 (201, 204) を除き、ほぼすべての API のレスポンスは基本的に Content-Type: application/json; charset=UTF-8 である。ただし例外として application/octet-streamapplication/gzip を返す API もある。

JSON 内での改行やインデントはしない。

日付

JSON で日付を表す場合は常に ISO 8601 形式 YYYY-MM-DDTHH:mm:ss.sssZ の文字列とする。 UNIX タイムスタンプの整数を使わない。日付のフォーマットやタイムゾーンのことはクライアント側で考えさせる。

バリデータはどのフィールドが日付形式なのかを理解しており、この形式の日付文字列を自動的に JavaScript の Date 型に変換する。詳細はバリデーションを参照。

例外としてシリーズ検索などの部分では MongoDB Extended JSON 形式で日付を表すことがある。

数字型は JSON 中でも数字型、論理型は JSON 中でも論理型で表現する。これらのために文字列型を使ってはいけない。

// 禁止!
{ isSomething: "true", someNumberData: "320.5" }

キー文字列

常にキャメルケース (someData)を用いる。略語についても 2 文字目以降を小文字とする(someHtmlEntity, circusCsPlugin, ...)。

Id/Uid も 2 文字目以降を小文字にする(語源的にも ID は identifier の abbreviation であって acronym ではないので…)。

古いデータベースの部分で ID や UID という大文字が残っている部分がある。いずれリファクタリングして解消する。

HTTP の verb

API の verb を正しく区別すること。

GET
単一リソースの取得、リソースの検索
POST
ID を付けない新規アイテムの作成(POST が成功した時点でそのリソースを参照する ID が作成される)
PUT
ID 付きでのアイテムの作成、ID による既存アイテムの置き換え(リソース作成時点で既に ID が分かっている)
DELETE
ID によるアイテムの削除
PATCH
ID による既存リソースの一部の変更

エラーについて

HTTP のステータスコードを正しく使用する。

200 OK
GET リクエストが成功した場合に使用する
201 Created
POST リクエストが成功した場合に使用する
204 No Content
PUT/DELETE リクエストが成功した場合に使用する

バリデーションエラーやリソースが見つからない場合などには可能な限り 4xx のステータスコードを返す。5xx は本当の致命的なサーバ側エラー時のみに返る(基本的にユーザ側からのリクエストの中身によって起きてはならない)。

エラーメッセージのフォーマット

すべてのエラーは errorHandler で一元的にフォーマットされるはずなので、そちらを参照。

必要もなく個々のルートのレベルでエラーメッセージを細々とフォーマットせずに、上流のエラーハンドラに任せること!

欠損データについて

データベースに保存する際、すべてのプロパティは省略不可能とする。値がない場合でも空文字列や空の配列、null などを適宜入れる。これはバリデータにより保証する。

データベースから読み取る際も、すべてのプロパティの整合性をチェックしてから返す。

ツールを使ったリクエストの作成/検証

VS Code の "REST Client" 拡張

テキストベースのクライアントであり非常に使いやすいのでおすすめ。

cURL

HTTP 通信のためのコマンドラインツール curl からも CIRCUS API へリクエストを発行することが可能。下準備として永続トークンを準備する。トークンはパスワードと同様に機密情報として扱うこと。

% node circus add-permanent-token <userEmail>
CIRCUS-API CLI version 0.4.0

Your permanent access token is: d6fc7f5e72dc283e83bd74737647108d06cbc38a

その後に以下のようなコマンドを実行する。

% curl -H "Authorization: Bearer d6fc7f5e72dc283e83bd74737647108d06cbc38a" http://localhost:8080/api/series

毎回コマンドラインから認証を書くのは面倒なので、cURL の設定ファイルの仕組みを利用する。以下のようなテキストファイルを作成して .curl のような適当な名前で保存。

# Add CIRCUS API Authorization Header
-H "Authorization: Bearer d6fc7f5e72dc283e83bd74737647108d06cbc38a"

curl を実行する時に、以下のように設定テキストファイルのパスを指定する。

% curl -K .curl http://localhost:8080/api/series

シリーズアップロードを行うことも可能。以下の例では dicom.dcm というファイルを default ドメインにアップロードする。複数ファイルをアップロードする場合は -F files=@filename の部分を複数回繰り返すことも可能。

% curl -K .curl -F files=@dicom.dcm http://localhost:8080/api/series/domain/default

その他、以下のようなツールが API のデバッグ中には役立つ(CIRCUS とは直接関係ない)

  • jq: コマンドライン JSON ユーリティティ