blog

【備忘録】REST・Clean・DDDを意識して認証APIを実装してみた話(Go)

palm

背景

現在、App Storeに登録するのを目標にとあるアプリを実装しています。

REST、Cleanの本を読み、DDD(ドメイン駆動設計)も実装に挑戦してみたいと思い、認証・認可まで実装が終わりました。

それぞれの特性を尊重し、厳格に守り、綺麗なアーキテクチャを保ちたい。
“RESTと呼ばれているもの”、”RESTっぽい”で終わらせたくない。

チェックリストを用意し、そんな思いで実装してみました。

「アプリケーション状態がハイパーテキストによって駆動されていない API は REST ではない」

Roy Fielding

REST とは

REST API は、分散型ハイパーメディア・システムの接続に使用される方式である、Representational State Transfer (REST) アーキテクチャー方式の設計原則に準拠したAPIです。REST APIは、RESTful APIまたはRESTful Web APIと呼ばれることもあります。

RESTは、開発者に比較的高いレベルの柔軟性、一貫性、拡張性、効率性を実現します。REST APIは、Web APIを構築するための軽量な方法です。

ハイパーメディアシステム:分散システム. ハイパーメディア. 画像、音声、映像などさまざまなメディアをハイパーリンクで結びつけた構成システムのこと。

6つのREST設計原則(アーキテクチャー上の制約)

  • 統一されたインターフェース
  • クライアントとサーバーの分離
  • ステートレス
  • キャッシュ性
  • 階層化されたシステム・アーキテクチャー
  • コード・オンデマンド(オプション)

これを厳格に守らないとRESTful APIと呼べ!と、Roy Fielding(提唱者)に怒られてしまいます。

今回の認証/認可の実装で気をつけた定義を紹介します。

統一されたインターフェース 

統一されたエンドポイント設計

  • エンドポイントはRESTのベストプラクティスに基づいて設計
  • POST /v1/auth/signupユーザーの新規登録リクエスト
  • POST /v1/auth/login認証トークン発行のためのログインリクエスト
  • GET /v1/me現在の認証済みユーザー情報を取得

HTTPメソッドは、リソースの操作内容に基づき正しく選定しました。(POSTをデータ生成処理に使用、GETをデータ取得に使用など)

// サインアップ用ユースケースの生成
signupUC := authuc.SignupUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
// ログイン用ユースケースの生成
loginUC := authuc.LoginUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
// 自分の情報取得用ユースケースの生成
getMeUC := meuc.GetMeUsecase{Users: d.Users}

// ハンドラーにユースケースを渡して生成
ah := authhttp.AuthHandler{Signup: signupUC, Login: loginUC}
// GET /v1/me に対応するハンドラー
mh := mehttp.MeHandler{GetMe: getMeUC}

一貫性のあるレスポンス設計

  • 期待されるエラーレスポンスコードやそれに適切なステータスコード
  • 401 Unauthorized不正トークン
  • 409 Conflictemail重複
  • 201成功

クライアントがレスポンスコード(404、201)を見れば、その状態を理解しやすいようになります。

  • ステータスコード: システムの状態に焦点を当てた呼び方
  • レスポンスコード: サーバーからの返信に含まれるコードという側面に焦点を当てた呼び方
// エラーをHTTPレスポンスに変換
if errors.Is(err, auth.ErrInvalidCredentials) {
    http.Error(w, "unauthorized", http.StatusUnauthorized) // ステータスコード 401
    return
}
if errors.Is(err, auth.ErrEmailAlreadyExists) {
    http.Error(w, "email already exists", http.StatusConflict) // ステータスコード 409
    return
}

クライアントとサーバーの分離 

役割の分離

  • 認証APIやバックエンドサーバーは、データ管理とビジネスロジック(本質的な処理)に注力させます。
  • クライアント(フロントエンド)は、エンドユーザー(最終消費者)へのインタフェース(ルール)を提供し、それを使用してAPIを呼び出します。

サーバーとクライアントの役割が明確に分かれるようになります。

