APIを単一責任のレイヤーに構造化し、軽量な注入でそれらをつなぎ合わせて、テスト可能にしましょう。
このレッスンを Kodokon で開くルーティング、ビジネスロジック、データアクセスを一つのファイルに混ぜると、必ず高くつきます。サーバーを立ち上げずにビジネスルールをテストすることはできず、すべてを書き直さずにインフラを差し替えることもできません。三つのレイヤーに分割すれば、この二つの問題が解決します。ルートはURLを宣言し、委譲します。コントローラーはHTTPをビジネス呼び出しに翻訳します。リクエストを読み取り、ステータスコードを選び、レスポンスをシリアライズするのです。サービスはビジネスルールを保持し、Expressについては何一つ知りません。依存のルールは厳格です。各レイヤーは自分のすぐ下のレイヤーだけを知り、決してその逆はありません。
import { Router } from 'express';
export function createUserRouter(controller) {
const router = Router();
router.get('/', controller.list);
router.post('/', controller.create);
return router;
}コントローラーはサービスを引数として受け取るファクトリーです。ビジネスルールは一切持ちません。その仕事はreqからデータを取り出し、サービスを呼び出し、その結果をHTTPレスポンスに変えることに限られます。あらゆるエラーはnextに送られ、中央のエラーミドルウェアによって処理されます。コントローラーのあちこちに散らばったres.status(500)は決してありません。
export function createUserController(service) {
return {
async list(req, res, next) {
try {
const users = await service.listUsers();
res.json(users);
} catch (err) {
next(err);
}
},
async create(req, res, next) {
try {
const user = await service.createUser(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
},
};
}サービスは判断を集中させます。メールアドレスの一意性、作成のルールなどです。Expressについては何も知らないまま、ビジネスエラーを投げます。せいぜいstatusフィールドで補強される程度です。サービスもまた、依存であるデータリポジトリを引数として受け取ります。これが軽量な注入です。起動時に一度だけ呼び出される素朴なファクトリー関数が、重量級の注入コンテナに取って代わり、すべてのレイヤーをテストで差し替え可能にします。
export function createUserService(repository) {
return {
listUsers() {
return repository.findAll();
},
createUser(input) {
const found = repository.findByEmail(input.email);
if (found) {
const err = new Error('Email already used');
err.status = 409;
throw err;
}
return repository.insert(input);
},
};
}残るはコンポジションルートです。ファクトリーが組み合わさる唯一の場所です。createAppはインフラの依存(ここではデータベース。次のレッスンで扱います)を受け取り、まだリッスンしていない完全なExpressアプリケーションを返します。構築とリッスンを分けるのは些細なことに見えますが、これこそが、本番サーバーを三行に保ったまま、テストにAPIを一時的なポートでマウントさせるものです。データベースを作り、アプリを作り、リッスンする、それだけです。
import express from 'express';
import { createUserRepository } from './db/users.js';
import { createUserService } from './services/users.js';
import {
createUserController,
} from './controllers/users.js';
import { createUserRouter } from './routes/users.js';
export function createApp({ db }) {
const app = express();
app.use(express.json());
const repository = createUserRepository(db);
const service = createUserService(repository);
const controller = createUserController(service);
app.use('/api/users', createUserRouter(controller));
return app;
}createAppは自分でネットワークのリッスンを始めないのですか?