0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js】リファクタリングを通して学ぶドメイン駆動設計 Part6: コントローラーの実装

Posted at

はじめに

この記事は「リファクタリングを通して学ぶドメイン駆動設計」シリーズのPart6です。

今回はコントローラーの実装について解説します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • Docker Engine 27.0.3
  • Docker Compose 2
  • PostgreSQL 18.1
  • Node.js 24.11.0
  • npm 11.6.2
  • TypeScript 5.9.3
  • Express 5.1.0
  • Prisma 6.18.0
  • Zod 4.1.12

プレゼンテーション層での実装

コントローラーはプレゼンテーション層で実装します。ビジネスロジックは持たず、HTTPリクエスト/レスポンスの変換のみを行います。

プレゼンテーション層の役割

プレゼンテーション層は、外部からのリクエストを受け取り、アプリケーション層(ユースケース)を呼び出し、レスポンスを返します。

この層の責務はHTTPの詳細を扱うことであり、ビジネスロジックは含みません。

責務 説明
リクエストの受け取り HTTPリクエストからデータを取り出す
バリデーション Zodなどで入力値の形式をチェック
ユースケースの呼び出し アプリケーション層に処理を委譲
レスポンスの返却 ユースケースの結果をHTTPレスポンスに変換
エラーハンドリング 例外をHTTPステータスコードとメッセージに変換

コントローラーの実装

プレゼンテーション層では、HTTPリクエストを受け取り、ユースケースを呼び出します。

presentation/controllers/UserController.ts
import { Request, Response } from "express";
import { CreateUserUseCase } from "../../application/usecases/CreateUserUseCase";
import { DeleteUserUseCase } from "../../application/usecases/DeleteUserUseCase";
import { GetAllUsersUseCase } from "../../application/usecases/GetAllUsersUseCase";
import { GetUserUseCase } from "../../application/usecases/GetUserUseCase";
import { UpdateUserUseCase } from "../../application/usecases/UpdateUserUseCase";
import {
  CreateUserInput,
  UpdateUserInput,
  UserIdParam,
} from "../../schemas/user.schema";

export class UserController {
  constructor(
    private readonly createUserUseCase: CreateUserUseCase,
    private readonly getUserUseCase: GetUserUseCase,
    private readonly getAllUsersUseCase: GetAllUsersUseCase,
    private readonly updateUserUseCase: UpdateUserUseCase,
    private readonly deleteUserUseCase: DeleteUserUseCase
  ) {}

  async create(
    req: Request<{}, {}, CreateUserInput>,
    res: Response
  ): Promise<void> {
    try {
      const { email, name } = req.body;
      const result = await this.createUserUseCase.execute({ email, name });
      res.status(201).json(result);
    } catch (error) {
      if (error instanceof Error) {
        res.status(400).json({ error: error.message });
      }

      res.status(500).json({ error: "Failed to create user" });
    }
  }

  async getAll(_: Request, res: Response): Promise<void> {
    try {
      const result = await this.getAllUsersUseCase.execute();
      res.json(result);
    } catch (error) {
      res.status(500).json({ error: "Failed to fetch users" });
    }
  }

  async getById(req: Request<UserIdParam>, res: Response): Promise<void> {
    try {
      const { id } = req.params;
      const result = await this.getUserUseCase.execute({ id: Number(id) });

      if (!result) {
        res.status(404).json({ error: "User not found" });
        return;
      }

      res.json(result);
    } catch (error) {
      if (error instanceof Error) {
        res.status(400).json({ error: error.message });
      }

      res.status(500).json({ error: "Failed to fetch user" });
    }
  }

  async update(
    req: Request<UserIdParam, {}, UpdateUserInput>,
    res: Response
  ): Promise<void> {
    try {
      const { id } = req.params;
      const { email, name } = req.body;
      const result = await this.updateUserUseCase.execute({
        id: Number(id),
        email,
        name,
      });
      res.json(result);
    } catch (error) {
      if (error instanceof Error) {
        res.status(400).json({ error: error.message });
      }

      res.status(500).json({ error: "Failed to update user" });
    }
  }

  async delete(req: Request<UserIdParam>, res: Response): Promise<void> {
    try {
      const { id } = req.params;
      await this.deleteUserUseCase.execute({ id: Number(id) });
      res.status(204).send();
    } catch (error) {
      if (error instanceof Error) {
        res.status(400).json({ error: error.message });
      }

      res.status(500).json({ error: "Failed to delete user" });
    }
  }
}

ポイント1. 薄いコントローラー

コントローラーは非常にシンプルな構造になっています。