拡張性の確保

  • この分離によって、クライアントが進化を遂げても、サーバー側に直接の影響を与えません。
  • その逆も可能。
// トークンが有効かを先に確認
let (meData, meResponse) = try await apiClient.request(
    "GET",
    path: "v1/me",
    requiresAuth: true
)
let meBody = String(data: meData, encoding: .utf8) ?? "<non-utf8 response>"
try apiClient.validate(meResponse, data: meData)
print("[Debug][me] status=\(meResponse.statusCode) body=\(meBody)")

ステートレス

認証の簡易性とシンプルさ

  • 各リクエストはJWT認証トークン(ログイン情報をJSON形式でコンパクトにまとめたもの)に基づき、サーバーに状態を保存する必要がない構造にしました。
  • 一度ログインし、ユーザーがトークンを取得した後、クライアントからの各リクエストは完全に独立しています。

スケーラブルで高速なAPI認証に最適

  • 認証サービス(Auth0)は、各リクエストについてJWTの正当性を検証してクライアントの認証ができます。
  • 改ざん防止効果もあります。
// JWTトークンの発行例
tok, exp, err := uc.Tokens.IssueAccessToken(rec.ID, uc.TTL)
if err != nil {
    return LoginOutput{}, err
}

// トークンと有効期限を返す
return LoginOutput{AccessToken: tok, ExpiresIn: exp}, nil

キャッシュ性

GETリクエストのキャッシュ性に配慮

  • 今回作成した API の GET /v1/me は、ログイン済みのユーザー情報を返す際にキャッシュ可能です。

認証関連APIでは動的データを扱うため、安全性を確保する目的で、基本的にはキャッシュを無効化するためのヘッダーを設定することが多いみたいです。

アクセストークンやユーザー情報の短時間キャッシュを実装し、コンテンツのやり取りを効率化することができます。

Cache-Control: no-store
Pragma: no-cache

階層化されたシステム・アーキテクチャー 

設計の階層性

今回の実装は、DDD(ドメイン駆動設計)に基づき、認証機能もいくつかのレイヤーに分離しました。

  • ドメイン層:メインのビジネスロジック(認証フローやトークン生成)を定義
  • アプリケーション層:ユースケース(利用シーン)に合わせたサービス提供
  • インフラ層:データベースアクセスや外部API呼び出しを担当

バックエンド自体、可能な限り疎結合(独立性が高く、他にレイヤーに影響しにくい)設計の原則を守り、特定のクライアントやネットワーク層に依存しない構成にすることで、柔軟性や再利用性を維持できます。

type Deps struct {
    Users       port.UserRepo       // Infrastructure
    Hasher      port.PasswordHasher // Application Logic
    Tokens      port.TokenIssuer    // Domain Logic
    DB          *pgxpool.Pool       // Infrastructure
    ShareTokens port.ShareTokenGenerator // Application Logic
}

func NewRouter(d Deps) http.Handler { ... }

コード・オンデマンド(オプション)

未使用(RESTではオプション的なもの)

  • この実装では、コード・オンデマンド対応をしませんでした。
  • RESTでも、オプション扱いのため必須ではないからです。

例えば、クライアントがサーバーからスクリプトを受け取り、実行するケースのことをコード・オンデマンドと呼びます。

まとめ:RESTアーキテクチャーを意識した実装

このAPI設計では

  • 統一されたインターフェース
  • 一貫性のあるステータスコード
  • ステートレスな通信
  • クライアントとサーバーの明確な役割分担

本実装は、RESTアーキテクチャの制約を強く意識した HTTP API であり、
HATEOAS までは踏み込んでいないものの、

「REST風」に留まらない設計判断を行った API であると考えております。

なお、本APIでは HATEOAS は、採用していません。

モバイルアプリ(iOS)をクライアントとする前提であったので、
状態遷移をハイパーメディアで表現するメリットが限定的であり、
OpenAPI による契約駆動を優先しました。

クリーンアーキテクチャ とは

クリーンアーキテクチャの基本概念は、ソフトウェア設計における依存関係の逆転と責務の分離にあり、ロバート・C・マーチン(通称「アンクルボブ」)によって提唱されました。