async create(
  req: Request<{}, {}, CreateUserInput>,
  res: Response
): Promise<void> {
  try {
    // 1. リクエストからデータを取り出す
    const { email, name } = req.body;

    // 2. ユースケースを呼び出す
    const result = await this.createUserUseCase.execute({ email, name });

    // 3. レスポンスを返す
    res.status(201).json(result);
  } catch (error) {
    // 4. エラーハンドリング
    if (error instanceof Error) {
      res.status(400).json({ error: error.message });
    }
    
    res.status(500).json({ error: "Failed to create user" });
  }
}

コントローラーが持つべきでないもの:

  • ビジネスロジック(ユースケースに任せる)
  • データベース操作(リポジトリに任せる)
  • 複雑な条件分岐(ドメイン層に任せる)
  • バリデーションロジック(Zodミドルウェアに任せる)

コントローラーが持つべきもの:

  • HTTPリクエストの解析
  • ユースケースの呼び出し
  • HTTPレスポンスの生成
  • HTTPステータスコードの決定
  • エラーのHTTPレスポンスへの変換

ポイント2. エラーハンドリング

コントローラーでは、ドメイン層やアプリケーション層で発生したエラーを、適切なHTTPステータスコードとメッセージに変換します。

try {
  const result = await this.createUserUseCase.execute({ email, name });
  res.status(201).json(result);
} catch (error) {
  if (error instanceof Error) {
    // ビジネスロジックのエラー(例: Email already exists)
    res.status(400).json({ error: error.message });
  }
  // 予期しないエラー
  res.status(500).json({ error: "Failed to create user" });
}

ポイント3. ユースケースへの依存

コントローラーは、コンストラクタでユースケースを受け取ります。

export class UserController {
  constructor(
    private readonly createUserUseCase: CreateUserUseCase,
    private readonly getUserUseCase: GetUserUseCase,
    private readonly getAllUsersUseCase: GetAllUsersUseCase,
    private readonly updateUserUseCase: UpdateUserUseCase,
    private readonly deleteUserUseCase: DeleteUserUseCase
  ) {}
}

これにより、以下のメリットがあります。

メリット1. テストが容易

// テスト時はモックユースケースを注入できる
const mockCreateUserUseCase = {
  execute: jest.fn().mockResolvedValue({
    id: 1,
    email: 'test@example.com',
    name: 'Test User',
  }),
};

const controller = new UserController(
  mockCreateUserUseCase as any,
  // ... 他のモック
);

メリット2. ビジネスロジックの分離

コントローラーはHTTPの詳細だけを扱い、ビジネスロジックはユースケースに委譲されています。

ルーティングの設定

ルーティング設定を行います。

presentation/routes/UserRoutes.ts
import express, { Request, Response } from "express";
import z from "zod";
import { validate } from "../../middlewares/validate.middleware";
import {
  CreateUserInput,
  createUserSchema,
  UpdateUserInput,
  updateUserSchema,
  UserIdParam,
  userIdSchema,
} from "../../schemas/user.schema";
import { UserController } from "../controllers/UserController";

export function setupUserRoutes(
  router: express.Router,
  userController: UserController
) {
  // ユーザー作成
  router.post(
    "/users",
    validate(z.object({ body: createUserSchema })),
    (req: Request<{}, {}, CreateUserInput>, res: Response) =>
      userController.create(req, res)
  );

  // 全ユーザー取得
  router.get("/users", (req: Request, res: Response) =>
    userController.getAll(req, res)
  );

  // 特定ユーザー取得
  router.get(
    "/users/:id",
    validate(z.object({ params: userIdSchema })),
    (req: Request<UserIdParam>, res: Response) =>
      userController.getById(req, res)
  );

  // ユーザー更新
  router.put(
    "/users/:id",
    validate(
      z.object({
        params: userIdSchema,
        body: updateUserSchema,
      })
    ),
    (req: Request<UserIdParam, {}, UpdateUserInput>, res: Response) =>
      userController.update(req, res)
  );

  // ユーザー削除
  router.delete(
    "/users/:id",
    validate(z.object({ params: userIdSchema })),
    (req: Request<UserIdParam>, res: Response) =>
      userController.delete(req, res)
  );
}

ルーティング設定のポイント

  • Zodバリデーション: validateミドルウェアでリクエストの形式をチェック
  • 型定義: Request<UserIdParam, {}, CreateUserInput>のように明示的に指定
  • シンプルな構造: コントローラーのメソッドを呼び出すだけ

これにより、元のコードにあったバリデーション機能と型安全性を維持しています。

まとめ

今回は、DDDにおけるコントローラー(Controller) の実装について説明しました。

  • ポイント
    • コントローラーは薄く保ち、HTTPの詳細のみを扱う
    • ビジネスロジックはユースケースに委譲する
    • エラーを適切なHTTPステータスコードに変換する
  • メリット
    • ビジネスロジックとHTTPの詳細が分離される
    • テストが容易になる
    • 各層の責務が明確になる

次回は、依存性注入 について説明します。これまで作成してきた各層を組み立て、実際に動作するアプリケーションを完成させます。

参考資料

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?