マーチン氏は、システムが長期間にわたり変更に強く、メンテナンスが容易であるべきだという考えを強調しています。

ソフトウェア開発の過程で、頻繁に直面する変更要求に柔軟に対応するための手法が求められていたという課題があり、これを解決するためのアプローチとしてクリーンアーキテクチャが生まれました。

クリーンアーキテクチャの主要な目標は、ビジネスロジックを外部のインフラストラクチャから切り離し、独立性を持たせること(疎結合)です。例えばデータベースやUIの変更があっても、ビジネスロジックには影響を与えないように設計されます。

クリーンアーキテクチャの原則

クリーンアーキテクチャは、ビジネスロジックを技術的詳細(DB、UI、フレームワーク)から分離し、保守性・テスト容易性・独立性を高める設計思想です。

中核となる原則は「依存の方向を常に内側(高レベルのビジネスルール=ドメイン知識そのもの)へ向ける」ことであり、外側のレイヤー(低レベルな詳細=UI/データベース周辺)が内側のビジネスロジックに影響を与えない構造のことです。

主要な設計原則

  • SOLID原則(中でも重視されるのは「依存関係逆転の原則」)
  • 依存性のルール
  • 関心事の分離
  • テスト容易性
  • 円(レイヤー)の構成

SOLID原則

クリーンアーキテクチャの基本原則の中は、SOLID原則はとても重要な原則となっています。

SOLID原則とは、以下の5つの設計指針を指します。

  • 単一責任の原則
  • オープン・クローズド原則
  • リスコフの置換原則
  • インターフェース分離の原則
  • 依存性逆転の原則

これらの原則は、クリーンアーキテクチャにおけるモジュール設計の品質を向上させ、保守性の高いシステムを作り上げるための大切な基盤となります。

単一責任の原則

各エンドポイント(signuploginme)が明確に一つの役割を果たすように実装しました。

  • signup: ユーザーの登録
  • login: ログイン処理およびトークン発行
  • me: 認証済みユーザーの情報取得

ユーザー認証やトークン発行といった責任を中心にコンポーネントの分離で、コードベース自体も独立した機能ごとに疎結合に構成しました。

ルール

  • 各クラスやモジュールは、はっきりとした単一の責任を持つべきである
  • 変更理由は1つだけとなるべき
signupUC := authuc.SignupUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
loginUC := authuc.LoginUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
getMeUC := meuc.GetMeUsecase{Users: d.Users}

r.Route("/v1", func(r chi.Router) {
    r.Post("/auth/signup", ah.SignupHTTP) // signupエンドポイント
    r.Post("/auth/login", ah.LoginHTTP)   // loginエンドポイント
    r.Get("/me", mh.MeHTTP)               // meエンドポイント
})

オープン・クローズド原則

今回の実装は、拡張に対してオープンで変更に対してクローズドな構造にしました。

  • 認証サービス(Auth0)やトークン生成処理を、ユースケース層(利用シーン)やインフラ層から独立させました。
  • 新しい認証サーバーやトークン形式を導入する拡張が簡単に行えます。
  • 他のハッシュアルゴリズムに移行したい場合でも、ポートの実装を差し替えるだけの、拡張可能な設計。

ルール

  • クラスやモジュールは拡張に対してオープンであり、修正に対してクローズドであるべき。
  • 新たな機能を追加する際に既存のコードを直接変更するのではなく、拡張機能を追加する形にする
type PasswordHasher interface {
    Hash(plain string) (string, error)     // ハッシュ化
    Verify(hash, plain string) bool       // 検証
}

リスコフの置換原則 

リポジトリやユースケースのようなクラスが抽象化され、さまざまな実装を助けます。

  • インターフェース(ポート)を設けて具象の実装(アダプタ)が交換可能となる構造を実装。
  • 認証ロジックを抽象化し、bcryptargon2 などの異なるアルゴリズムの実装を切り替えやすい設計にしました。

ルール

  • 派生クラス(サブクラス)は、基底クラス(スーパークラス)のオブジェクトの代わりに使用できるべき。
  • サブクラスを使うことで動作が壊れるようではいけない。
type Hasher struct {
    Cost int // bcryptのコスト設定
}

func (h Hasher) Hash(plain string) (string, error) {
    password, err := bcrypt.GenerateFromPassword([]byte(plain), h.Cost)
    return string(password), err
}

func (h Hasher) Verify(hash, plain string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
}

インターフェース分離の原則

各層が役割ごとに明確に分割し、一部のコードに複数のレイヤーが密結合するリスクを回避する。

  • インターフェースは、各ユースケースや目的に応じて分かれて明確に設計しました。
  • PasswordHasherインターフェースは、ハッシュと検証に分離させました。

必要最低限のメソッドのみをインターフェースに含めることで、クライアントが意図しない余計なメソッドに依存せずに済むのです。

ルール

  • クライアントは利用しないメソッドへの依存を強いられるべきではない。(大規模なインターフェースを分割して必要な機能だけ提供する)
type PasswordHasher interface {
    Hash(plain string) (string, error)
    Verify(hash, plain string) bool
}

依存関係逆転の原則

クリーンアーキテクチャは、ドメイン層に依存しない形でアプリケーションを構築することが強く求められます。

  • HTTPルーターの構築では、依存関係注入(構造体)を通じて高レベルモジュールと低レベルモジュールの結合が避けられるようにしました。
  • 高レベルモジュール(ユースケース)と低レベルモジュール(DBアクセスなど)が直接結合されることなく、ポートやアダプタを通じた抽象化を適用した設計を意識しました。

この原則に基づくことで、インフラ層やフレームワークへの依存を薄め、変更に強い設計を持つと言えることができます。

ルール

  • 高レベルモジュールは低レベルモジュールに依存すべきでない。両者は抽象に依存すべき。
  • 抽象は詳細に依存すべきではなく、詳細が抽象に依存するべき。
type Deps struct {
    Users       port.UserRepo
    Hasher      port.PasswordHasher
    Tokens      port.TokenIssuer
    DB          *pgxpool.Pool
    ShareTokens port.ShareTokenGenerator
}

func NewRouter(d Deps) http.Handler {
    // DIで依存関係を注入し、高レベルモジュールと低レベルモジュールを分離
    loginUC := authuc.LoginUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
}

依存性のルール

依存関係は内側から外側に向かってのみ存在可能となっています。

例えば、ドメイン層(内側)は、外部のアプリケーション層およびインフラストラクチャ層に依存してはならず、その逆にのみ依存が許されます。

ポートとアダプタの使用

  • 外部システム(データベース、認証サービスなど)への依存を port パッケージに抽象化し、これで、アーキテクチャ全体の依存性が逆転。
  • ユースケース層やドメイン層は、直接的に詳細な実装を呼び出すのではなく、ポート(インターフェース)に依存する形で構築しました。
  • 低レベルモジュール側(adapter層)では、ポートインターフェースに準拠した具象クラスが提供されるようにしました。

依存関係の注入

  • 依存関係は構造体を通じて注入され、レイヤー間の結合を減らしました。

ルール

  • ソースコードの依存関係は、外側から内側(エンティティ > ユースケース > インターフェースアダプター > フレームワーク)へ向かう必要がある。
  • 内側の層は外側の層について何も知らない(触れてはいけない)。

関心事の分離

明確な関心事ごとにレイヤリング・分離した実装を意識しました。

ドメイン層(エンティティ、ビジネスルール)、ユースケース層(アプリケーションロジック)、インターフェース層(ユーザーや他のシステムと接続)、インフラ層(データベースや外部API接続)に分離します。

ドメイン層

  • 業務ルールや不変条件を保証できるVO(Value Object)を利用

インターフェース層

  • internal/adapter/httpで、http.Handlerなどの外部インターフェースに対する具体的な実装を行いました。

ユースケース層

  • internal/usecase以下に、各特定の業務ロジックを分離しました。
  • LoginUsecaseはログイン処理のみを担当します。

インフラ層

  • データベースリポジトリなどの操作は infra/db フォルダ以下に実装しました。

ルール

  • データベース、UI、フレームワークなどの技術的詳細は「外側」に配置し、ビジネスルール(業務ロジック)を「内側」に隠蔽・隔離する。

ドメイン層(不変条件を保証するVO)

type Email string

func NewEmail(e string) (Email, error) {
    e = strings.TrimSpace(strings.ToLower(e))
    if e == "" {
        return "", errors.New("email is empty")
    }
    if !emailRegexp.MatchString(e) {
        return "", errors.New("email is invalid")
    }
    return Email(e), nil
}

インターフェース層(ルーティングやエラーハンドリング)

signupUC := authuc.SignupUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
loginUC := authuc.LoginUsecase{Users: d.Users, Hasher: d.Hasher, Tokens: d.Tokens, TTL: 15 * time.Minute}
getMeUC := meuc.GetMeUsecase{Users: d.Users}

ah := authhttp.AuthHandler{Signup: signupUC, Login: loginUC} // 認証用エンドポイント
mh := mehttp.MeHandler{GetMe: getMeUC}                       // 現在のユーザー情報取得

r.Route("/v1", func(r chi.Router) {
    r.Post("/auth/signup", ah.SignupHTTP) // サインアップ処理
    r.Post("/auth/login", ah.LoginHTTP)   // ログイン処理
    r.Get("/me", mh.MeHTTP)               // ユーザー情報取得
})

ユースケース層(ログインプロセスの管理)

type LoginUsecase struct {
    Users  port.UserRepo
    Hasher port.PasswordHasher
    Tokens port.TokenIssuer
}

func (uc LoginUsecase) Execute(ctx context.Context, in LoginInput) (LoginOutput, error) {
    em, err := user.NewEmail(in.Email)
    if err != nil { return LoginOutput{}, ErrBadRequest }
    rec, err := uc.Users.FindByEmail(ctx, em)
    if err != nil || !uc.Hasher.Verify(rec.PasswordHash, in.Password) { return LoginOutput{}, ErrInvalidCredentials }
    tok, exp, err := uc.Tokens.IssueAccessToken(rec.ID, uc.TTL)
    return LoginOutput{AccessToken: tok, ExpiresIn: exp}, nil
}

インフラ層(データベースリポジトリ)

type PostRepository struct {
    Pool *pgxpool.Pool
}

func (r *PostRepository) Insert(ctx context.Context, p *dpost.Post) (*dpost.Post, error) {
    const q = `
        INSERT INTO quote_posts (
          user_id, book_id, quote_text, note_text, page, visibility, share_token, created_at
        ) VALUES (
          $1, $2, $3, $4, $5, $6, $7, $8
        ) RETURNING id, created_at
    `
    var id string
    var createdAt time.Time
    err := r.Pool.QueryRow(ctx, q, ...).Scan(&id, &createdAt)
    return p, err
}

テスト容易性

各レイヤーが疎結合であるほど、個別にテストが可能になり、安定した開発を行いやすくなります。

依存注入によるテスト用モックの使用

  • 主だったユーザーレポジトリ、トークン発行機能など(コンポーネント)が依存関係の外部管理(DI)されているため、テスト時には簡単にモックへ置き換え可能な実装にしました。
  • テスト実行時にはnewMemUserRepo()でメモリ内リポジトリを使用し、外部依存を排除し、bcrypt.HasherCostもテスト用に軽量化しました。

テストケース記述

  • ユーザー登録APIの動作テストを記述。
  • APIの振る舞いが正しいかどうか、レスポンスコードやエラーハンドリングも網羅的に検証可能にしました。

ルール

  • ビジネスルール(Entity/Use Case)は、UI、データベース、Webサーバー、その他の外部要素がなくてもテストできる。
users := newMemUserRepo()
hasher := bcrypt.Hasher{Cost: 4} 
tokens := jwt.Issuer{Secret: "supersecret_supersecret"}

srv := httptest.NewServer(httpad.NewRouter(httpad.Deps{
    Users: users, Hasher: hasher, Tokens: tokens,
}))

signupBody := []byte(`{"email":"test@example.com","password":"password123"}`)
res, err := http.Post(srv.URL+"/v1/auth/signup", "application/json", bytes.NewReader(signupBody))
if res.StatusCode != http.StatusCreated {
    t.Fatalf("signup status=%d", res.StatusCode)
}

円(レイヤー)の構成

最も内側のレイヤーがドメインモデルであり、その外側にユースケース層、さらにその外側にインターフェース層が配置されます。

ビジネスルール(内側)を守り、外部のインフラとの分離させるようにするために、そのような配置をします。

円形構成に基づくレイヤリング

  • ドメイン層(internal/domain: 業務モデルとビジネスルール
  • ユースケース層(internal/usecase: アプリケーション固有のビジネスロジック(ログイン処理)
  • アダプタ層(internal/adapter: 外部へのインターフェースやDBアクセスの実装
  • インフラ層(internal/infra: 基盤依存

各レイヤー間は疎結合で設計され、上位レイヤーが下位レイヤーに依存しないようになっています。

ルール

  • Entities最も内側。企業やアプリケーションのビジネスルール。
  • Use Casesアプリケーション固有のビジネスルール。エンティティの制御。
  • Interface Adaptersユースケースと外部のデータを変換(MVCなど)
  • Frameworks& Drivers最も外側。DBやWebフレームワークなどの具体的なツール。 

ドメイン層

type User struct {
    ID    UserID
    Email Email
}

ユースケース層

func (uc LoginUsecase) Execute(ctx context.Context, in LoginInput) (LoginOutput, error) {
    ...
}

インターフェースアダプタ層

type Deps struct {
    Users port.UserRepo
    ...
}

func NewRouter(d Deps) http.Handler {
    ...
}

フレームワーク&ドライバー層

FROM golang:1.24-alpine AS build
COPY services/backend ./services/backend
RUN go build -o /bin/api ./services/backend/cmd/api

まとめ:クリーンアーキテクチャーを意識した実装

このAPI設計では

  • SOLID原則(中でも重視されるのは「依存関係逆転の原則」)
  • 依存性のルール
  • 関心事の分離
  • テスト容易性
  • 円(レイヤー)の構成

を意識し、ビジネスロジックが外部の詳細(HTTP、DB、フレームワーク)に引きずられない構造を重視して実装しました。

ドメイン駆動設計(DDD) とは

ドメイン駆動設計では「ソフトウェアで問題解決しようとする対象領域」をドメインと呼びます。

ソフトウェア開発の中心に業務知識(ドメイン知識)を置き、スペシャリストとの密な協力を通じて、ドメインモデルを構築することで、ビジネス価値の最大化を目指す手法です。

主な目的は、複雑なドメイン知識を理解し、それをソフトウェアのコードに忠実に反映させることです。

ビジネスニーズ(需要)に最適化されたシステムを構築できます。

今回取り入れたDDDの原則(ちょびっと)

  • Value Object (VO)
  • Entity (User)
  • Repositoryの設計
  • Domain Error と Transport Error の分離

Value Object (VO)

重要とした要件

VO (Value Object)は単なるプリミティブ型のラッパーではなく、不変条件を保証する責務を持つべき。

  • 特にDDDで、Emailはその妥当性を型によって保証することが重要であり、以下の最低限の要件を決めました。
  • 正規化:trim(余分なものを取り除く)およびlowercase(小文字化)しておく
  • 空文字列を許容しない
  • メールアドレスとして最低限の妥当性確認(過剰な正規表現は避けるべき)
  • VOの生成に失敗したらエラーを返し、不正な値をVOとして許容しない

Usecase層(何をしたいか)でEmail型を扱う際、それが必ず「妥当な値のみ」存在することが保証され、事前に、バグや例外が混入するリスクを大幅に軽減する効果があります。

// Email はメールアドレスを表す値オブジェクト(Value Object)です
// 役割:正規化とバリデーション
package user

import (
	"errors"
	"regexp"
	"strings"
)

// emailの正規表現(最低限なルール)
var emailRegexp = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)

type Email string

// VOを生成する関数
func NewEmail(e string) (Email, error) {
	e = strings.TrimSpace(strings.ToLower(e))
	// バリデーション
	if e == "" {
		return "", errors.New("email is empty")
	}
	if !emailRegexp.MatchString(e) {
		return "", errors.New("email is invalid")
	}
	return Email(e), nil
}

func (e Email) String() string {
	return string(e)
}

Entity (User)

重要とした要件

認証APIのようにドメインロジックが薄い領域では、エンティティに過剰な振る舞いを詰め込まない

  • ユーザーエンティティ(ユーザー情報)でトークン発行やログインロジックを持つことは避けるべき。
  • エンティティは、モデルの本質的な特性(「ユーザー」「プロフィール」など)を表現する基本的なデータ構造以上の役割を持たない。

重要なユーザーモデル(利用者)をエンティティに分けて定義することで、責任範囲を明確化しました。挙動(振る舞い)に関する実装はUsecase層(アプリケーションの動作・機能)で処理しました。

package user

type User struct {
    ID    UserID
    Email Email
    Role  string
}

Repositoryの設計

重要とした要件

リポジトリ管理(データの保管場所)を集約単位(オブジェクトの集合体)で分割する。

  • 一連の関連エンティティやVO(UserSession)について、専用のリポジトリを作成
  • 責務が増えた場合に、適切に分割可能な構造にする必要がある

UserRepoで、ユーザーレポジトリの機能を一元的に管理し、他のエンティティが登場した場合(セッション情報など)、それ専用のリポジトリ分割が容易になるように設計しました。

users := postgres.UserRepo{DB: db}

Domain Error と Transport Error の分離

重要とした要件

ドメイン(またはユースケース層)でのエラーは「意味的なエラーメッセージ」を返却する。

  • メールアドレスの重複エラーはErrEmailExistsのようにドメインで定義
  • Transport Error(HTTPステータスコードなど)は、HTTP層(HandlerやRouter)でマッピングする。
  • ドメイン・ユースケース層にHTTPステータスコードの文脈や直接的な値を流し込む設計は避ける。

ドメインエラー定義

  • ドメイン層の ErrInvalidCredentials で、意味的なエラーがVOやユースケース層で明確に定義しました。
var (
  ErrInvalidCredentials = errors.New("invalid credentials")
)

HTTPレスポンスマッピング

  • ハンドラー(HTTP層)では、ドメイン層で発生したエラーをHTTPレスポンスコードに対応付けし、プロセス間通信設計に依存させないように設計しました。
if errors.Is(err, ErrInvalidCredentials) {
    http.Error(w, "unauthorized", http.StatusUnauthorized)
    return
}

まとめ:DDDを意識した実装

このAPI設計では

  • Value Object (VO)
  • Entity (User)
  • Repositoryの設計
  • Domain Error と Transport Error の分離

を通じて、業務ルールや意味をコード上のモデルとして明確に表現することを意識しました。

さいごに

  • REST は、HTTP を使った通信や状態遷移の考え方
  • Clean Architecture は、依存関係を内側から外側へ一方向に保つための設計の考え方
  • DDD は、業務の内容をモデルとして表現して理解しやすくするためのアプローチ

だと理解しています。

正直、今の段階では少しやりすぎかなと思う部分もありますが、あとから機能を足したり変更したりするときに困らない構成にはなっているとは思っています。


今後の実装を進めながら、設計についても試行錯誤しつつ、理解を深めていけたらいいなと思っています。

参考サイト

https://www.ibm.com/jp-ja/think/topics/rest-apis

https://www.issoh.co.jp/column/details/3406

https://products.sint.co.jp/ober/blog/ddd

スポンサーリンク
ABOUT ME
palm
palm
東京通信大学 42Tokyo
2002年生まれの学生です。 趣味は小説を読むことです。たまに技術書も読みます。
記事URLをコピーしました