<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>プログラミング | エンジニアライブログ</title>
	<atom:link href="https://tomoyuki65.com/category/programming/feed" rel="self" type="application/rss+xml" />
	<link>https://tomoyuki65.com</link>
	<description></description>
	<lastBuildDate>Wed, 24 Dec 2025 09:51:14 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
<atom:link rel="hub" href="https://pubsubhubbub.appspot.com"/>
<atom:link rel="hub" href="https://pubsubhubbub.superfeedr.com"/>
<atom:link rel="hub" href="https://websubhub.com/hub"/>
<atom:link rel="self" href="https://tomoyuki65.com/category/programming/feed"/>
	<item>
		<title>PythonのDjangoで管理画面を開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python</link>
					<comments>https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Wed, 24 Dec 2025 09:51:14 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20145</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 Webアプリケーション開発の際には、別途DB操作に関わる管理画面を作ったりしますが、PythonのDjangoを使うと簡単に管理画面が作れるらし...</p>
The post <a href="https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python">PythonのDjangoで管理画面を開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img fetchpriority="high" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-35-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20194" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-35-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-35-min-300x214.png 300w" sizes="(max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>Webアプリケーション開発の際には、別途DB操作に関わる<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>管理画面</strong></span>を作ったりしますが、<strong><span style="color: #ff0000;">PythonのDjangoを使うと簡単に管理画面が作れるらしい</span></strong>というのを知ったので、試してみることにしました！</p>
<p>この記事では、そんなPythonのDjangoで管理画面を開発する方法についてまとめます。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>PythonのDjangoで管理画面を開発する方法まとめ</h2>
<p><span>まずは以下のコマンドを実行し、各種ファイルを作成します。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir django-admin &amp;&amp; cd django-admin
$ mkdir -p deploy/docker/local/py &amp;&amp; touch deploy/docker/local/py/Dockerfile
$ mkdir -p deploy/docker/local/db &amp;&amp; touch deploy/docker/local/db/Dockerfile
$ mkdir -p deploy/docker/local/db/init &amp;&amp; touch deploy/docker/local/db/init/init.sql
$ touch .env compose.yml</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル開発環境の構築については、いつものようにDockerを利用するため、試したい方は事前にDocker DesktopなどをインストールしてDockerを使える環境を準備して下さい。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# タイムゾーン設定
ENV TZ=Asia/Tokyo

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

EXPOSE 8000</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はPythonのバージョン「3.14」を使います。各種ライブラリのパッケージ管理には「poetry」を使います。開発時専用のライブラリを入れるためにpoetryのコンフィグ設定で仮想環境設定「virtualenvs.in-project」を有効化しています。</span></p>
<p>&nbsp;</p>
<p>・「deploy/docker/local/db/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/db/Dockerfile"><code>FROM postgres:18.1

ENV LANG ja_JP.utf8

# PostgreSQLの日本語化で「ja_JP.utf8」を使うために必要
RUN apt-get update &amp;&amp; \
    apt-get install -y locales &amp;&amp; \
    rm -rf /var/lib/apt/lists/* &amp;&amp; \
    localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8</code></pre>
</div>
<p>&nbsp;</p>
<p>・「deploy/docker/local/db/init/init.sql」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-sql" data-lang="SQL" data-file="deploy/docker/local/db/init/init.sql"><code>-- usersテーブルの作成
CREATE TABLE "public"."users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "uid" character varying NOT NULL, "last_name" character varying NOT NULL, "first_name" character varying NOT NULL, "email" character varying NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "deleted_at" timestamptz NULL, PRIMARY KEY ("id"));
CREATE INDEX "user_deleted_at" ON "public"."users" ("deleted_at");
CREATE UNIQUE INDEX "users_email_key" ON "public"."users" ("email");
CREATE UNIQUE INDEX "users_uid_key" ON "public"."users" ("uid");</code></pre>
</div>
<p><span style="color: #ff0000;">※管理画面は後から作成されることが多く、対象のDBには既にテーブルが存在することを前提に試すため、DBコンテナ起動時にSQLを実行して「users」テーブルを作成しておきます。尚、今回はDBにPostgreSQLを使用します。</span></p>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".env"><code>ENV=local
DB_NAME=pg-db
DB_USER=pg-user
DB_PASSWORD=pg-password
DB_HOST=db-django</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル環境用の環境変数ファイル</span></p>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  django:
    container_name: django-admin
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    volumes:
      - .:/py
    ports:
      - "8000:8000"
    env_file:
      - .env
    tty: true
    stdin_open: true</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、Dockerコンテナをビルドします。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、パッケージ管理用のpoetryの初期化をします。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>docker compose run --rm django poetry init</code></pre>
</div>
<p>&nbsp;</p>
<p><span>コマンド実行後、対話形式で各種設定について聞かれるので、「Package name [py]:」は「django-admin」、「Author [None, n to skip]:」は「n」、「Would you like to define your main dependencies interactively? (yes/no) [yes]」は「no」、「Would you like to define your development dependencies interactively? (yes/no) [yes]」は「no」を入力して実行し、それ以外はそのまま実行して進めます。</span></p>
<img decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1.jpg" alt="" width="1392" height="1012" class="aligncenter wp-image-20148 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1.jpg 1392w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1-300x218.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1-1024x744.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1-768x558.jpg 768w" sizes="(max-width: 1392px) 100vw, 1392px" />
<p>&nbsp;</p>
<p>完了後、poetryの設定ファイル「pyproject.toml」が作成されます。</p>
<p>次に以下のコマンドを実行し、今回利用する各種パッケージをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm django poetry add django "psycopg[binary]"
$ docker compose run --rm django poetry add --dev ruff</code></pre>
</div>
<p><span style="color: #ff0000;">※djangoの他、PostgreSQL用のドライバー「psycopg[binary]」および、開発専用ライブラリとして「ruff」（フォーマッター + 静的コード解析）を使います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Djangoのプロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm django poetry run django-admin startproject myapp ./src</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、以下のように各種ファイルが作成されればOKです。</p>
<img decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2.png" alt="" width="1548" height="834" class="aligncenter size-full wp-image-20151" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2.png 1548w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-300x162.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-1024x552.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-768x414.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-1536x828.png 1536w" sizes="(max-width: 1548px) 100vw, 1548px" />
<p>&nbsp;</p>
<p>次に設定用のファイル「src/myapp/settings.py」を以下のように修正します。</p>
<p>・「src/myapp/settings.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/settings.py"><code>"""
Django settings for myapp project.

Generated by 'django-admin startproject' using Django 6.0.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""

import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-jr21o1cjct+97fvqon)d$hdjm8oo7h0ko_th=dlv!8@ts#4!&amp;5'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # プロジェクトのディレクトリ追加
    'myapp'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myapp.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'myapp.wsgi.application'


# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases

DATABASES = {
    # PostgreSQLへの接続設定
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ['DB_HOST'],
        'PORT': '5432',
    }
}


# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/

# 日本語化設定
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/

STATIC_URL = 'static/'</code></pre>
</div>
<p><span style="color: #ff0000;">※INSTALLED_APPSにプロジェクトのディレクトリを追加、DATABASESにPostgreSQLへの接続設定を追加、LANGUAGE_CODEとTIME_ZONEで日本語化設定をしています。</span></p>
<p>&nbsp;</p>
<p>次にファイル「deploy/docker/local/py/Dockerfile」、「compose.yml」をそれぞれ以下のように修正します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# タイムゾーン設定
ENV TZ=Asia/Tokyo

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

# poetry.lockから依存関係をインストール
COPY pyproject.toml poetry.lock .
RUN poetry install --no-root

COPY ./src ./src

EXPOSE 8000</code></pre>
</div>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  django:
    container_name: django-admin
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    command: poetry run python ./src/manage.py runserver 0.0.0.0:8000
    volumes:
      - ./pyproject.toml:/py/pyproject.toml
      - ./poetry.lock:/py/poetry.lock
      - ./src:/py/src
    ports:
      - "8000:8000"
    env_file:
      - .env
    tty: true
    stdin_open: true
    depends_on:
      - db-django
  db-django:
    container_name: django-db
    build:
      context: .
      dockerfile: ./deploy/docker/local/db/Dockerfile
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      TZ: Asia/Tokyo
      # ローカル環境でもパスワードを有効化する設定
      POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256
    volumes:
      - ./deploy/docker/local/db/init:/docker-entrypoint-initdb.d
      - db-django-data:/var/lib/postgresql
    ports:
      - "5432:5432"
    env_file:
      - .env
volumes:
  db-django-data:</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナの再ビルドおよび起動をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:8000」を開き、以下のような画面が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3.jpg" alt="" width="2688" height="1840" class="aligncenter wp-image-20152 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3.jpg 2688w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-300x205.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-1024x701.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-768x526.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-1536x1051.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-2048x1402.jpg 2048w" sizes="auto, (max-width: 2688px) 100vw, 2688px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、DBからスキーマ情報を取得します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run python ./src/manage.py inspectdb &gt; ./src/myapp/models.py</code></pre>
</div>
<p><span style="color: #ff0000;">※尚、後からテーブルを追加するような場合は、別のファイル名で出力し、元の「src/myapp/models.py」に別途手動で設定を追加するようにして下さい。</span></p>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようにファイルが作成されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4.jpg" alt="" width="2372" height="1570" class="aligncenter wp-image-20153 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4.jpg 2372w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-300x200.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-1024x678.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-768x508.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-1536x1017.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-2048x1356.jpg 2048w" sizes="auto, (max-width: 2372px) 100vw, 2372px" />
<p><span style="color: #ff0000;">※DBからスキーマ情報を取得した場合、メタ情報の設定に「managed = False」が追加され、マイグレーション関連の処理が実行されないようになっています。</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/myapp/models.py」を以下のように修正します。</p>
<p>・「src/myapp/models.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/models.py"><code>from django.db import models
from django.utils.translation import gettext_noop

class Users(models.Model):
    id = models.BigAutoField(primary_key=True)
    uid = models.CharField(unique=True)
    last_name = models.CharField()
    first_name = models.CharField()
    email = models.CharField(unique=True)
    # 作成日と更新日を自動設定するように修正
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'users'
        # 表示設定（日本語化で翻訳されないようにgettext_noopを使用）
        verbose_name = gettext_noop("User")
        verbose_name_plural = gettext_noop("Users")</code></pre>
</div>
<p><span style="color: #ff0000;">※DBスキーマ用のモデル設定はこのファイルで行います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、管理画面用の設定ファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch src/myapp/admin.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「src/myapp/admin.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/admin.py"><code>from django.contrib import admin
from django.contrib.admin.models import LogEntry
from .models import Users

# サイト名の設定
admin.site.site_header = "Django Admin"
admin.site.site_title = "Django Admin"

# 操作履歴用にデフォルトであるモデルを管理画面に追加
admin.site.register(LogEntry)

# DBスキーマに関するモデルを管理画面に追加
admin.site.register(Users)</code></pre>
</div>
<p><span style="color: #ff0000;">※管理画面用の設定はこのファイルで行います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、管理画面用の設定をDBに追加するためのマイグレーションを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run python ./src/manage.py migrate</code></pre>
</div>
<p><span style="color: #ff0000;">※マイグレーションを実行すると、管理画面用の各種テーブル等がDBに作成されます。もしモデルの定義のメタ情報で「managed = False」が設定されていなければ、自動でマイグレーションが実行されてテーブルなど作成されたりするので注意して下さい。尚、メタ情報で「managed = False」を設定していても、管理者ユーザーの権限設定で使用できるデータを追加するためにマイグレーションの実行は必要なので、後からモデルを追加した際は再度マイグレーションの実行が必要です。</span></p>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようにマイグレーションが成功しているログ出力が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5.jpg" alt="" width="1318" height="622" class="aligncenter wp-image-20154 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5.jpg 1318w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5-300x142.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5-1024x483.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5-768x362.jpg 768w" sizes="auto, (max-width: 1318px) 100vw, 1318px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、管理画面用のスーパーユーザーを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run python ./src/manage.py createsuperuser</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、各種入力を求められるため、今回はユーザー名「local-db-root」、メールアドレスは無し、パスワード「pass-pg-2512」を設定して実行します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6.png" alt="" width="1482" height="176" class="aligncenter wp-image-20155 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6.png 1482w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6-300x36.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6-1024x122.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6-768x91.png 768w" sizes="auto, (max-width: 1482px) 100vw, 1482px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナを再起動させます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:8000/admin」を開き、以下のように管理画面用のログイン画面が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7.png" alt="" width="2678" height="1594" class="aligncenter size-full wp-image-20156" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-300x179.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-1024x610.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-1536x914.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-2048x1219.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-486x290.png 486w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>次に先ほど作成したスーパーユーザーのユーザー名「local-db-root」とパスワード「pass-pg-2512」でログインし、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9.png" alt="" width="2676" height="1828" class="aligncenter size-full wp-image-20158" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-300x205.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-1024x700.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-768x525.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-1536x1049.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-2048x1399.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これで<strong><span style="color: #3366ff;">DBスキーマから設定した対象テーブルのCRUD処理や、管理画面として最低限必要になる管理者ユーザー機能（パーミッション機能を含む）や、操作履歴のログ機能が簡単に実装</span></strong>できます。</p>
<p>&nbsp;</p>
<h3>CRUD機能を試す</h3>
<p>では簡単に実装できたCRUD機能を試してみます。</p>
<p>まずはデータを追加するため、Usersの右側の「+追加」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10.png" alt="" width="2684" height="1588" class="aligncenter size-full wp-image-20164" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10.png 2684w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-2048x1212.png 2048w" sizes="auto, (max-width: 2684px) 100vw, 2684px" />
<p>&nbsp;</p>
<p>次に「Uid」、「Last name」、「First name」、「Email」をそれぞれ入力し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20165" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これでデータ追加がされたため、一覧画面から対象のデータをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12.png" alt="" width="2682" height="1588" class="aligncenter size-full wp-image-20166" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-2048x1213.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<p>これで追加したデータの詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13.png" alt="" width="2676" height="1586" class="aligncenter size-full wp-image-20167" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-2048x1214.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>次に「Email」を修正し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20168" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これでデータの更新ができたので、一覧画面から対象のデータをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15.png" alt="" width="2674" height="1584" class="aligncenter size-full wp-image-20169" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-2048x1213.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>対象データの詳細画面から「Email」を確認し、更新されていればOKです。</p>
<p>続けて、画面右下の「削除」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16.png" alt="" width="2682" height="1586" class="aligncenter size-full wp-image-20170" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-1536x908.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-2048x1211.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<p>削除の確認画面が表示されるので、「はい、大丈夫です」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17.png" alt="" width="2680" height="1588" class="aligncenter size-full wp-image-20171" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17.png 2680w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-2048x1214.png 2048w" sizes="auto, (max-width: 2680px) 100vw, 2680px" />
<p>&nbsp;</p>
<p>これでデータが削除されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18.png" alt="" width="2684" height="1590" class="aligncenter size-full wp-image-20172" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18.png 2684w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-2048x1213.png 2048w" sizes="auto, (max-width: 2684px) 100vw, 2684px" />
<p>&nbsp;</p>
<h3>操作履歴を確認する</h3>
<p>次にメニューに追加しておいた操作履歴を確認してみます。</p>
<p>まずはトップページから「ログエントリー」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19.png" alt="" width="2678" height="1580" class="aligncenter size-full wp-image-20174" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-1024x604.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-768x453.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-1536x906.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-2048x1208.png 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>次に削除に関するデータをクリックしてみます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20.png" alt="" width="2674" height="1584" class="aligncenter size-full wp-image-20175" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-2048x1213.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>これで操作履歴の詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21.png" alt="" width="2678" height="1592" class="aligncenter size-full wp-image-20176" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-1024x609.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-1536x913.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-2048x1217.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-486x290.png 486w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>表示などをカスタマイズして改善する</h2>
<p>上記のようにCRUD機能は簡単に実装できますが、<strong><span style="color: #ff0000;">そのままだと画面表示がイマイチだったり、実務では論理削除が必要になったりする</span></strong>ため、いくつか<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>カスタマイズ</strong></span>してみます。</p>
<p>では管理画面用の設定ファイル「src/myapp/admin.py」を以下のように修正します。</p>
<p>・「src/myapp/admin.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/admin.py"><code>from django.contrib import admin
from django.contrib.admin.models import LogEntry
from django.utils import timezone

from .models import Users

# サイト名の設定
admin.site.site_header = "Django Admin"
admin.site.site_title = "Django Admin"


###############################
# 操作履歴をメニューに追加する設定
###############################


# デフォルトで存在するモデルを使用する
@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
    # 一覧画面に表示するフィールド設定
    list_display = (
        "action_time",
        "user",
        "content_type",
        "object_repr",
        "action_flag",
    )

    # 一覧画面のフィルター設定
    list_filter = ("action_flag", "content_type")

    # 検索用の対象フィールド
    search_fields = ("object_repr", "change_message", "user__username")

    # 変更不可フィールド設定
    readonly_fields = (
        "action_time",
        "user",
        "content_type",
        "object_repr",
        "action_flag",
    )

    # 詳細画面の表示設定
    fieldsets = (
        (
            None,
            {
                "fields": (
                    "action_time",
                    "user",
                    "content_type",
                    "object_repr",
                    "action_flag",
                ),
            },
        ),
    )


###############################
# 共通フィルター用のクラス定義
###############################


# 削除日用のクラス定義
class DeletedAtFilter(admin.SimpleListFilter):
    title = "削除日（deleted_at）"
    parameter_name = "deleted"

    # 選択肢の設定
    def lookups(self, request, model_admin):
        return (
            ("alive", "未削除"),
            ("deleted", "削除済み"),
        )

    # 選択肢に対するクエリ設定
    def queryset(self, request, queryset):
        value = self.value()
        if value == "alive":
            return queryset.filter(deleted_at__isnull=True)
        if value == "deleted":
            return queryset.filter(deleted_at__isnull=False)
        return queryset


###############################
# DBモデルをメニューに追加する設定
###############################


# usersの設定
@admin.register(Users)
class UsersAdmin(admin.ModelAdmin):
    # 一覧画面に表示するフィールド設定
    list_display = [field.name for field in Users._meta.get_fields()]

    # 一覧画面のフィルター設定
    list_filter = ((DeletedAtFilter),)

    # 検索用の対象フィールド
    search_fields = ("id", "uid", "last_name", "first_name", "email")

    # 変更不可フィールド設定
    readonly_fields = ("id", "created_at", "updated_at")

    # 詳細画面の表示設定
    fieldsets = (
        (
            None,
            {
                "fields": (
                    "id",
                    "uid",
                    "last_name",
                    "first_name",
                    "email",
                    "created_at",
                    "updated_at",
                    "deleted_at",
                ),
            },
        ),
    )

    # 単体用の論理削除設定
    def delete_model(self, request, obj):
        obj.updated_at = timezone.now()
        obj.deleted_at = timezone.now()
        obj.save()

    # 一括用の論理削除設定
    def delete_queryset(self, request, queryset):
        queryset.update(updated_at=timezone.now(), deleted_at=timezone.now())</code></pre>
</div>
<p><span style="color: #ff0000;">※削除処理時は論理削除をするように設定</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、フォーマット修正を行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run ruff format .
$ docker compose exec django poetry run ruff check . --select I --fix</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、静的コード解析でエラーがでないことを確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run ruff check .</code></pre>
</div>
<p>&nbsp;</p>
<h3>CRUD機能を試す</h3>
<p>では再度CRUD機能を試してみます。</p>
<p>まずはデータを追加するため、Usersの右側の「+追加」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22.png" alt="" width="2684" height="1588" class="aligncenter size-full wp-image-20178" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22.png 2684w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-2048x1212.png 2048w" sizes="auto, (max-width: 2684px) 100vw, 2684px" />
<p>&nbsp;</p>
<p>次に「Uid」、「Last name」、「First name」、「Email」をそれぞれ入力し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20179" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これでデータが追加され、一覧画面からも対象データの詳細を確認できます。</p>
<p>続けて対象データのIDをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24.png" alt="" width="2678" height="1580" class="aligncenter size-full wp-image-20180" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-1024x604.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-768x453.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-1536x906.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-2048x1208.png 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>これで追加したデータの詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20181" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>次に「Email」を修正し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26.png" alt="" width="2682" height="1586" class="aligncenter size-full wp-image-20182" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-1536x908.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-2048x1211.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<p>これでデータの更新ができたので、一覧画面から対象データのIDをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27.png" alt="" width="2674" height="1586" class="aligncenter size-full wp-image-20183" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-768x456.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-1536x911.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-2048x1215.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>対象データの詳細画面から「Email」を確認し、更新されていればOKです。</p>
<p>続けて、画面右下の「削除」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28.png" alt="" width="2678" height="1592" class="aligncenter size-full wp-image-20184" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-1024x609.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-1536x913.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-2048x1217.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-486x290.png 486w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>削除の確認画面が表示されるので、「はい、大丈夫です」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29.png" alt="" width="2674" height="1588" class="aligncenter size-full wp-image-20185" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-1024x608.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-768x456.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-1536x912.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-2048x1216.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-486x290.png 486w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>これで削除処理ができたので、対象データのDeleted_atに削除日が設定されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30.png" alt="" width="2674" height="1582" class="aligncenter size-full wp-image-20186" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-2048x1212.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<h3>操作履歴を確認する</h3>
<p>次に再度操作履歴を確認してみます。</p>
<p>まずはトップページから「ログエントリー」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31.png" alt="" width="2672" height="1590" class="aligncenter size-full wp-image-20187" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31.png 2672w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-300x179.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-1024x609.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-1536x914.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-2048x1219.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-486x290.png 486w" sizes="auto, (max-width: 2672px) 100vw, 2672px" />
<p>&nbsp;</p>
<p>これで一覧画面からも詳細が確認できます。</p>
<p>続けて最新の操作履歴をクリックしてみます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32.jpg" alt="" width="2678" height="1582" class="aligncenter wp-image-20188 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32.jpg 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-300x177.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-1024x605.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-768x454.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-1536x907.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-2048x1210.jpg 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>これで操作履歴の詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33.png" alt="" width="2674" height="1580" class="aligncenter size-full wp-image-20189" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-1024x605.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-1536x908.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-2048x1210.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<h2>グループ機能と管理ユーザー機能について</h2>
<p>詳細は割愛しますが、デフォルトで<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>グループ機能や管理ユーザー機能</strong></span>もついており、これらを使うことで<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>各種権限を付与した管理ユーザーの作成も可能</strong></span>です。</p>
<p><span style="color: #ff0000;">※例えばユーザーテーブルを確認することだけができる管理ユーザーの作成などが可能</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34.png" alt="" width="2678" height="1586" class="aligncenter size-full wp-image-20191" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-2048x1213.png 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はPythonのDjangoで管理画面を開発する方法についてまとめました。</p>
<p>実際に試してみると、<span style="color: #3366ff;"><strong>本当に簡単に管理画面を作ることができ、かつシンプルでスマートな感じもめちゃめちゃ良かった</strong></span>です。</p>
<p>私は以前にPHPのLaravel-Adminでの管理画面開発の経験もありますが、それと比較しても作りやすかったので、今後はこれを使っていくべきだなと思いました。</p>
<p>今回は簡単な例をご紹介しましたが、他にも色々カスタマイズはできそうなので、より深掘りしていくのもありかなと思います。</p>
<p>ということで、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>管理画面を作るならPythonのDjangoがおすすめ</strong></span>なので、興味がある方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python">PythonのDjangoで管理画面を開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python</link>
					<comments>https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Sat, 06 Dec 2025 00:19:19 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20088</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 日進月歩で生成AI関連が盛り上がっていますが、将来的にもあらゆるアプリケーションにAI関連機能が組み込まれるのは避けられないでしょう。 そんなA...</p>
The post <a href="https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python">PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-11.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20128" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-11.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-11-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>日進月歩で生成AI関連が盛り上がっていますが、<strong><span style="color: #ff0000;">将来的にもあらゆるアプリケーションにAI関連機能が組み込まれる</span></strong>のは避けられないでしょう。</p>
<p>そんな<span style="color: #ff0000;"><strong>AI関連機能を作るならPythonを使うのが主軸</strong></span>であり、かつ<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>マイクロサービス</strong></span>として作るなら<strong>「<span style="border-bottom: 2px solid #be3144;">FastAPI</span>」</strong>というフレームワークを使いながら、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>DDD（ドメイン駆動設計）</strong></span>と呼ばれる方法で作られることが多いのではと思います。</p>
<p>そこでこの記事では、PythonのFastAPIでDDD構成のバックエンドAPIを開発する方法についてまとめます。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</h2>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir fastapi-domain &amp;&amp; cd fastapi-domain
$ mkdir -p deploy/docker/local/py &amp;&amp; touch deploy/docker/local/py/Dockerfile
$ mkdir -p src/myapp &amp;&amp; touch src/myapp/__init__.py src/myapp/main.py
$ touch .env compose.yml</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル開発環境の構築については、いつものようにDockerを利用するため、試したい方は事前にDocker DesktopなどをインストールしてDockerを使える環境を準備して下さい。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

EXPOSE 9004</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はPythonのバージョン「3.14」を使います。各種ライブラリのパッケージ管理には「poetry」を使います。開発時専用のライブラリを入れるためにpoetryのコンフィグ設定で仮想環境設定「virtualenvs.in-project」を有効化しています。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/main.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/main.py"><code>from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

app = FastAPI()


@app.get("/", response_class=PlainTextResponse)
def root() -&gt; str:
    return "Hello World !!"</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".env"><code>ENV=local
TEST_VALUE=test123</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル環境用の環境変数ファイル</span></p>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  fastapi:
    container_name: fastapi
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    volumes:
      - .:/py
    ports:
      - "9004:9004"
    env_file:
      - .env
    tty: true
    stdin_open: true</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナをビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、パッケージ管理用のpoetryの初期化をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm fastapi poetry init</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、対話形式で各種設定について聞かれるので、「Package name [py]:」は「fastapi-domain」、「Author [None, n to skip]:」は「n」、「Would you like to define your main dependencies interactively? (yes/no) [yes]」は「no」、「Would you like to define your development dependencies interactively? (yes/no) [yes]」は「no」を入力して実行し、それ以外はそのまま実行して進めます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1.jpg" alt="" width="1660" height="1016" class="aligncenter wp-image-20094 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1.jpg 1660w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-300x184.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-1024x627.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-768x470.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-1536x940.jpg 1536w" sizes="auto, (max-width: 1660px) 100vw, 1660px" />
<p>&nbsp;</p>
<p>完了後、poetryの設定ファイル「pyproject.toml」が作成されます。</p>
<p>次に以下のコマンドを実行し、今回利用する各種パッケージをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm fastapi poetry add fastapi uvicorn pydantic
$ docker compose run --rm fastapi poetry add --dev ruff mypy</code></pre>
</div>
<p><span style="color: #ff0000;">※FastAPI用の「fastapi」、「uvicorn」、「pydantic」、開発専用ライブラリとして「ruff」（フォーマッター + 静的コード解析）、「mypy」（型チェック）を使います。</span></p>
<p>&nbsp;</p>
<p>次にpoetryの設定ファイル「pyproject.toml」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="pyproject.toml"><code>[project]
name = "fastapi-domain"
version = "0.1.0"
description = ""
authors = [
    {name = "Your Name",email = "you@example.com"}
]
readme = "README.md"
requires-python = "&gt;=3.14"
dependencies = [
    "fastapi (&gt;=0.122.0,&lt;0.123.0)",
    "uvicorn (&gt;=0.38.0,&lt;0.39.0)",
    "pydantic (&gt;=2.12.5,&lt;3.0.0)"
]

# パッケージの読み込み設定
packages = [
    { include = "myapp", from = "src" }
]


[build-system]
requires = ["poetry-core&gt;=2.0.0,&lt;3.0.0"]
build-backend = "poetry.core.masonry.api"

[dependency-groups]
dev = [
    "ruff (&gt;=0.14.7,&lt;0.15.0)",
    "mypy (&gt;=1.19.0,&lt;2.0.0)"
]


# =========================================================
# ruffの設定（フォーマッター + 静的コード解析）
# =========================================================
[tool.ruff]
# Pythonのバージョン指定
target-version = "py314"

# チェック対象外のディレクトリ
exclude = [
    ".venv",
    ".mypy_cache",
    ".ruff_cache",
]

# ==========
# lint設定
# ==========
lint.select = [
    "E", # Pycodestyle エラー
    "F", # Pyflakes
    "B", # bugbear（バグ検出）
    "I", # isort（import整理）
    "UP", # pyupgrade（最新構文）
]

# =========================================================
# mypyの設定（型チェック）
# =========================================================
[tool.mypy]
# Pythonのバージョン指定
python_version = "3.14"

# 厳密モードの有効化
strict = true

# 追加の表示設定
warn_unused_configs = true # 無効な設定や使われていない設定を警告として表示
warn_unreachable = true # 到達不可能コードを警告
show_error_context = true # エラー発生箇所の前後コンテキストを表示
show_column_numbers = true # エラー箇所の列番号を表示

# Pydantic v2の設定
plugins = ["pydantic.mypy"]

# チェック対象外のディレクトリ
exclude = [
    ".venv",
    ".mypy_cache",
    ".ruff_cache",
]

# 外部ライブラリは無視 
ignore_missing_imports = true</code></pre>
</div>
<p><span style="color: #ff0000;">※[project]にpackagesの設定を追加（srcディレクトリのmyappパッケージを読み込む）、ruffとmypyの設定（ほぼ最小構成のはず！）を追加します。</span></p>
<p>&nbsp;</p>
<p>次にファイル「deploy/docker/local/py/Dockerfile」、「compose.yml」をそれぞれ以下のように修正します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# タイムゾーン設定
ENV TZ=Asia/Tokyo

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

# poetry.lockから依存関係をインストール
COPY pyproject.toml poetry.lock .
RUN poetry install --no-root

COPY ./src ./src
EXPOSE 9004</code></pre>
</div>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  fastapi:
    container_name: fastapi
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    command: poetry run uvicorn myapp.main:app --app-dir src --reload --host 0.0.0.0 --port 9004
    volumes:
      - ./pyproject.toml:/py/pyproject.toml
      - ./poetry.lock:/py/poetry.lock
      - ./src:/py/src
    ports:
      - "9004:9004"
    env_file:
      - .env
    tty: true
    stdin_open: true</code></pre>
</div>
<p><span style="color: #ff0000;">※commandでオプション「&#8211;app-dir src」を付与し、「myapp」がトップレベルになるようにしています。これによってimport時のファイルパスが短くなります。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナの再ビルドおよび起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ログ出力を確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose logs</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようにFastAPIの起動に関するログ出力がされていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2.png" alt="" width="1048" height="204" class="aligncenter size-full wp-image-20098" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2.png 1048w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2-300x58.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2-1024x199.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2-768x149.png 768w" sizes="auto, (max-width: 1048px) 100vw, 1048px" />
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:9004」を開き、以下のように「Hello World !!」が出力されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3.png" alt="" width="890" height="524" class="aligncenter size-full wp-image-20099" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3.png 890w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3-768x452.png 768w" sizes="auto, (max-width: 890px) 100vw, 890px" />
<p>&nbsp;</p>
<h3>OpenAPIの仕様書を確認する方法</h3>
<p>FastAPIでは<strong><span style="color: #3366ff;">OpenAPI形式の仕様書が自動で作成される</span></strong>ため、確認したい場合はブラウザで「http://localhost:9004/docs」または「http://localhost:9004/redoc」を開くと確認できます。</p>
<p>・「http://localhost:9004/docs」</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4.png" alt="" width="2690" height="1832" class="aligncenter size-full wp-image-20100" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4.png 2690w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-300x204.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-1024x697.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-768x523.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-1536x1046.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-2048x1395.png 2048w" sizes="auto, (max-width: 2690px) 100vw, 2690px" />
<p>&nbsp;</p>
<p>・「http://localhost:9004/redoc」</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5.png" alt="" width="2682" height="1840" class="aligncenter size-full wp-image-20102" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-300x206.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-1024x703.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-768x527.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-1536x1054.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-2048x1405.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<h3>コード修正後に利用するコマンド</h3>
<p>今回は開発専用ライブラリとして<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「ruff」（フォーマッター + 静的コード解析）、「mypy」（型チェック）を使える</strong></span>ようにしたため、コードを修正した際は必要に応じて以下のコマンドを実行し、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>フォーマット統一</strong></span>および、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>警告やエラーがでていないことを確認</strong></span>して下さい。</p>
<p><strong>・フォーマット修正</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff format .</code></pre>
</div>
<p>&nbsp;</p>
<p><strong>・importの並び替え（フォーマット修正）</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff check . --select I --fix</code></pre>
</div>
<p>&nbsp;</p>
<p><strong>・静的コード解析によるチェック</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff check .</code></pre>
</div>
<p>&nbsp;</p>
<p><strong>・型チェック</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run mypy .</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>共通設定用の各種ファイル作成</h2>
<p>次に以下のコマンドを実行し、共通設定用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/infrastructure/config
$ touch src/myapp/infrastructure/__init__.py src/myapp/infrastructure/config/settings.py src/myapp/infrastructure/config/__init__.py
$ mkdir -p src/myapp/infrastructure/logger
$ touch src/myapp/infrastructure/logger/__init__.py src/myapp/infrastructure/logger/logger.py
$ mkdir -p src/myapp/application/context
$ touch src/myapp/application/__init__.py src/myapp/application/context/__init__.py src/myapp/application/context/request_context.py
$ mkdir -p src/myapp/presentation/middleware
$ touch src/myapp/presentation/__init__.py src/myapp/presentation/middleware/__init__.py src/myapp/presentation/middleware/request_middleware.py
$ mkdir -p src/myapp/presentation/exception
$ touch src/myapp/presentation/exception/__init__.py src/myapp/presentation/exception/definitions.py src/myapp/presentation/exception/handlers.py
$ mkdir -p src/myapp/infrastructure/database
$ touch src/myapp/infrastructure/database/__init__.py src/myapp/infrastructure/database/database.py
$ mkdir -p src/myapp/presentation/schema/common
$ touch src/myapp/presentation/schema/__init__.py src/myapp/presentation/schema/common/error.py src/myapp/presentation/schema/common/__init__.py</code></pre>
</div>
<p><span style="color: #ff0000;">※Pythonの場合は、各ディレクトリをパッケージと認識させるための「__init__.py」も作成します。（最近は省略もできるっぽいですが、テストコードなどで影響が出る可能性があったりして、基本的に作った方がいいようです。）</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/infrastructure/config/settings.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/config/settings.py"><code>from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    env: str = "local"
    # デフォル値を付けない場合は必須になるので注意！
    test_value: str

    model_config = SettingsConfigDict(
        env_file=".env",
    )


# サーバー起動時にインスタンス化
# .envがない場合はOSから環境変数を読み込む
settings = Settings()


def get_settings() -&gt; Settings:
    return settings</code></pre>
</div>
<p><span style="color: #ff0000;">※環境変数を読み込むためのファイルです。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/config/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/config/__init__.py"><code>from .settings import Settings, get_settings

__all__ = ["Settings", "get_settings"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/logger/logger.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/logger/logger.py"><code>import logging
from logging.config import dictConfig

from myapp.application.context import request_id
from myapp.infrastructure.config.settings import get_settings

settings = get_settings()


class EnvFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -&gt; bool:
        record.env = settings.env
        return True

class RequestIDFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -&gt; bool:
        record.request_id = request_id.get()
        return True


LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {
        "request_id_filter": {"()": RequestIDFilter},
        "env_filter": {"()": EnvFilter},
    },
    "formatters": {
        "default": {
            "format": (
                "%(asctime)s [%(levelname)s] "
                "ENV=%(env)s - %(name)s "
                "[request_id=%(request_id)s] "
                "%(message)s"
            )
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "filters": ["request_id_filter", "env_filter"],
            "formatter": "default",
        }
    },
    "root": {
        "level": "INFO",
        "handlers": ["console"],
    },
}


def init_logging() -&gt; None:
    dictConfig(LOGGING)


def get_logger(name: str = "fastapi-domain") -&gt; logging.Logger:
    return logging.getLogger(name)</code></pre>
</div>
<p><span style="color: #ff0000;">※ロガーの設定変更が必要な場合はこのファイルを修正して下さい。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/logger/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/logger/__init__.py"><code>from .logger import get_logger, init_logging

__all__ = ["init_logging", "get_logger"]</code></pre>
</div>
<p><span style="color: #ff0000;">※「__init__.py」を設定することで、他のファイルからimportする時のパスを短くできます。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/application/context/request_context.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/context/request_context.py"><code>from contextvars import ContextVar

# リクエストID（リクエスト単位で一意のID）
request_id: ContextVar[str | None] = ContextVar("request_id", default=None)</code></pre>
</div>
<p><span style="color: #ff0000;">※リクエスト単位で一意の値を持たせられるようにするため、コンテキスト変数を作成します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/application/context/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/context/__init__.py"><code>from .request_context import request_id

__all__ = ["request_id"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/middleware/request_middleware.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/middleware/request_middleware.py"><code>import uuid

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response

from myapp.application.context import request_id
from myapp.infrastructure.logger import get_logger


class RequestMiddleware(BaseHTTPMiddleware):
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -&gt; Response:
        # UUIDの取得
        new_uuid = str(uuid.uuid4())

        # リクエストID用コンテキストにUUIDを設定
        request_id.set(new_uuid)

        # レスポンスヘッダーにX-Request-IDを設定
        response = await call_next(request)
        response.headers["X-Request-ID"] = new_uuid

        # リクエスト開始ログ出力
        logger = get_logger()
        logger.info("start request !!")

        return response</code></pre>
</div>
<p><span style="color: #ff0000;">※コンテキストとレスポンスヘッダーにリクエストIDを設定します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/middleware/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/middleware/__init__.py"><code>from .request_middleware import RequestMiddleware

__all__ = ["RequestMiddleware"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/exception/definitions.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/exception/definitions.py"><code>######################
# 例外定義
######################

# DBエラー
class DatabaseError(Exception):
    def __init__(self, message: str):
        self.message = message
        super().__init__(message)</code></pre>
</div>
<p><span style="color: #ff0000;">※カスタム例外を定義します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/exception/handlers.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/exception/handlers.py"><code>from typing import cast

from fastapi import Request
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

from myapp.infrastructure.logger import get_logger

######################
# 例外用のハンドラー定義
######################


# RequestValidationError用
async def request_validation_error_handler(
    request: Request, exc: Exception
) -&gt; JSONResponse:
    cast_exc = cast(RequestValidationError, exc)
    logger = get_logger()
    logger.warning(f"バリデーションエラー: {cast_exc.errors()}")
    return await request_validation_exception_handler(request, cast_exc)

# バリデーションエラー
async def valid_error_exception_handler(
    request: Request, exc: Exception
) -&gt; JSONResponse:
    return JSONResponse(
        status_code=422,
        content={"detail": str(exc)},
    )


# DBエラー
async def db_error_exception_handler(request: Request, exc: Exception) -&gt; JSONResponse:
    return JSONResponse(
        status_code=500,
        content={"detail": str(exc)},
    )


# 共通エラー
async def general_exception_handler(request: Request, exc: Exception) -&gt; JSONResponse:
    return JSONResponse(
        status_code=500,
        content={"detail": f"Internal Server Error: {str(exc)}"},
    )</code></pre>
</div>
<p><span style="color: #ff0000;">※例外発生時の処理を制御させるためのハンドラー設定です。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/exception/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/exception/__init__.py"><code>from .definitions import DatabaseError
from .handlers import (
    db_error_exception_handler,
    general_exception_handler,
    request_validation_error_handler,
    valid_error_exception_handler,
)

__all__ = [
    "request_validation_error_handler",
    "valid_error_exception_handler",
    "DatabaseError",
    "db_error_exception_handler",
    "general_exception_handler",
]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/database/database.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/database/database.py"><code># 今回はDBインスタンスはダミーとする。
def get_db() -&gt; str:
    return "DBインスタンスのダミー"</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はDBは使わないで進めるため、ダミーとして文字列を返すように定義する。実際にDBを使う場合は、インスタンスを返すようにして下さい。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/database/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/database/__init__.py"><code>from .database import get_db

__all__ = ["get_db"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/common/error.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/common/error.py"><code>from pydantic import BaseModel, ConfigDict


# 400 Bad Request
class BadRequestResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Bad Request",
            }
        },
    )


# 401 Unauthorized
class UnauthorizedResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Unauthorized",
            }
        },
    )


# 422 Unprocessable Entity
class UnprocessableEntityResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Unprocessable Entity",
            }
        },
    )


# 500 Internal Server Error
class InternalServerErrorResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Internal Server Error",
            }
        },
    )</code></pre>
</div>
<p><span style="color: #ff0000;">※主にOpenAPI用に共通エラー用のスキーマを定義します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/common/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/common/__init__.py"><code>from .error import (
    BadRequestResponse,
    InternalServerErrorResponse,
    UnauthorizedResponse,
    UnprocessableEntityResponse,
)

__all__ = [
    "BadRequestResponse",
    "UnauthorizedResponse",
    "UnprocessableEntityResponse",
    "InternalServerErrorResponse",
]</code></pre>
</div>
<p>&nbsp;</p>
<h2>DDD（ドメイン駆動設計）のディレクトリ構成について</h2>
<p>この後にDDD（ドメイン駆動設計）でAPIを作成していきますが、ディレクトリ構成としてはDDDの思想に基づいたレイヤードアーキテクチャを採用しています。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>/src
 ├── /myapp
 |    ├── /application（アプリケーション層）
 |    |    ├── /context（独立した値を持たせるためのコンテキスト変数）
 |    |    └── /usecase（ユースケース層）
 |    |
 |    ├── /di（依存注入によってユースケースのインスタンスをまとめる）
 |    |
 |    ├── /domain（ドメイン層）
 |    |    ├── entity（ドメインモデルの定義）
 |    |    ├── ValueObject（意味のある値を扱うためのオブジェクト定義）
 |    |    ├── repository（リポジトリのインターフェース定義）
 |    |    └── （仮）service（外部サービスのインターフェース定義）
 |    |
 |    ├── /infrastructure（インフラストラクチャー層）
 |    |    ├── /database（データベース設定）
 |    |    ├── /logger（ロガーの実装）
 |    |    ├── /persistence（リポジトリの実装。DB操作による永続化層。）
 |    |    ├── （仮）/cache（キャッシュを含めたリポジトリの実装。インターフェースはリポジトリと同一。）
 |    |    └── （仮）/externalapi（外部サービスの実装）
 |    |
 |    └── /presentation（プレゼンテーション層）
 |         ├── /exception（カスタム例外定義）
 |         ├── /handler（ハンドラー層）
 |         ├── /middleware（ミドルウェア定義）
 |         ├── /router（ルーター定義）
 |         └── /schema（APIの入出力の仕様を決めるスキーマ定義）
 |
 └── /tests（テストコード用ディレクトリ）
</code></pre>
</div>
<p><span style="color: #ff0000;">※（仮）のものは将来的に追加する想定の例です。</span></p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Postドメインを例にAPIを作る</h2>
<p>次に以下の手順でPostドメインを例にAPIを作成します。</p>
<p>&nbsp;</p>
<h3>ドメインの定義</h3>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir src/myapp/domain/post
$ touch src/myapp/domain/__init__.py src/myapp/domain/post/text.py src/myapp/domain/post/entity.py src/myapp/domain/post/repository.py src/myapp/domain/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/domain/post/text.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/text.py"><code># 型ヒントでまだ定義されていないクラス名を文字列として書けるようにする設定
# これによってクラス同士が互いを参照するような場合でも型ヒントが使いやすくなる
from __future__ import annotations

from dataclasses import dataclass


# 値オブジェクトの定義（フィールドは変更不可設定）
@dataclass(frozen=True)
    class Text:
    value: str

def __post_init__(self) -&gt; None:
    ######################
    # バリデーションチェック
    ######################
    if not self.value:
        raise ValueError("テキストの値は必須です。")

    if len(self.value) &gt; 20:
        raise ValueError("テキストの値は20文字以内で入力して下さい。")

    # オブジェクトを文字列として表示する際に呼ばれるメソッド
    # print() や str() を使う時にこのメソッドの戻り値が表示されます
    def __str__(self) -&gt; str:
        return self.value

    # DBからの復元用クラスメソッド（チェック処理無し）
    @classmethod
    def from_raw(cls, raw_value: str) -&gt; Text:
        # __init__を呼ばずにインスタンス作成した値を設定
        obj = cls.__new__(cls)
        object.__setattr__(obj, "value", raw_value)
        return obj</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はPostエンティティ（モデル）のフィールド「text」に意味があることを想定して値オブジェクト（意味のある値を扱うためのオブジェクト）を定義します。新規データ登録時は必ずバリデーションチェックが必要ですが、DB登録後のデータ取得時は基本的にバリデーションチェックは不要なため、復元用のメソッド（from_raw）も定義します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/domain/post/entity.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/entity.py"><code># 型ヒントでまだ定義されていないクラス名を文字列として書けるようにする設定
# これによってクラス同士が互いを参照するような場合でも型ヒントが使いやすくなる
from __future__ import annotations

from dataclasses import dataclass

from .text import Text


@dataclass
class Post:
    text: Text
    id: int = 0

    def __post_init__(self) -&gt; None:
    ######################
    # バリデーションチェック
    ######################
    if self.text is None:
        raise ValueError("textは必須です。")

    # DBからの復元用クラスメソッド（チェック処理無し）
    @classmethod
    def from_raw(cls, raw_id: int, raw_text: Text) -&gt; Post:
        # __init__を呼ばずにインスタンス作成した値を設定
        obj = cls.__new__(cls)
        obj.id = raw_id
        obj.text = raw_text
        return obj</code></pre>
</div>
<p><span style="color: #ff0000;">※Postエンティティ（モデル）を定義します。フィールド「text」の型には上記で定義した値オブジェクトを利用します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/domain/post/repository.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/repository.py"><code>from abc import ABC, abstractmethod

from .entity import Post


class PostRepository(ABC):
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    @abstractmethod
    async def create(self, db: str, post: Post) -&gt; Post:
        pass

    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    @abstractmethod
    async def find_all(self, db: str) -&gt; list[Post]:
        pass</code></pre>
</div>
<p><span style="color: #ff0000;">※Postモデルのリポジトリ用の抽象クラス（他のクラスに継承し、定義したメソッドを持つことを強制させる設計図のようなもの）を定義します。今回は例として新規データ作成用と全データ取得用の2種類のみ定義します。ユースケース層でトランザクション管理をするため、メソッドでDBインスタンスを渡せる設計にしてますが、今回はDB設定はダミー値を使うのでstr型で設定してます。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/domain/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/__init__.py"><code>from .entity import Post
from .repository import PostRepository
from .text import Text

__all__ = ["Post", "PostRepository", "Text"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>リポジトリの実装</h3>
<p>次に以下のコマンドを実行し、リポジトリ実装用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/infrastructure/persistence/post
$ touch src/myapp/infrastructure/persistence/__init__.py src/myapp/infrastructure/persistence/post/post_repo_impl.py src/myapp/infrastructure/persistence/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/infrastructure/persistence/post/post_repo_impl.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/persistence/post/post_repo_impl.py"><code>from myapp.domain.post import Post, PostRepository, Text
from myapp.presentation.exception import DatabaseError


class PostRepositoryImpl(PostRepository):
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    async def create(self, db str, post: Post) -&gt; Post:
        try:
            # DBへデータ登録を完了した想定でpostを返す
            return post

        except Exception as e:
            raise DatabaseError(f"DBエラーが発生しました。: {str(e)}") from e

    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    async def find_all(self, db str) -&gt; list[Post]:
        try:
            # DBからデータを取得した想定で固定値を返す
            posts = [
                Post.from_raw(
                    raw_id=1, raw_text=Text.from_raw(raw_value="Postデータ１")
                ),
                Post.from_raw(
                    raw_id=2, raw_text=Text.from_raw(raw_value="Postデータ２")
                ),
            ]

            return posts

        except Exception as e:
            raise DatabaseError(f"DBエラーが発生しました。: {str(e)}") from e</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はDBを使わないので固定値で返しますが、DBから取得したデータはfrom_rawメソッドを利用してドメインに変換して返します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/persistence/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/persistence/post/__init__.py"><code>from .post_repo_impl import PostRepositoryImpl

__all__ = ["PostRepositoryImpl"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>スキーマの定義</h3>
<p>次に以下のコマンドを実行し、APIの入出力の仕様を決めるスキーマ定義用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/presentation/schema/post
$ touch src/myapp/presentation/schema/post/request.py src/myapp/presentation/schema/post/response.py src/myapp/presentation/schema/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/presentation/schema/post/request.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/post/request.py"><code>from pydantic import BaseModel, ConfigDict, Field


class PostCreateRequest(BaseModel):
    text: str = Field(..., max_length=20, description="テキスト")

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "text": "Postテキスト",
            }
        },
    )</code></pre>
</div>
<p><span style="color: #ff0000;">※リクエスト用のスキーマ定義の場合、各種フィールどには「Field()」を使ってバリデーションチェックの条件を付けます。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/post/response.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/post/response.py"><code>from pydantic import BaseModel


class PostResponse(BaseModel):
    id: int
    text: str</code></pre>
</div>
<p><span style="color: #ff0000;">※Postデータをレスポンス結果として返すためのスキーマ定義</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/post/__init__.py"><code>from .request import PostCreateRequest
from .response import PostResponse

__all__ = ["PostCreateRequest", "PostResponse"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>ユースケースの定義</h3>
<p>次に以下のコマンドを実行し、ユースケース用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/application/usecase/post
$ touch src/myapp/application/usecase/__init__.py src/myapp/application/usecase/post/create_post.py src/myapp/application/usecase/post/get_posts.py src/myapp/application/usecase/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/application/usecase/post/create_post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/usecase/post/create_post.py"><code>import logging

from fastapi import HTTPException

from myapp.domain.post import Post, PostRepository, Text
from myapp.presentation.schema.post import PostCreateRequest, PostResponse


class CreatePostUsecase:
    # コンストラクタで依存注入する
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    def __init__(
        self, logger: logging.Logger, db: str, post_repo: PostRepository
    ) -&gt; None:
        self.logger = logger
        self.db = db
        self.post_repo = post_repo

    async def execute(self, req: PostCreateRequest) -&gt; PostResponse:
        try:
            # 新規Postデータ作成
            newPost = Post(text=Text(req.text))

            # DB登録
            post = await self.post_repo.create(self.db, newPost)
            return PostResponse(id=post.id, text=post.text.value)

        # ValueError発生時の例外処理
        except ValueError as e:
            self.logger.warning("バリデーションエラー: %s", e)
            raise HTTPException(
                status_code=422, detail=f"バリデーションエラー: {str(e)}"
            ) from e

        except Exception as e:
            self.logger.error("DB登録に失敗しました。: %s", e)
            raise e</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/application/usecase/post/get_posts.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/usecase/post/get_posts.py"><code>import logging

from myapp.domain.post import PostRepository
from myapp.presentation.schema.post import PostResponse


class GetPostsUsecase:
    # コンストラクタで依存注入する
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    def __init__(
        self, logger: logging.Logger, db: str, post_repo: PostRepository
    ) -&gt; None:
        self.logger = logger
        self.db = db
        self.post_repo = post_repo

    async def execute(self) -&gt; list[PostResponse]:
        try:
            # データ取得
            posts = await self.post_repo.find_all(self.db)
            return [PostResponse(id=p.id, text=p.text.value) for p in posts]

        except Exception as e:
            self.logger.error("DBからのデータ取得に失敗しました。: %s", e)
            raise e</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/application/usecase/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/usecase/post/__init__.py"><code>from .create_post import CreatePostUsecase
from .get_posts import GetPostsUsecase

__all__ = ["CreatePostUsecase", "GetPostsUsecase"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>DIコンテナの作成</h3>
<p>次に以下のコマンドを実行し、依存注入によってユースケースのインスタンスをまとめるためのファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/di
$ touch src/myapp/di/container.py src/myapp/di/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「src/myapp/di/container.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/di/container.py"><code>import logging
from typing import Annotated

from fastapi import Depends

from myapp.application.usecase.post import CreatePostUsecase, GetPostsUsecase
from myapp.domain.post import PostRepository
from myapp.infrastructure.database import get_db
from myapp.infrastructure.logger import get_logger
from myapp.infrastructure.persistence.post import PostRepositoryImpl


# リポジトリのDI用関数
def get_post_repository() -&gt; PostRepository:
    return PostRepositoryImpl()


# Post用ユースケース
def create_post_usecase(
    logger: Annotated[logging.Logger, Depends(get_logger)],
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    db: Annotated[str, Depends(get_db)],
    post_repo: Annotated[PostRepository, Depends(get_post_repository)],
) -&gt; CreatePostUsecase:
    return CreatePostUsecase(logger=logger, db=db, post_repo=post_repo)


def get_posts_usecase(
    logger: Annotated[logging.Logger, Depends(get_logger)],
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    db: Annotated[str, Depends(get_db)],
    post_repo: Annotated[PostRepository, Depends(get_post_repository)],
) -&gt; GetPostsUsecase:
    return GetPostsUsecase(logger=logger, db=db, post_repo=post_repo)</code></pre>
</div>
<p><span style="color: #ff0000;">※FastAPI用のDependsを使って依存注入しています。今回使ったPythonのバージョンでは「Annotated」を使った依存注入が最適でした。（Pythonのバージョンによってここら辺の書き方が変わるので注意）</span></p>
<p>&nbsp;</p>
<h3>ハンドラーの定義</h3>
<p>次に以下のコマンドを実行し、ハンドラー用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/presentation/handler/post
$ touch src/myapp/presentation/handler/__init__.py src/myapp/presentation/handler/post/post.py src/myapp/presentation/handler/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/presentation/handler/post/post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/handler/post/post.py"><code>from typing import Annotated

from fastapi import Depends

from myapp.application.usecase.post import CreatePostUsecase, GetPostsUsecase
from myapp.di.container import create_post_usecase, get_posts_usecase
from myapp.presentation.schema.post import PostCreateRequest, PostResponse


class CreatePostHandler:
    def __init__(
        self,
        usecase: Annotated[CreatePostUsecase, Depends(create_post_usecase)],
    ):
        self.usecase = usecase

    async def execute(self, req: PostCreateRequest) -&gt; PostResponse:
        return await self.usecase.execute(req)


class GetPostsHandler:
    def __init__(
        self,
        usecase: Annotated[GetPostsUsecase, Depends(get_posts_usecase)],
    ):
        self.usecase = usecase

    async def execute(self) -&gt; list[PostResponse]:
        return await self.usecase.execute()</code></pre>
</div>
<p><span style="color: #ff0000;">※DIコンテナを使ってハンドラーに依存注入して実行します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/handler/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from .post import CreatePostHandler, GetPostsHandler

__all__ = ["CreatePostHandler", "GetPostsHandler"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>ルーター設定</h3>
<p>次に以下のコマンドを実行し、ルーター設定用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/presentation/router
$ touch src/myapp/presentation/router/router.py src/myapp/presentation/router/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/presentation/router/router.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from typing import Annotated, Any

from fastapi import APIRouter, Depends

from myapp.presentation.handler.post import CreatePostHandler, GetPostsHandler
from myapp.presentation.schema.common import InternalServerErrorResponse
from myapp.presentation.schema.post import PostCreateRequest, PostResponse

router = APIRouter()

# OpenAPI用の共通エラーレスポンス定義
common_error_res: dict[int | str, dict[str, Any]] | None = {
    500: {"description": "サーバーエラー", "model": InternalServerErrorResponse},
}


@router.post(
    "/post",
    response_model=PostResponse,
    status_code=201,
    responses=common_error_res,
    summary="Postデータ新規作成",
    description="Postデータを新規作成する",
    tags=["Post"],
)
async def create_post(
    req: PostCreateRequest,
    handler: Annotated[CreatePostHandler, Depends(CreatePostHandler)],
) -&gt; PostResponse:
    return await handler.execute(req)


@router.get(
    "/posts",
    response_model=list[PostResponse],
    status_code=200,
    responses=common_error_res,
    summary="Postデータ全件取得",
    description="Postデータを全件取得する",
    tags=["Post"],
)
async def get_posts(
    handler: Annotated[GetPostsHandler, Depends(GetPostsHandler)],
) -&gt; list[PostResponse]:
    return await handler.execute()</code></pre>
</div>
<p><span style="color: #ff0000;">※ハンドラーをルーター設定に依存注入して実行します。また@router部分ではOpenAPI用の定義も記述しています。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/router/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from .router import router

__all__ = ["router"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>main.pyの修正</h3>
<p>次にファイル「src/myapp/main.py」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware

from myapp.infrastructure.logger import get_logger, init_logging
from myapp.presentation.exception import (
    DatabaseError,
    db_error_exception_handler,
    general_exception_handler,
    request_validation_error_handler,
    valid_error_exception_handler,
)
from myapp.presentation.middleware import RequestMiddleware
from myapp.presentation.router import router


##########################
# アプリ全体の共通初期化処理
##########################
@asynccontextmanager
async def lifespan(app: FastAPI) -&gt; AsyncIterator[None]:
    ############
    # 起動時処理
    ############
    # ロガーの初期化
    init_logging()

    # サーバー起動ログ出力
    logger = get_logger()
    logger.info("start server !!!")
    # アプリ起動
    yield
    ############
    # 終了時処理
    ############
    # サーバー停止ログ出力
    logger.info("stop server !!!")


#############
# ルーター設定
#############
app = FastAPI(
    lifespan=lifespan,
    title="fastapi-domain API",
    description="FastAPIによるDDD構成のAPIです。",
    version="1.0.0",
    # terms_of_service="https://example.com/terms/",
    # contact={
    # "name": "サポート",
    # "url": "https://example.com/contact/",
    # "email": "support@example.com",
    # },
    # license_info={
    # "name": "Apache 2.0",
    # "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
    # },
)
app.include_router(router)

###################
# ミドルウェアの設定
###################

# 許可したいオリジン
origins = [
    "http://localhost:3000",
]

# CORS設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
)

# リクエスト用ミドルウェア
app.add_middleware(RequestMiddleware)


###################
# 例外ハンドラー設定
###################
app.add_exception_handler(RequestValidationError, request_validation_error_handler)
app.add_exception_handler(DatabaseError, db_error_exception_handler)
app.add_exception_handler(ValueError, valid_error_exception_handler)
app.add_exception_handler(Exception, general_exception_handler)</code></pre>
</div>
<p><span style="color: #ff0000;">※各種共通用設定も記述しています。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、フォーマット修正、静的コード解析、型チェックを行い、警告が出ないことを確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff format .
$ docker compose exec fastapi poetry run ruff check . --select I --fix
$ docker compose exec fastapi poetry run ruff check .
$ docker compose exec fastapi poetry run mypy .</code></pre>
</div>
<p>&nbsp;</p>
<h3>Dockerコンテナの再ビルドと起動</h3>
<p>次に以下のコマンドを実行し、Dockerコンテナを再ビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>PostドメインのAPIを試す</h2>
<p>次に上記で作成したPostドメインのAPIをPostmanを使って試します。</p>
<p>まずはPOSTメソッドで「http://localhost:9004/post」を実行し、下図のようにステータスコード201で想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6.jpg" alt="" width="2536" height="1576" class="aligncenter wp-image-20118 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-1024x636.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-768x477.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-1536x955.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-2048x1273.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次に<span style="color: #ff0000;"><strong>リクエストパラメータ「text」を21文字以上にして再度実行</strong></span>し、バリデーションチェックでエラーになればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7.jpg" alt="" width="2540" height="1572" class="aligncenter size-full wp-image-20119" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-768x475.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-1536x951.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-2048x1268.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:9004/posts」を実行し、下図のようにステータスコード200で想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8.jpg" alt="" width="2534" height="1568" class="aligncenter size-full wp-image-20120" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-768x475.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-1536x950.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-2048x1267.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<h2>テストコードを追加</h2>
<p>次にテストコードを追加して試しますが、まずは以下のコマンドを実行し、テストコードに必要なライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry add --dev pytest pytest-asyncio pytest-mock httpx</code></pre>
</div>
<p>&nbsp;</p>
<p>次にpytestの設定をするため、ファイル「pyproject.toml」の末尾に以下のような設定を追加します。</p>
<p>・「pyproject.toml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="pyproject.toml"><code>・・・

# =========================================================
# pytestの設定
# =========================================================
[tool.pytest.ini_options]
# パス設定
pythonpath = ["src"]
testpaths = ["src/tests"]

# マーカー設定
markers = [
    "unit: mark a test as a unit test",
    "integration: mark a test as an integration test"
]

</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを再ビルドして起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テストコード用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/tests/domain/post
$ touch src/tests/__init__.py src/tests/domain/__init__.py src/tests/domain/post/test_text.py src/tests/domain/post/test_entity.py src/tests/domain/post/__init__.py
$ mkdir -p src/tests/application/usecase/post
$ touch src/tests/application/__init__.py src/tests/application/usecase/__init__.py src/tests/application/usecase/post/test_create_post.py src/tests/application/usecase/post/test_get_posts.py src/tests/application/usecase/post/__init__.py
$ mkdir -p src/tests/presentation/handler/post
$ touch src/tests/presentation/__init__.py src/tests/presentation/handler/__init__.py src/tests/presentation/handler/post/test_post.py src/tests/presentation/handler/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/tests/domain/post/test_text.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/domain/post/test_text.py"><code>import dataclasses

import pytest

from myapp.domain.post import Text


@pytest.mark.unit
class TestText:
    def test_create_text_success(self) -&gt; None:
        text = Text("hello")
        assert text.value == "hello"
        assert str(text) == "hello"

    def test_empty_value_should_raise_error(self) -&gt; None:
        with pytest.raises(ValueError, match="テキストの値は必須です。"):
            Text("")

    def test_over_20_chars_should_raise_error(self) -&gt; None:
        over_text = "a" * 21
        with pytest.raises(
            ValueError, match="テキストの値は20文字以内で入力して下さい。"
        ):
            Text(over_text)

    def test_exactly_20_chars_is_ok(self) -&gt; None:
        t = Text("a" * 20)
        assert t.value == "a" * 20

    def test_frozen_object_cannot_be_modified(self) -&gt; None:
        t = Text("hello")
        with pytest.raises(dataclasses.FrozenInstanceError):
            t.value = "changed" # type: ignore[misc]

    def test_from_raw_should_skip_validation(self) -&gt; None:
        raw = "a" * 100
            t = Text.from_raw(raw)
        assert t.value == raw
        assert isinstance(t, Text)</code></pre>
</div>
<p><span style="color: #ff0000;">※テスト実行時にユニットテストのみを指定できるようにするため、「@pytest.mark.unit」を付けています。mypyの型チェックでのエラーをスキップするため、対象箇所に「# type: ignore[misc]」を付けています。</span></p>
<p>&nbsp;</p>
<p>・「src/tests/domain/post/test_entity.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/domain/post/test_entity.py"><code>import pytest

from myapp.domain.post import Post, Text


@pytest.mark.unit
class TestPost:
    def test_create_post_success(self) -&gt; None:
        text = Text("hello")
        post = Post(text=text, id=1)

        assert post.id == 1
        assert post.text == text
        assert isinstance(post.text, Text)

    def test_missing_text_should_raise_error(self) -&gt; None:
        with pytest.raises(ValueError, match="textは必須です。"):
            Post(text=None) # type: ignore[arg-type]

    def test_default_id_is_zero(self) -&gt; None:
        text = Text("hello")
        post = Post(text=text)

        assert post.id == 0

    def test_from_raw_should_skip_validation(self) -&gt; None:
        raw_text = Text.from_raw("raw-text")
        post = Post.from_raw(raw_id=123, raw_text=raw_text)

        assert isinstance(post, Post)
        assert post.id == 123
        assert post.text == raw_text
        assert post.text.value == "raw-text"</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/tests/application/usecase/post/test_create_post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/application/usecase/post/test_create_post.py"><code>import pytest
from pytest_mock import MockerFixture

from myapp.application.usecase.post import CreatePostUsecase
from myapp.domain.post import Post, Text
from myapp.presentation.schema.post import PostCreateRequest, PostResponse


@pytest.mark.unit
@pytest.mark.asyncio
class TestCreatePost:
    # 正常系テスト
    async def test_create_post_success(self, mocker: MockerFixture) -&gt; None:
        # ===================
        # モック作成
        # ===================
        mock_logger = mocker.Mock()
        mock_db = mocker.Mock()
        mock_post_repo = mocker.Mock()

        # リポジトリのモック化
        mock_post_return_value = Post(text=Text("Postデータ"))
        mock_post_return_value.id = 1
        mock_post_repo.create = mocker.AsyncMock(return_value=mock_post_return_value)

        # ユースケースのモック化
        usecase = CreatePostUsecase(
            logger=mock_logger, db=mock_db, post_repo=mock_post_repo
        )

        # ===================
        # リクエストデータ作成
        # ===================
        req = PostCreateRequest(text="Postデータ")

        # ===================
        # テスト実行
        # ===================
        res = await usecase.execute(req)

        # ===================
        # 検証
        # ===================
        assert isinstance(res, PostResponse)
        assert res.id == 1
        assert res.text == "Postデータ"

        # 対象のリポジトリが1回呼ばれること
        mock_post_repo.create.assert_awaited_once()</code></pre>
</div>
<p><span style="color: #ff0000;">※非同期関数の場合は「@pytest.mark.asyncio」を付けます。ユニットテストでは各種モック化してテストします。</span></p>
<p>&nbsp;</p>
<p>・「src/tests/application/usecase/post/test_get_posts.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/application/usecase/post/test_get_posts.py"><code>import pytest
from pytest_mock import MockerFixture

from myapp.application.usecase.post import GetPostsUsecase
from myapp.domain.post import Post, Text
from myapp.presentation.schema.post import PostResponse


@pytest.mark.unit
@pytest.mark.asyncio
class TestGetPosts:
    # 正常系テスト
    async def test_get_posts_success(self, mocker: MockerFixture) -&gt; None:
        # ===================
        # モック作成
        # ===================
        mock_logger = mocker.Mock()
        mock_db = mocker.Mock()
        mock_post_repo = mocker.Mock()

        # リポジトリのモック化
        mock_post_return_value = [
            Post.from_raw(raw_id=1, raw_text=Text.from_raw(raw_value="Postデータ１")),
            Post.from_raw(raw_id=2, raw_text=Text.from_raw(raw_value="Postデータ２")),
        ]
        mock_post_repo.find_all = mocker.AsyncMock(return_value=mock_post_return_value)

        # ユースケースのモック化
        usecase = GetPostsUsecase(
            logger=mock_logger, db=mock_db, post_repo=mock_post_repo
        )

        # ===================
        # テスト実行
        # ===================
        res = await usecase.execute()

        # ===================
        # 検証
        # ===================
        assert isinstance(res, list)
        assert all(isinstance(item, PostResponse) for item in res)
        assert res[0].id == 1
        assert res[0].text == "Postデータ１"
        assert res[1].id == 2
        assert res[1].text == "Postデータ２"

        # 対象のリポジトリが1回呼ばれること
        mock_post_repo.find_all.assert_awaited_once()</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/tests/presentation/handler/post/test_post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>import pytest
from fastapi.testclient import TestClient

from myapp.main import app

client = TestClient(app)


@pytest.mark.integration
class TestPostAPI:
    def test_create_post_success(self) -&gt; None:
        # テスト実行
        res = client.post(
            "/post",
            json={"text": "Postテキスト"},
        )

        # 検証
        assert res.status_code == 201
        assert res.json() == {"id": 0, "text": "Postテキスト"}

    def test_create_post_valid_error(self) -&gt; None:
        # テスト実行
        res = client.post(
            "/post",
            json={"text": "aaaaabbbbbcccccddddd1"},
        )

        # 検証
        assert res.status_code == 422
        data = res.json()
        assert "String should have at most 20 characters" in data["detail"][0]["msg"]
        assert data["detail"][0]["ctx"]["max_length"] == 20

    def test_get_posts_success(self) -&gt; None:
        # テスト実行
        res = client.get("/posts")

        # 検証
        assert res.status_code == 200
        assert res.json() == [
            {"id": 1, "text": "Postデータ１"},
            {"id": 2, "text": "Postデータ２"},
        ]</code></pre>
</div>
<p><span style="color: #ff0000;">※ハンドラーのテストはインテグレーションテストとして、リクエストを実行してテストします。（もし実際にDB操作がある場合はそれも実行されるため、テスト用のDB設定も必要になります。）</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ユニットテストを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run pytest -m unit </code></pre>
</div>
<p>&nbsp;</p>
<p>コマンドを実行後、以下のように全てのテストがPASSすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9.jpg" alt="" width="1530" height="454" class="aligncenter wp-image-20121 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9.jpg 1530w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9-300x89.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9-1024x304.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9-768x228.jpg 768w" sizes="auto, (max-width: 1530px) 100vw, 1530px" />
<p><span style="color: #ff0000;">※オプション「-m unit」でマーク「unit」が付いたものだけ実行しているため、それ以外のスキップしたテストが「3 deselected」として表示されますが、それは仕様なので気にしなくてOKです。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、インテグレーションテストを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run pytest -m integration</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンドを実行後、以下のように全てのテストがPASSすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10.jpg" alt="" width="3022" height="374" class="aligncenter wp-image-20122 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10.jpg 3022w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-300x37.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-1024x127.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-768x95.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-1536x190.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-2048x253.jpg 2048w" sizes="auto, (max-width: 3022px) 100vw, 3022px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2><span id="toc15">データベースついて</span></h2>
<p><strong><span style="color: #ff0000;">今回はDB部分は省略</span></strong>しましたが、組み込みたい場合は以前に書いた以下の記事などを参考にしつつ<span style="color: #ff0000;">（生成AIを使っていない時代に書いた記事で情報が古いので参考程度にどうぞ）</span>、<strong><span style="color: #ff0000;">必要な部分を追加修正</span></strong>していけばできると思います。</p>
<p>&nbsp;</p>
<p><strong>関連記事</strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/how-to-use-fastapi"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2024/05/fastapi-22-min-486x290.png" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">【FastAPI入門】Dockerで環境構築してPythonのAPIを開発する方法まとめ</p><span class="date gf">2024年5月4日</span></div></a></div>
<p>&nbsp;</p>
<h2>あるドメインのデータを別のドメインで使いたい場合について</h2>
<p>上記では基本的なDDD構成についてご紹介しましたが、<strong><span style="color: #ff0000;">実際にはあるドメインのデータを別のドメインのユースケースなどで使いたいというようなことも出てくる</span></strong>と思います。</p>
<p>その際はどう処理を書くべきか悩ましいところですが、トランザクション管理（データベースで「安全にまとめて作業をする仕組み」のこと）の観点からすると<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>1ハンドラー1ユースケースで作るのが最適</strong></span>なため、一つのユースケース内で各種ドメインを呼び出すような形で作って下さい。</p>
<p>ただし、将来的にマイクロサービス化したくなった場合、<strong><span style="color: #ff0000;">ドメイン単位で独立した設計になっていないユースケースの部分については分割するのが難しくなります</span></strong>が、その点はしょうがないのであきらめましょう！</p>
<p><strong><span style="color: #ff0000;">※ただし、ドメインの境界線はしっかり分けてDB設計、データ更新していくのが大事ではあるので、その点はモノリスになりすぎないように注意して下さい。</span></strong></p>
<p>&nbsp;</p>
<h2>本番環境用Dockerコンテナを作って試す</h2>
<p>次に本番環境へのデプロイを想定し、専用のDockerコンテナを作ってローカル環境で試してみますが、<strong><span style="color: #ff0000;">コンテナ一つで複数ワーカーを動かしたいかどうかで若干やり方が変わる</span></strong>ため、それぞれご紹介します。</p>
<p>まずは以下のコマンドを実行し、本番環境用のDockerfileを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p deploy/docker/prod &amp;&amp; touch deploy/docker/prod/Dockerfile</code></pre>
</div>
<p>&nbsp;</p>
<h3>k8sやCloud Runなどコンテナ単位でスケールする場合</h3>
<p>k8sやCloud Runなどコンテナ単位でスケールする場合、上記までのローカル環境と同様に「Uvicorn」のみで起動させればよいです。</p>
<p>その場合、ファイル「deploy/docker/prod/Dockerfile」は以下のように記述します。</p>
<p>・「deploy/docker/prod/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/prod/Dockerfile"><code>####################
# ビルドステージ
####################
FROM python:3.14.0-slim-trixie AS builder

# poetryをインストール
RUN pip3 install --no-cache-dir poetry

# プラグイン「poetry-plugin-export」を追加
RUN poetry self add poetry-plugin-export

WORKDIR /build

# 依存関係のみコピー
COPY pyproject.toml poetry.lock ./

# 本番環境用の依存関係のみをファイル「requirements.txt」に記述
RUN poetry export -f requirements.txt --without-hashes --only main -o requirements.txt

####################
# 実行ステージ
####################
FROM python:3.14.0-slim-trixie AS runner

# 環境変数設定
ENV ENV=production

# テスト用の環境変数設定（ローカルではコマンドで渡す必要があるため）
ARG TEST_VALUE
ENV TEST_VALUE=${TEST_VALUE}

# タイムゾーン設定
ENV TZ=Asia/Tokyo

WORKDIR /py

# 必要なパッケージをインストール
COPY --from=builder /build/requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

COPY ./src/myapp ./src/myapp

# 非rootユーザーを設定
RUN addgroup --system --gid 1001 appuser &amp;&amp; \
    adduser --system --uid 1001 appuser
USER appuser

EXPOSE 9004

# k8sやCloud Runなどコンテナ単位でスケールする場合
# Uvicornのみで起動させる
CMD ["uvicorn", "myapp.main:app", "--app-dir", "src", "--host", "0.0.0.0", "--port", "9004"]
</code></pre>
</div>
<p><span style="color: #ff0000;">※環境変数「TEST_VALUE」は必須にしているため、ビルドコマンド実行時に値を渡せるようにしています。</span></p>
<p>&nbsp;</p>
<h3>ECSなどでコンテナ一つで複数ワーカーを動かしたい場合</h3>
<p>ECSなどでコンテナ一つで複数ワーカーを動かしたい場合、サーバー起動には「Gunicorn + UvicornWorker」の組み合わせを利用します。</p>
<p>その場合、まずは以下のコマンドを実行し、poetryで「gunicorn」をインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry add gunicorn</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Gunicorn用の設定ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>touch gunicorn_conf.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「gunicorn_conf.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="gunicorn_conf.py"><code>import os

# サーバー設定
bind = "0.0.0.0:9004"
workers = int(os.getenv("WORKERS", "4"))
worker_class = "uvicorn.workers.UvicornWorker"

# ログ設定
loglevel = "info"
accesslog = "-" # 標準出力に出す
errorlog = "-" # 標準エラー出力に出す</code></pre>
</div>
<p>&nbsp;</p>
<p>次に事前に作成したファイル「deploy/docker/prod/Dockerfile」は以下のように記述します。</p>
<p>・「deploy/docker/prod/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/prod/Dockerfile"><code>####################
# ビルドステージ
####################
FROM python:3.14.0-slim-trixie AS builder

# poetryをインストール
RUN pip3 install --no-cache-dir poetry

# プラグイン「poetry-plugin-export」を追加
RUN poetry self add poetry-plugin-export

WORKDIR /build

# 依存関係のみコピー
COPY pyproject.toml poetry.lock ./

# 本番環境用の依存関係のみをファイル「requirements.txt」に記述
RUN poetry export -f requirements.txt --without-hashes --only main -o requirements.txt

####################
# 実行ステージ
####################
FROM python:3.14.0-slim-trixie AS runner

# 環境変数設定
ENV ENV=production

# テスト用の環境変数設定（ローカルではコマンドで渡す必要があるため）
ARG TEST_VALUE
ENV TEST_VALUE=${TEST_VALUE}

# タイムゾーン設定
ENV TZ=Asia/Tokyo

WORKDIR /py

# 必要なパッケージをインストール
COPY --from=builder /build/requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

COPY ./src/myapp ./src/myapp

# 非rootユーザーを設定
RUN addgroup --system --gid 1001 appuser &amp;&amp; \
    adduser --system --uid 1001 appuser
USER appuser

EXPOSE 9004

# ECSなどでコンテナ一つで複数ワーカーを動かしたい場合
# （Gunicorn + UvicornWorker）の組み合わせ
COPY gunicorn_conf.py .
WORKDIR /py/src
CMD ["gunicorn", "-c", "/py/gunicorn_conf.py", "myapp.main:app"]
</code></pre>
</div>
<p><span style="color: #ff0000;">※環境変数「TEST_VALUE」は必須にしているため、ビルドコマンド実行時に値を渡せるようにしています。</span></p>
<p>&nbsp;</p>
<h3>Dockerコンテナ単体でのビルドと起動</h3>
<p>次に以下のコマンドを実行し、Dockerコンテナのビルドおよび起動をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker build --no-cache --build-arg TEST_VALUE='PROD-VALUE' -f ./deploy/docker/prod/Dockerfile -t fastapi-domain-api:latest .
$ docker run -d -p 80:9004 fastapi-domain-api:latest</code></pre>
</div>
<p><span style="color: #ff0000;">※ビルド時に「&#8211;build-arg TEST_VALUE=&#8217;PROD-VALUE&#8217;」で環境変数への値を渡しています。今回はテストなのでタグは「latest」ですが、実際にはバージョンのタグを指定するのでご注意下さい。</span></p>
<p>&nbsp;</p>
<h3>PostmanでAPIを試す</h3>
<p>次にエンドポイントを「http://localhost/」とし、上記と同様にAPIをPostmanで実行して試して下さい。</p>
<p>各種APIを実行し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12.jpg" alt="" width="2538" height="1572" class="aligncenter size-full wp-image-20133" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12.jpg 2538w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-768x476.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-1536x951.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-2048x1269.jpg 2048w" sizes="auto, (max-width: 2538px) 100vw, 2538px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はPythonのFastAPIでDDD構成のバックエンドAPIを開発する方法について解説しました。</p>
<p><strong><span style="color: #ff0000;">これから数年間は間違いなくAI関連機能の開発が盛り上がるのは避けられない</span></strong>ため、その際は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>主にPythonを使ったAPI開発が増える</strong></span>のではと思います。</p>
<p>その際は<span style="color: #3366ff;"><strong>軽量でバランスがいいFastAPIでのAPI開発が増えそう</strong></span>ですが、<strong><span style="color: #ff0000;">実務ではドメイン駆動設計で開発することが多い</span></strong>と思うので、FastAPIでDDD構成のバックエンドAPIを開発したい方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python">PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Rust専門の技術ブログ「Rust-Tech」を開設しました！</title>
		<link>https://tomoyuki65.com/notice-of-establishment-of-rust-tech</link>
					<comments>https://tomoyuki65.com/notice-of-establishment-of-rust-tech#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Fri, 30 May 2025 06:50:56 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20009</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 特定の技術に特化した専門技術ブログとして「Golang-Tech」を別途運営していますが、また新しくRust専門技術ブログとして「Rust-Te...</p>
The post <a href="https://tomoyuki65.com/notice-of-establishment-of-rust-tech">Rust専門の技術ブログ「Rust-Tech」を開設しました！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/05/rust250530-1-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20013" srcset="https://tomoyuki65.com/wp-content/uploads/2025/05/rust250530-1-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/05/rust250530-1-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>特定の技術に特化した専門技術ブログとして「<a href="https://golang.tomoyuki65.com" target="_blank" rel="noopener">Golang-Tech</a>」を別途運営していますが、また新しく<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Rust専門技術ブログ</strong></span>として「<a href="https://rust.tomoyuki65.com" target="_blank" rel="noopener">Rust-Tech</a>」を解説しました！</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Rust専門の技術ブログ「Rust-Tech」を開設しました！</h2>
<p>Rustについては<span style="color: #ff0000;"><strong>まだまだ普及はしていません</strong></span>が、私の予想ではおそらく<strong><span style="color: #ff0000;">あと3〜5年後ぐらいには今のGolangぐらい普及している可能性があるなと睨んでいる</span></strong>ため、Golangをメインで使いつつも、サブにRustを使っていこうと思ってます。</p>
<p>そんな<strong><span style="color: #3366ff;">Rustはメモリ安全性やパフォーマンス性が高いのが特徴</span></strong>で、大量データを処理したり、高トラフィックが想定されるような<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>パフォーマンスを最重視したい場面においては、GolangよりもRustが採用されるケースが増えてくる</strong></span>と思います。</p>
<p>逆に<strong><span style="color: #ff0000;">弱点としては学習コストが高く、まだ情報も少ないこと</span></strong>で、私も実際に試しましたが、ある程度理解するまでに非常に苦労しました。Rustの能力を引き出すには、あと2年ぐらいは使い込まないと厳しそうです。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>ということで、私はこれからRustも使っていこうと思ってますが、キャッチアップしたことはRust-Techの方にまとめていきます。</p>
<p>もしRustに興味がある方がいたら、ぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<div class="supplement boader">
<p style="text-align: center;"><span style="color: #808080;">＼ Rust専門の技術ブログはこちら ／</span></p>
<div class="btn-wrap aligncenter rich_orange"><img loading="lazy" decoding="async" src="//ad.jp.ap.valuecommerce.com/servlet/gifbanner?sid=3371598&amp;pid=886856626" width="1" height="1" border="0" /><a href="https://rust.tomoyuki65.com">&gt;&gt; Rust-Tech</a></div>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/notice-of-establishment-of-rust-tech">Rust専門の技術ブログ「Rust-Tech」を開設しました！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/notice-of-establishment-of-rust-tech/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>RustのaxumでバックエンドAPI開発を試す！</title>
		<link>https://tomoyuki65.com/trying-out-the-backend-api-with-axum-in-rust</link>
					<comments>https://tomoyuki65.com/trying-out-the-backend-api-with-axum-in-rust#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Sat, 26 Apr 2025 15:03:17 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19953</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 まだまだ普及はしていませんが、一部の間で愛されているプログラミング言語に「Rust」があります。 そんなRustはメモリ安全性を重視したプログラ...</p>
The post <a href="https://tomoyuki65.com/trying-out-the-backend-api-with-axum-in-rust">RustのaxumでバックエンドAPI開発を試す！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-6-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-19970" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-6-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-6-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>まだまだ普及はしていませんが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>一部の間で愛されているプログラミング言語に「Rust」</strong></span>があります。</p>
<p>そんな<strong></strong>Rustは<span style="border-bottom: 2px solid #be3144;"><strong>メモリ安全性を重視したプログラミング言語</strong></span>であり、<strong><span style="color: #3366ff;">CやC++言語並のパフォーマンスを発揮しながら、</span></strong><span _ngcontent-ng-c2459883256="" class="ng-star-inserted"><strong><span style="color: #3366ff;">ガベージコレクション（不要になったメモリ領域を自動的に解放する仕組み）なしでメモリ管理の安全性を実現（メモリ関連のバグを未然に防ぎやすい）</span></strong>しているのが特徴です。</span></p>
<p>どちらかというとC言語に近いため、ツール開発等に向いているような言語ですが、Web系のバックエンドAPI開発も普通に可能なため、<strong><span style="color: #ff0000;">将来的に普及していく可能性は高い</span></strong>と思われます。</p>
<p>ということで、そんなRustを使ってバックエンドAPI開発について試してみましたので、この記事では得られた知見をまとめます。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>RustのaxumでバックエンドAPI開発を試す！</h2>
<p>今回はaxumというフレームワークを用いてバックエンドAPI開発を試してみますが、<span style="color: #ff0000;"><strong>開発環境の構築にはDockerを使います</strong></span>ので、もしまだ使えないという方は<strong></strong>Docker Desktopなどをインストールして事前に使えるようにして下さい。</p>
<p>ではまず以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir rust-sample &amp;&amp; cd rust-sample
$ mkdir -p docker/local/rust &amp;&amp; cd docker/local/rust
$ touch Dockerfile &amp;&amp; cd ../../..
$ touch .env compose.yml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成した各種ファイルをそれぞれ以下のように記述します。</p>
<p>・「docker/local/rust/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="docker/local/rust/Dockerfile"><code>FROM rust:1.86

WORKDIR /app

COPY . .

# ホットリロード用のライブラリをインストール
RUN cargo install cargo-watch

# Rust用のリンターをインストール
RUN rustup component add clippy</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".env"><code>ENV=local
PORT=8080</code></pre>
</div>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  api:
    container_name: rust-api
    build:
      context: .
      dockerfile: ./docker/local/rust/Dockerfile
    command: cargo watch -x run
    volumes:
      - .:/app
    ports:
      - "8080:8080"
    env_file:
      - ./.env
    tty: true
    stdin_open: true</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナをビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Rustのプロジェクト作成用の初期化を行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm api cargo init --name rust_api</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、下図のように「src/main.rs」、「.gitignore」、「Cargo.toml」が作成されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-1.png" alt="" width="760" height="374" class="aligncenter wp-image-19957" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-1.png 1298w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-1-300x147.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-1-1024x503.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-1-768x377.png 768w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、「src/main.rs」をコンパイルして実行してみます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm api cargo run</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、下図のように文字列「Hello, world!」が出力されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-2.png" alt="" width="966" height="152" class="aligncenter size-full wp-image-19958" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-2.png 966w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-2-300x47.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-2-768x121.png 768w" sizes="auto, (max-width: 966px) 100vw, 966px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>axumというフレームワークでAPIを作る</h2>
<p>次に以下のコマンドを実行し、APIで必要になる各種クレートを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm api cargo add axum
$ docker compose run --rm api cargo add tokio --features full
$ docker compose run --rm api cargo add serde --features derive
$ docker compose run --rm api cargo add envy
$ docker compose run --rm api cargo add thiserror
$ docker compose run --rm api cargo add serde_json</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、API用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/api &amp;&amp; cd src/api
$ touch mod.rs router.rs

$ mkdir configs &amp;&amp; cd configs
$ touch mod.rs config.rs &amp;&amp; cd ..

$ mkdir errors &amp;&amp; cd errors
$ touch mod.rs error.rs &amp;&amp; cd ..

$ mkdir repositories &amp;&amp; cd repositories &amp;&amp; touch mod.rs
$ mkdir sample &amp;&amp; cd sample
$ touch mod.rs sample_repository.rs &amp;&amp; cd ../..

$ mkdir services &amp;&amp; cd services &amp;&amp; touch mod.rs
$ mkdir sample &amp;&amp; cd sample
$ touch mod.rs sample_service.rs &amp;&amp; cd ../..

$ mkdir handlers &amp;&amp; cd handlers &amp;&amp; touch mod.rs
$ mkdir sample &amp;&amp; cd sample
$ touch mod.rs sample_handler.rs &amp;&amp; cd ../..

$ mkdir usecases &amp;&amp; cd usecases &amp;&amp; touch mod.rs
$ mkdir sample &amp;&amp; cd sample
$ touch mod.rs sample_usecase.rs &amp;&amp; cd ../../../..</code></pre>
</div>
<p><strong><span style="color: #ff0000;">※クリーンアーキテクチャを参考に、ハンドラー層、ユースケース層、サービス層、リポジトリー層に分ける形でファイルを分割しています。</span></strong></p>
<p>&nbsp;</p>
<p>次に作成した各種ファイルをそれぞれ以下のように記述します。</p>
<p>・「src/api/mod.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/mod.rs"><code>pub mod configs;
pub mod errors;
pub mod handlers;
pub mod repositories;
pub mod router;
pub mod services;
pub mod usecases;</code></pre>
</div>
</div>
<p><strong><span style="color: #ff0000;">※Rustではディレクトリ内にmod.rsを作成し、それによってモジュールの公開設定を行います。対象のモジュールを公開すると、別のモジュールファイルからアクセスできるようになります。尚、対象のモジュールの特定の処理だけ公開するとかも可能です。</span></strong></p>
<p>&nbsp;</p>
<p>・「src/api/router.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/router.rs"><code>// axum
use axum::{
    Router,
    routing::{get, post},
};

// ハンドラー用のモジュール
use super::handlers::sample::sample_handler;

pub fn router() -&gt; Router {
    // APIのグループ「v1」
    let v1 = Router::new()
        .route("/sample/get", get(sample_handler::sample_get))
        .route(
            "/sample/get/{id}",
            get(sample_handler::sample_get_path_query),
        )
        .route("/sample/post", post(sample_handler::sample_post));

    // ルーティング
    Router::new()
        .nest("/api/v1", v1)
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/configs/mod.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/configs/mod.rs"><code>pub mod config;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/configs/config.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/configs/config.rs"><code>use envy;
use serde::{Deserialize};

// 環境変数のデフォルト値を返す関数
fn default_env() -&gt; String {
    "local".to_string()
}

fn default_port() -&gt; u16 {
    8080
}

// 環境変数の構造体
#[derive(Deserialize, Debug)]
pub struct Config {
    #[serde(default = "default_env")]
    pub env: String,
    #[serde(default = "default_port")]
    pub port: u16,
}

// 環境変数を返す関数
pub fn get_config() -&gt; Config {
    match envy::from_env::&lt;Config&gt;() {
        Ok(config) =&gt; config,
        Err(err) =&gt; {
            println!("環境変数の初期化エラー: {}", err);

            // 環境変数にデフォルト値を設定して返す
            Config {
                env: default_env(),
                port: default_port(),
            }
        }
    }
}</code></pre>
</div>
</div>
<p><strong><span style="color: #ff0000;">※コンフィグファイルで環境変数を取得できるようにしています。「</span></strong><strong><span style="color: #ff0000;">envy::from_env::&lt;Config&gt;()」戻り値は「Result&lt;T, E&gt;」型になっているため、matchで「Ok」または「Err」で条件判定させています。</span></strong></p>
<p>&nbsp;</p>
<p>・「src/api/errors/mod.rs」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/errors/mod.rs"><code>pub mod error;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/api/errors/error.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/errors/error.rs"><code>use thiserror::Error;

#[derive(Error, Debug)]
pub enum CommonError {
    #[error("Internal Server Error")]
    InternalServerError,
}</code></pre>
</div>
</div>
<p><strong><span style="color: #ff0000;">※エラーファイルで、共通のエラー型を定義できるようにしています。</span></strong></p>
<p>&nbsp;</p>
<p>・「src/api/repositories/mod.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/repositories/mod.rs"><code>pub mod sample;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/repositories/sample/mod.rs」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/repositories/sample/mod.rs"><code>pub mod sample_repository;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/api/repositories/sample/sample_repository.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/repositories/sample/sample_repository.rs"><code>// 共通エラー用モジュール
use crate::api::errors::error::CommonError;

// 文字列「Sample Hello !!」を返す関数
pub async fn sample_hello() -&gt; Result&lt;String, CommonError&gt; {
    let text = "Sample Hello !!".to_string();

    if text.is_empty() {
        return Err(CommonError::InternalServerError);
    }

    Ok(text)
}</code></pre>
</div>
</div>
</div>
<p><strong><span style="color: #ff0000;">※リポジトリーファイルではDB操作や外部APIの実行などを記述する想定ですが、今回の例では文字列を返すだけの簡単な関数の処理にしています。</span></strong></p>
<p>&nbsp;</p>
<p>・「src/api/services/mod.rs」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/services/mod.rs"><code>pub mod sample;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/api/services/sample/mod.rs」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/services/sample/mod.rs"><code>pub mod sample_service;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/api/services/sample/sample_service.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/services/sample/sample_service.rs"><code>// 共通エラー用モジュール
use crate::api::errors::error::CommonError;

// リポジトリ用のモジュール
use crate::api::repositories::sample::sample_repository;

// サンプルテキストを取得するサービス
pub async fn sample_get_text_hello() -&gt; Result&lt;String, CommonError&gt; {
    let text = match sample_repository::sample_hello().await {
        Ok(text) =&gt; text,
        Err(err) =&gt; return Err(err),
    };

    Ok(text)
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/handlers/mod.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/handlers/mod.rs"><code>pub mod sample;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/handlers/sample/mod.rs」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/handlers/sample/mod.rs"><code>pub mod sample_handler;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/api/handlers/sample/sample_handler.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/handlers/sample/sample_handler.rs"><code>// axum
use axum::{
    extract::{Path, Query},
    response::{Json, Response},
};

// 変換用のクレート
use serde::Deserialize;

// サービス用のモジュール
use crate::api::usecases::sample::sample_usecase;

// クエリパラメータ用の構造体
#[derive(Deserialize, Debug)]
pub struct QueryParams {
    pub item: Option&lt;String&gt;,
}

// リクエストボディの構造体
#[derive(Deserialize, Debug)]
pub struct RequestBody {
    pub name: String,
}

// GETメソッド用のAPIサンプル
pub async fn sample_get() -&gt; Response {
    sample_usecase::sample_get_usecase().await
}

// GETメソッドかつパスパラメータとクエリパラメータ有りのAPIサンプル
pub async fn sample_get_path_query(
    Path(id): Path&lt;String&gt;,
    Query(params): Query&lt;QueryParams&gt;,
) -&gt; Response {
    sample_usecase::sample_get_path_query_usecase(id, params).await
}

// POSTメソッド用のAPIサンプル
pub async fn sample_post(Json(body): Json&lt;RequestBody&gt;) -&gt; Response {
    sample_usecase::sample_post_usecase(body).await
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/usecases/mod.rs」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/usecases/mod.rs"><code>pub mod sample;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/api/usecases/sample/mod.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/usecases/sample/mod.rs"><code>pub mod sample_usecase;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/usecases/sample/sample_usecase.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/usecases/sample/sample_usecase.rs"><code>// axum
use axum::{
    http::{StatusCode},
    response::{IntoResponse, Json, Response},
};

// json変換用マクロ
use serde_json::json;

// サービス用のモジュール
use crate::api::services::sample::sample_service;

// クエリパラメータ用の構造体
use crate::api::handlers::sample::sample_handler::QueryParams;

// リクエストボディ用の構造体
use crate::api::handlers::sample::sample_handler::RequestBody;

// GETメソッド用APIのサンプルユースケース
pub async fn sample_get_usecase() -&gt; Response {
    // サンプルテキストを取得するサービスを実行
    let text = match sample_service::sample_get_text_hello().await {
        Ok(text) =&gt; text,
        Err(err) =&gt; {
            // json形式のメッセージを設定
            let msg = Json(json!({ "message": err.to_string()}));

            // レスポンス結果の設定
            let res = (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response();

            // 戻り値としてレスポンス結果を返す
            return res;
        }
    };

    // json形式のメッセージを設定
    let msg = Json(json!({ "message": text}));

    // レスポンス結果を設定して戻り値として返す
    (StatusCode::OK, msg).into_response()
}

// GETメソッドかつパスパラメータとクエリパラメータ有りのサンプルユースケース
pub async fn sample_get_path_query_usecase(id: String, params: QueryParams) -&gt; Response {
    let text = format!(
        "id: {}, item: {}",
        id,
        params.item.unwrap_or("".to_string())
    );

    // json形式のメッセージを設定
    let msg = Json(json!({ "message": text}));

    // レスポンス結果を設定して戻り値として返す
    (StatusCode::OK, msg).into_response()
}

// POSTメソッドのサンプルユースケース
pub async fn sample_post_usecase(body: RequestBody) -&gt; Response {
    let text = format!("name: {}", body.name);

    // json形式のメッセージを設定
    let msg = Json(json!({ "message": text}));

    // レスポンス結果を設定して戻り値として返す
    (StatusCode::OK, msg).into_response()
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にファイル「src/main.rs」を以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git"><code>// axum
use axum::{
    Router,
    serve,
};

// apiモジュール
mod api;

// routerモジュール
use api::router::router;

// configsモジュール
use api::configs::config;

#[tokio::main]
async fn main() {
    // 環境変数取得
    let config = config::get_config();

    // サーバー起動のログ出力
    println!("Start rust_api (ENV:{}) !!", config.env);

    // サーバー起動
    let app = Router::new().merge(router());
    let addr = format!("0.0.0.0:{}", config.port);
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    serve(listener, app).await.unwrap();
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したAPIを試しますが、<strong><span style="color: #ff0000;">APIの実行にはPostmanを使って試す</span></strong>ため、もしまだ使ってないという方は事前に使えるようにして下さい。</p>
<p>ではまずGETメソッドのAPI「http://localhost:8080/api/v1/sample/get」を実行し、下図のように想定通りに正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3.png" alt="" width="2536" height="1666" class="aligncenter size-full wp-image-19963" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3.png 2536w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-300x197.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-1024x673.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-768x505.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-1536x1009.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-2048x1345.png 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次にGETメソッドかつパスパラメータとクエリパラメータ有りのAPI「http://localhost:8080/api/v1/sample/get/11?item=book」を実行し、下図のように想定通りに正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4.jpg" alt="" width="2540" height="1666" class="aligncenter wp-image-19964 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4-1024x672.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4-768x504.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4-1536x1007.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-4-2048x1343.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にPOSTメソッドのAPI「http://localhost:8080/api/v1/sample/post」を実行し、下図のように想定通りに正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5.jpg" alt="" width="2538" height="1672" class="aligncenter size-full wp-image-19965" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5.jpg 2538w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5-300x198.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5-1024x675.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5-768x506.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5-1536x1012.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-5-2048x1349.jpg 2048w" sizes="auto, (max-width: 2538px) 100vw, 2538px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>テストコードを考慮してリポジトリー層をモック化しやすいように修正</h2>
<p>上記ではクリーンアーキテクチャを参考にハンドラー層、ユースケース層、サービス層、リポジトリー層にコードを分けてAPIを作りましたが、さらに<strong><span style="color: #ff0000;">テストコードを書くことを考慮</span></strong>すると、<strong><span style="color: #ff0000;">特に</span><span style="color: #ff0000;">リポジトリー層についてはモック化しやすいようにしておく必要</span></strong>があります。</p>
<p>そこでRustのstruct（構造体の定義）、impl（メソッドの定義）、trait（インターフェースの定義）を利用し、<span style="color: #ff0000;"><strong>リポジトリー層をサービス層へ依存注入（他のオブジェクトを受け取って利用）できるように修正</strong></span>していきます。</p>
<p>まずは以下のコマンドを実行し、traitでasync fn（非同期関数）を扱いやすくする「async_trait」クレートを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api cargo add async_trait</code></pre>
</div>
<p>&nbsp;</p>
<p>次にユースケース層のファイルは1ハンドラー1ユースケースになるようファイル分割して作るため、以下のコマンドを実行してファイル「src/api/usecases/sample/sample_usecase.rs」の削除および、新しいファイルを3つ作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ rm -f src/api/usecases/sample/sample_usecase.rs
$ touch src/api/usecases/sample/sample_get_usecase.rs.rs
$ touch src/api/usecases/sample/sample_get_path_query_usecase.rs.rs
$ touch src/api/usecases/sample/sample_post_usecase.rs.rs</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下の各種ファイルについて、それぞれ修正等を行います。</p>
<p>・「src/api/repositories/sample/sample_repository.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/repositories/sample/sample_repository.rs"><code>// 共通エラー用モジュール
use crate::api::errors::error::CommonError;

// サンプルリポジトリーの構造体
pub struct SampleRepository;

impl SampleRepository {
    // 初期化用メソッド
    pub fn new() -&gt; Self {
        SampleRepository
    }
}

// サンプルリポジトリー用のトレイト（モック化もできるように定義）
#[async_trait::async_trait]
pub trait SampleRepositoryTrait {
    async fn sample_hello(&amp;self) -&gt; Result&lt;String, CommonError&gt;;
}

#[async_trait::async_trait]
impl SampleRepositoryTrait for SampleRepository {
    // 文字列「Sample Hello !!」を返す関数
    async fn sample_hello(&amp;self) -&gt; Result&lt;String, CommonError&gt; {
        let text = "Sample Hello !!".to_string();

        if text.is_empty() {
            return Err(CommonError::InternalServerError);
        }

        Ok(text)
    }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/services/sample/sample_service.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/services/sample/sample_service.rs"><code>// 共通エラー用モジュール
use crate::api::errors::error::CommonError;

// リポジトリ用のモジュール
use crate::api::repositories::sample::sample_repository::SampleRepositoryTrait;

// 使用するリポジトリーをまとめる構造体
pub struct SampleCommonRepository {
    // Box&lt;T&gt;型で動的にメモリ領域確保
    // Send: オブジェクトが異なるスレッド間で安全に送信できることを保証
    // Sync: オブジェクトが複数のスレッドから同時にアクセスできることを保証
    // 'static: オブジェクトのライフタイムがプログラムが終了するまで破棄されない
    pub sample_repo: Box&lt;dyn SampleRepositoryTrait + Send + Sync + 'static&gt;,
}

// サンプルサービス
pub struct SampleService {
    repo: SampleCommonRepository,
}

impl SampleService {
    pub fn new(repo: SampleCommonRepository) -&gt; Self {
        SampleService { repo }
    }
}

// サンプルサービス用のトレイト（モック化もできるように定義）
#[async_trait::async_trait]
pub trait SampleServiceTrait {
    async fn sample_get_text_hello(&amp;self) -&gt; Result&lt;String, CommonError&gt;;
}

#[async_trait::async_trait]
impl SampleServiceTrait for SampleService {
    async fn sample_get_text_hello(&amp;self) -&gt; Result&lt;String, CommonError&gt; {
        let text = match self.repo.sample_repo.sample_hello().await {
            Ok(text) =&gt; text,
            Err(err) =&gt; {
                return Err(err);
            }
        };

        Ok(text)
    }
}</code></pre>
</div>
</div>
</div>
<p><strong><span style="color: #ff0000;">※トレイトを依存注入できるようにするためにはBox&lt;T&gt;型を使う必要がありました。</span></strong></p>
<p>&nbsp;</p>
<p>・「src/api/usecases/sample/mod.rs」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/usecases/sample/mod.rs"><code>pub mod sample_get_path_query_usecase;
pub mod sample_get_usecase;
pub mod sample_post_usecase;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/usecases/sample/sample_get_usecase.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/usecases/sample/sample_get_usecase.rs"><code>// axum
use axum::{
    http::StatusCode,
    response::{IntoResponse, Json, Response},
};

// json変換用マクロ
use serde_json::json;

// サービスのモジュール
use crate::api::services::sample::sample_service::{SampleService, SampleServiceTrait};

// 使用するサービスをまとめる構造体
pub struct SampleCommonService {
    pub sample_service: SampleService,
}

// 実行するユースケースの構造体
pub struct SampleGetUsecase {
    pub service: SampleCommonService,
}

impl SampleGetUsecase {
    pub async fn exec(&amp;self) -&gt; Response {
        // サンプルテキストを取得するサービスを実行
        let text = match self.service.sample_service.sample_get_text_hello().await {
            Ok(text) =&gt; text,
            Err(err) =&gt; {
                // json形式のメッセージを設定
                let msg = Json(json!({ "message": err.to_string()}));

                // レスポンス結果の設定
                let res = (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response();

                // 戻り値としてレスポンス結果を返す
                return res;
            }
        };

        // json形式のメッセージを設定
        let msg = Json(json!({ "message": text}));

        // レスポンス結果を設定して戻り値として返す
        (StatusCode::OK, msg).into_response()
    }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/usecases/sample/sample_get_path_query_usecase.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/usecases/sample/sample_get_path_query_usecase.rs"><code>// axum
use axum::{
    http::StatusCode,
    response::{IntoResponse, Json, Response},
};

// json変換用マクロ
use serde_json::json;

// クエリパラメータ用の構造体
use crate::api::handlers::sample::sample_handler::QueryParams;

// 実行するユースケースの構造体
pub struct SampleGetPathQueryUsecase;

impl SampleGetPathQueryUsecase {
    pub async fn exec(&amp;self, id: String, params: QueryParams) -&gt; Response {
        // テキスト設定
        let text = format!(
            "id: {}, item: {}",
            id,
            params.item.unwrap_or("".to_string())
        );

        // json形式のメッセージを設定
        let msg = Json(json!({ "message": text}));

        // レスポンス結果を設定して戻り値として返す
        (StatusCode::OK, msg).into_response()
    }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/usecases/sample/sample_post_usecase.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git"><code>// axum
use axum::{
    http::StatusCode,
    response::{IntoResponse, Json, Response},
};

// json変換用マクロ
use serde_json::json;

// リクエストボディ用の構造体
use crate::api::handlers::sample::sample_handler::RequestBody;

// 実行するユースケースの構造体
pub struct SamplePostUsecase;

impl SamplePostUsecase {
    pub async fn exec(&amp;self, body: RequestBody) -&gt; Response {
        // テキスト設定
        let text = format!("name: {}", body.name);

        // json形式のメッセージを設定
        let msg = Json(json!({ "message": text}));

        // レスポンス結果を設定して戻り値として返す
        (StatusCode::OK, msg).into_response()
    }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>・「src/api/handlers/sample/sample_handler.rs」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/api/handlers/sample/sample_handler.rs"><code>// axum
use axum::{
    extract::{Path, Query},
    response::{Json, Response},
};

// 変換用のクレート
use serde::Deserialize;

// リポジトリーのモジュール
use crate::api::repositories::sample::sample_repository::SampleRepository;

// サービスのモジュール
use crate::api::services::sample::sample_service::{SampleCommonRepository, SampleService};

// ユースケースのモジュール
use crate::api::usecases::sample::sample_get_path_query_usecase::SampleGetPathQueryUsecase;
use crate::api::usecases::sample::sample_get_usecase::{SampleCommonService, SampleGetUsecase};
use crate::api::usecases::sample::sample_post_usecase::SamplePostUsecase;

// クエリパラメータ用の構造体
#[derive(Deserialize, Debug)]
pub struct QueryParams {
    pub item: Option&lt;String&gt;,
}

// リクエストボディの構造体
#[derive(Deserialize, Debug)]
pub struct RequestBody {
    pub name: String,
}

// GETメソッド用のAPIサンプル
pub async fn sample_get() -&gt; Response {
    // サービスのインスタンス化
    let sample_repo = Box::new(SampleRepository::new());
    let sample_common_repo = SampleCommonRepository { sample_repo };
    let sample_service = SampleService::new(sample_common_repo);
    let sample_common_service = SampleCommonService { sample_service };

    // ユースケースを実行
    let sample_get_usecase = SampleGetUsecase {
        service: sample_common_service,
    };
    sample_get_usecase.exec().await
}

// GETメソッドかつパスパラメータとクエリパラメータ有りのAPIサンプル
pub async fn sample_get_path_query(
    Path(id): Path&lt;String&gt;,
    Query(params): Query&lt;QueryParams&gt;,
) -&gt; Response {
    // ユースケースを実行
    let sample_get_path_query_usecase = SampleGetPathQueryUsecase;
    sample_get_path_query_usecase.exec(id, params).await
}

// POSTメソッド用のAPIサンプル
pub async fn sample_post(Json(body): Json&lt;RequestBody&gt;) -&gt; Response {
    // ユースケースを実行
    let sample_post_usecase = SamplePostUsecase;
    sample_post_usecase.exec(body).await
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に3つのAPIをそれぞれ再度実行してみて下さい。修正前と同様にそれぞれ正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3.png" alt="" width="2536" height="1666" class="aligncenter size-full wp-image-19963" srcset="https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3.png 2536w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-300x197.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-1024x673.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-768x505.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-1536x1009.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/04/rust-3-2048x1345.png 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>尚、<strong><span style="color: #ff0000;">この記事ではテストコードについては割愛</span></strong>しますが、<span style="color: #ff0000;"><strong>「mockall」クレートを追加後に対象のリポジトリーに「#[mockall::automock]」を追加</strong></span>することでモック用の構造体も自動作成され、それを使うと<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>テストコードで対象のリポジトリーのモック化が可能</strong></span>になります。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はRustのaxum（フレームワーク）でバックエンドAPI開発を試した知見をまとめました。</p>
<p>Rustに関しては<strong><span style="color: #ff0000;">Go言語以上に情報が少なかったり、言語自体の難易度も高め</span></strong>なので、今回ご紹介した部分の基本的なことでさえ<strong><span style="color: #ff0000;">キャッチアップするのが大変</span></strong>でした。</p>
<p>ただ<span style="color: #3366ff;"><strong>基本的なところはある程度ご紹介できたと思う</strong></span>ので、これからRustでバックエンドAPIを開発しようと検討している方は、ぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>各種SNSなど</strong></p>
<p>各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします！</p>
<ul>
<li> <a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">X（旧Twitter）</a></li>
<li> <a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">YouTube</a></li>
</ul>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/trying-out-the-backend-api-with-axum-in-rust">RustのaxumでバックエンドAPI開発を試す！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/trying-out-the-backend-api-with-axum-in-rust/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Nest.jsでGraphQLなどのBFF（Backend for Frontend）を開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-use-nestjs</link>
					<comments>https://tomoyuki65.com/how-to-use-nestjs#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Sat, 22 Feb 2025 21:27:05 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19784</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 中規模以上のWebサービスになってくるとフロントエンドとバックエンド通信の最適化のため、BFF（Backend for Frontend）と呼ば...</p>
The post <a href="https://tomoyuki65.com/how-to-use-nestjs">Nest.jsでGraphQLなどのBFF（Backend for Frontend）を開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-16-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-19831" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-16-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-16-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>中規模以上のWebサービスになってくると<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>フロントエンドとバックエンド通信の最適化</strong></span>のため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>BFF（Backend for Frontend）</strong></span>と呼ばれる中間層のアプリケーションを置くことがあります。</p>
<p>具体的には<strong><span style="color: #ff0000;">サービスの機能拡張に比例してバックエンドAPIの数が増えたりする</span></strong>と、各種フロントエンド側へ渡す<strong><span style="color: #ff0000;">データのカスタマイズ等が複雑になってしまう</span></strong>ため、<strong></strong>それらを緩和してフロントエンド側の負担を減らすためにBFFが必要になってきます。</p>
<p>そんなBFFを開発する際はパフォーマンス重視ならGo言語で開発するのがいいようですが、一般的な開発のしやすさでは<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Nest.js（TypeScript）</strong></span>というフレームワークが使われていたりするでしょう。</p>
<p>ということでこの記事では、そんなNest.jsの使い方について解説します。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Nest.jsでGraphQLなどのBFF（Backend for Frontend）を開発する方法まとめ</h2>
<p>まずNest.jsの開発を始めるには事前にNode.jsのインストールが必要になるため、もしまだNode.jsが使えない方は、voltaなどのパッケージ管理ツールを利用してインストールをして下さい。</p>
<p><strong><span style="color: #ff0000;">※この記事で利用しているNode.jsのバージョンはv22.13です。</span></strong></p>
<p>&nbsp;</p>
<p>事前準備完了後、以下のコマンドを実行し、Nest.jsのCLIをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i -g @nestjs/cli</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、新しいプロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir nest-sample &amp;&amp; cd nest-sample
$ nest new rest
$ cd rest</code></pre>
</div>
<p><strong><span style="color: #ff0000;">※nest newコマンドを実行すると利用するパッケージ管理ツールを聞かれますが、この記事ではnpmを使います。</span></strong></p>
<p>&nbsp;</p>
<p>次にNest.jsにはデフォルトでフォーマット用のPrettierとコード解析用のESLintが入っていて使えますが、ESLintは自動でコード修正する<strong><span style="color: #ff0000;">オプション「&#8211;fix」が邪魔</span></strong>なのと、フォーマットとコード解析を同時に行いたいため、package.jsonを以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="rest/package.json"><code>{
  "scripts": {

    ・・・

    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
    "fix": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" &amp;&amp; eslint \"{src,apps,libs,test}/**/*.ts\"",

    ・・・

  }

}</code></pre>
</div>
<p>&nbsp;</p>
<p>そして<span style="color: #ff0000;"><strong>テストコードではESLintのチェックを無効化したい</strong></span>ので、「rest/eslint.config.mjs」のignoresに「**/*.spec.ts」を追加してspecファイルをチェック対象外にします。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-js" data-lang="JavaScript" data-file="rest/eslint.config.mjs"><code>// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  {
    ignores: ['eslint.config.mjs', '**/*.spec.ts'],
  },
  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  eslintPluginPrettierRecommended,

・・・</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に環境変数を使えるようにするため、以下のコマンドを実行してライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i @nestjs/config</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、環境変数用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch .env</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成した環境変数用のファイルを以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="rest/.env"><code>NODE_ENV=development
ENV=local
PORT=3000</code></pre>
</div>
<p>&nbsp;</p>
<p>次に環境変数を使えるようにするため、「rest/src/main.ts」および「rest/src/app.module.ts」をそれぞれ以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/main.ts"><code>import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const port = configService.get&lt;number&gt;('PORT') ?? 3000;

  await app.listen(port);
}
void bootstrap();</code></pre>
</div>
</div>
<p><strong><span style="color: #ff0000;">※デフォルトでは環境変数の利用にprocess.envを利用していますが、私が試した時点（2025年2月）ではビルドするとエラーが発生していたので、@nestjs/configを使った方法が最適なようです。また、デフォルトでは「bootstrap()」の部分でESLintの警告が出ていたので、前に「void」を付けています。</span></strong></p>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/app.module.ts"><code>import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に環境変数が利用できることを確認するため、「rest/src/app.service.ts」と「rest/src/app.controller.spec.ts」をそれぞれ以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/app.service.ts"><code>import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
  constructor(private configService: ConfigService) {}

  getHello(): string {
    const port = this.configService.get&lt;number&gt;('PORT');
    const nodeEnv = this.configService.get&lt;string&gt;('NODE_ENV');
    const env = this.configService.get&lt;string&gt;('ENV');

    return `Hello World !! [PORT: ${port}, NODE_ENV: ${nodeEnv}, ENV: ${env}]`;
  }
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript"><code>import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';

describe('AppController', () =&gt; {
  let appController: AppController;
  let configService: ConfigService;

  beforeEach(async () =&gt; {
    const app: TestingModule = await Test.createTestingModule({
      imports: [ConfigModule.forRoot({ isGlobal: true })],
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get&lt;AppController&gt;(AppController);
    configService = app.get&lt;ConfigService&gt;(ConfigService);
  });

  describe('root', () =&gt; {
    it('should return text', () =&gt; {
      const port = configService.get&lt;number&gt;('PORT');
      const nodeEnv = 'test'; // テスト実行時はNODE_ENVがtestになる
      const env = configService.get&lt;string&gt;('ENV');

      expect(appController.getHello()).toBe(
        `Hello World !! [PORT: ${port}, NODE_ENV: ${nodeEnv}, ENV: ${env}]`,
      );
    });
  });
});</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コードチェックおよびテストコードを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run fix
$ npm run test</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようになればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-1-1024x678.png" alt="" width="760" height="503" class="aligncenter wp-image-19793" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-1-1024x678.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-1-300x200.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-1-768x509.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-1.png 1108w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカルサーバーを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run start:dev</code></pre>
</div>
<p><span style="color: #ff0000;">※start:devを使うとホットリロード（コード修正が即時反映）が有効になる</span></p>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3000」を開き、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-2-1024x644.png" alt="" width="760" height="478" class="aligncenter wp-image-19794" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-2-1024x644.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-2-300x189.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-2-768x483.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-2.png 1090w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>これで基本的な初期設定が完了したので、ショートカットキー「control + c」などでローカルサーバーを止めて下さい。</p>
<p>以降ではBFFをREST APIで作る場合と、GraphQLで作る場合に分けて解説します。</p>
<p>尚、両者の違いについては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>作りやすいのはREST API</strong></span>で、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>学習コストは高いですが、フロント側から実行する際に柔軟性が高いのがGraphQL</strong></span>です。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>BFFをREST APIで作る場合</h2>
<p>まずREST APIで作る場合について、以下のコマンドを実行し、新規API追加用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ nest g module sample
$ nest g service sample --no-spec
$ nest g controller sample
$ cd src/sample
$ touch sample.dto.ts
$ touch sample.repo.ts
$ cd ../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.module.ts"><code>import { Module } from '@nestjs/common';
import { SampleService } from './sample.service';
import { SampleController } from './sample.controller';
import { SampleRepository } from './sample.repo';

@Module({
  providers: [SampleService, SampleRepository],
  controllers: [SampleController],
})
export class SampleModule {}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.dto.ts"><code>// リクエストパラメータやレスポンスの型を定義
export interface GetSampleResponse {
  id: number;
  text: string;
}

export class GetSampleResponseDto implements GetSampleResponse {
  id: number;
  text: string;
}

export interface PostSampleRequestBody {
  text: string;
}

export class PostSampleRequestBodyDto implements PostSampleRequestBody {
  text: string;
}

export interface PostSampleResponse {
  upperText: string;
  lowerText: string;
  textLength: number;
}

export class PostSampleResponseDto implements PostSampleResponse {
  upperText: string;
  lowerText: string;
  textLength: number;
}</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※dto（Data Transfer Object）はデザインパターンの一つで、データ受け渡し専用クラスを指します。また、今回は省略していますが、クラスの方を使えばライブラリ「class-validator」を使ってバリデーションチェックなども入れられます。</span></p>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.repo.ts"><code>// 外部APIを呼び出す処理を記述する想定
import { Injectable } from '@nestjs/common';
import { PostSampleResponse } from './sample.dto';

@Injectable()
export class SampleRepository {
  // eslint-disable-next-line
  async textProcessing(text: string): Promise&lt;any&gt; {
    const upperText = text.toUpperCase();
    const lowerText = text.toLowerCase();
    const textLength = text.length;
    const data: PostSampleResponse = {
      upperText,
      lowerText,
      textLength,
    };
    const response = { data: data };

    return response;
  }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.service.ts"><code>import { Injectable } from '@nestjs/common';
import {
  GetSampleResponse,
  PostSampleRequestBody,
  PostSampleResponse,
} from './sample.dto';
import { SampleRepository } from './sample.repo';
import { HttpException, HttpStatus } from '@nestjs/common';

@Injectable()
export class SampleService {
  constructor(private readonly sampleRepo: SampleRepository) {}

  getService(): GetSampleResponse {
    return { id: 1, text: 'sample' } as GetSampleResponse;
  }

  async postService(body: PostSampleRequestBody): Promise&lt;PostSampleResponse&gt; {
    try {
      // eslint-disable-next-line
      const res = await this.sampleRepo.textProcessing(body.text);

      // eslint-disable-next-line
      return res.data;
    } catch (error) {
      console.error(error);
      throw new HttpException(
        'Internal Server Error',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.controller.ts"><code>import { Controller, Get, Post, Body, HttpCode } from '@nestjs/common';
import {
  GetSampleResponse,
  PostSampleRequestBody,
  PostSampleResponse,
} from './sample.dto';
import { SampleService } from './sample.service';

@Controller('api/v1/sample')
export class SampleController {
  constructor(private readonly sampleService: SampleService) {}

  @Get('get')
  getSample(): GetSampleResponse {
    return this.sampleService.getService();
  }

  @Post('post')
  @HttpCode(200)
  async postSample(
    @Body() body: PostSampleRequestBody,
  ): Promise&lt;PostSampleResponse&gt; {
    return await this.sampleService.postService(body);
  }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.controller.spec.ts"><code>import { Test, TestingModule } from '@nestjs/testing';
import { SampleController } from './sample.controller';
import { SampleService } from './sample.service';
import { SampleRepository } from './sample.repo';
import { GetSampleResponse, PostSampleResponse } from './sample.dto';
import { HttpException, HttpStatus } from '@nestjs/common';

describe('SampleController', () =&gt; {
  let originalConsoleLog: any;
  let controller: SampleController;
  let repository: SampleRepository;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      providers: [SampleService, SampleRepository],
      controllers: [SampleController],
    })
      // 外部APIの実行を想定し、モック化したいクラスがある場合
      .overrideProvider(SampleRepository)
      .useValue({
        textProcessing: jest.fn(),
      })
      .compile();

    controller = module.get&lt;SampleController&gt;(SampleController);
    repository = module.get&lt;SampleRepository&gt;(SampleRepository);

    // ログ出力のモック化
    originalConsoleLog = console.error;
    console.error = jest.fn();
  });

  afterEach(() =&gt; {
    // ログ出力のモック化解除
    console.error = originalConsoleLog;
  });

  it('should be defined', () =&gt; {
    expect(controller).toBeDefined();
  });

  it('should return GetSampleResponse', () =&gt; {
    const data: GetSampleResponse = { id: 1, text: 'sample' };
    expect(controller.getSample()).toEqual(data);
  });

  it('should return PostSampleResponse', async () =&gt; {
    const body = { text: 'abCde' };
    const data: PostSampleResponse = {
      upperText: 'ABCDE',
      lowerText: 'abcde',
      textLength: 5,
    };

    // モックの戻り値を設定
    (repository.textProcessing as jest.Mock).mockReturnValue({ data: data });

    const res = await controller.postSample(body);

    expect(res).toEqual(data);
  });

  it('should throw error', async () =&gt; {
    const body = { text: 'abCde' };

    // モックの戻り値を設定
    const error = new HttpException('Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
    (repository.textProcessing as jest.Mock).mockRejectedValue(error);

    try {
      await controller.postSample(body);
    } catch (error) {
      expect(error).toBeInstanceOf(HttpException);
      expect(error.message).toBe('Internal Server Error');
      expect(error.status).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  });
});</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テストを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run fix
$ npm run test</code></pre>
</div>
<p>&nbsp;</p>
<p>テスト実行後、以下のようになればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-3.png" alt="" width="761" height="387" class="aligncenter wp-image-19805" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-3.png 712w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-3-300x153.png 300w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、再度ローカルサーバーを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run start:dev</code></pre>
</div>
<p>&nbsp;</p>
<p>次に追加したAPIを試すため、API実行用ツールの<a href="https://www.postman.com" target="_blank" rel="noopener">Postman</a>を使って実行してみます。</p>
<p>まずGETメソッドのAPIを実行し、以下のように想定通りのレスポンス結果が返ってこればOKです。</p>
<img loading="lazy" decoding="async" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19806" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4.jpg 3014w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4-1024x639.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4-1536x959.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-4-2048x1279.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次にPOSTメソッドのAPIを実行し、以下のように想定通りのレスポンス結果が返ってこればOKです。</p>
<img loading="lazy" decoding="async" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5.jpg" alt="" width="760" height="476" class="aligncenter wp-image-19807 " srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5.jpg 3018w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5-1024x641.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5-768x481.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5-1536x962.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-5-2048x1283.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<h3>外部APIを実行したい場合</h3>
<p>今回は省略していますが、もしBFFから<span style="color: #ff0000;"><strong>外部APIを実行したい場合</strong></span>は、以下のコマンドで<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Axiosのライブラリをインストール</strong></span>して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i @nestjs/axios axios</code></pre>
</div>
<p><span style="color: #ff0000;">※Axios単体ではなく、Nest.jsに組み込まれている機能を使って実装していきます。（Axios単体でも実装できますが、基本的にはNest.jsの機能を使った方がいいようです。）</span></p>
<p>&nbsp;</p>
<p>次に「rest/src/sample/sample.module.ts」などの対象のモジュールファイルで「HttpModule」をインポートします。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.module.ts"><code>import { Module } from '@nestjs/common';
import { SampleService } from './sample.service';
import { SampleController } from './sample.controller';
import { SampleRepository } from './sample.repo';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [HttpModule],
  providers: [SampleService, SampleRepository],
  controllers: [SampleController],
})
export class SampleModule {}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に「rest/src/sample/sample.repo.ts」などの切り出したリポジトリ用のファイルを使って、以下のような感じで「firstValueFrom」を使って外部APIを実行して下さい。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.repo.ts"><code>// 外部APIを呼び出す処理を記述する想定
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class SampleRepository {
  constructor(private httpService: HttpService) {}

  async execExternalApi(): Promise&lt;any&gt; {
    const url = "https://api.thecatapi.com/v1/images/search";

    try {
      const res = await firstValueFrom(
        this.httpService.get&lt;any&gt;(url),
      );

      return res.data;
    } catch (error) {
      console.error(error);
    }
  }  
}</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※このように外部APIの実行部分を別ファイルに切り出せば、テストコードでモック化するのが容易になります。</span></p>
<p>&nbsp;</p>
<h3>OpenAPIのAPI仕様書を作りたい場合</h3>
<p>次にNest.jsでは簡単にOpenAPIのAPI仕様書を実装できるので、作りたい場合は以下のコマンドを実行して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i @nestjs/swagger
$ npm i fs-extra @types/fs-extra
$ npm i -D openapi-to-md
$ mkdir openapi</code></pre>
</div>
<p><span style="color: #ff0000;">※「fs-extra」、「@types/fs-extra」、「openapi-to-md」はファイルに出力するために使います。尚、私が試した時点ではビルド時に標準の「fs」が使えなかったので、npmライブラリの「fs-extra」を使うことで問題を解消できました。</span></p>
<p>&nbsp;</p>
<p>次に「rest/src/main.ts」を以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/main.ts"><code>import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as fs from 'fs-extra';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const port = configService.get&lt;number&gt;('PORT') ?? 3000;
  const nodeEnv = configService.get&lt;string&gt;('NODE_ENV') ?? 'development';

  // 開発環境のみOpenAPIを有効化
  if (nodeEnv != 'production') {
    const config = new DocumentBuilder()
      .setTitle('SampleのAPI仕様書')
      .setDescription('Nest.jsで作成したAPIの仕様書です。')
      .setVersion('1.0')
      .build();
    const documentFactory = () =&gt; SwaggerModule.createDocument(app, config);

    SwaggerModule.setup('api', app, documentFactory);

    // OpenAPIのJSONファイルを出力
    const outputFilePath = './openapi/openapi.json';
    await fs.writeFile(
      outputFilePath,
      JSON.stringify(documentFactory(), null, 2),
    );
  }
  await app.listen(port);
}
void bootstrap();</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に「rest/src/sample/sample.dto.ts」や「rest/src/sample/sample.controller.ts」などの対象のdtoファイルやコントローラーファイルにOpenAPI用の設定を追加します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.dto.ts"><code>// リクエストパラメータやレスポンスの型を定義
import { ApiProperty } from '@nestjs/swagger';
export interface GetSampleResponse {
  id: number;
  text: string;
}

export class GetSampleResponseDto implements GetSampleResponse {
  @ApiProperty()
  id: number;

  @ApiProperty()
  text: string;
}

export interface PostSampleRequestBody {
  text: string;
}

export class PostSampleRequestBodyDto implements PostSampleRequestBody {
  @ApiProperty()
  text: string;
}
export interface PostSampleResponse {
  upperText: string;
  lowerText: string;
  textLength: number;
}

export class PostSampleResponseDto implements PostSampleResponse {
  @ApiProperty()
  upperText: string;

  @ApiProperty()
  lowerText: string;

  @ApiProperty()
  textLength: number;
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="rest/src/sample/sample.controller.ts"><code>import { Controller, Get, Post, Body, HttpCode } from '@nestjs/common';
import {
  GetSampleResponse,
  GetSampleResponseDto,
  PostSampleRequestBody,
  PostSampleRequestBodyDto,
  PostSampleResponse,
  PostSampleResponseDto,
} from './sample.dto';
import { SampleService } from './sample.service';
import { ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';

@Controller('api/v1/sample')
export class SampleController {
  constructor(private readonly sampleService: SampleService) {}

  @Get('get')
  @ApiOperation({ description: 'GETメソッドのサンプルAPI' })
  @ApiResponse({
    status: 200,
    description: 'APIが正常終了',
    type: GetSampleResponseDto,
  })
  getSample(): GetSampleResponse {
    return this.sampleService.getService();
  }

  @Post('post')
  @HttpCode(200)
  @ApiOperation({ description: 'POSTメソッドのサンプルAPI' })
  @ApiBody({ type: PostSampleRequestBodyDto })
  @ApiResponse({
    status: 200,
    description: 'APIが正常終了',
    type: PostSampleResponseDto,
  })
  @ApiResponse({
    status: 500,
    description: 'Internal Server Error',
  })
  async postSample(
    @Body() body: PostSampleRequestBody,
  ): Promise&lt;PostSampleResponse&gt; {
    return await this.sampleService.postService(body);
  }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次にローカルサーバーを起動後、ブラウザで「http://localhost:3000/api」を開き、以下のようにOpenAPIのAPI仕様書を確認できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-6-1024x640.jpg" alt="" width="761" height="476" class="aligncenter wp-image-19812" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-6-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-6-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-6-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-6-1536x960.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-6-2048x1279.jpg 2048w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-7-1024x640.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19813" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-7-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-7-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-7-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-7-1536x960.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-7-2048x1280.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-8-1024x639.jpg" alt="" width="760" height="474" class="aligncenter wp-image-19814 " srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-8-1024x639.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-8-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-8-768x479.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-8-1536x958.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-8-2048x1277.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>そして「rest/openapi/openapi.json」にjson形式のファイルが作成されていると思いますが、md形式に変換したい場合は以下のコマンドを実行して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npx openapi-to-md </code></pre>
</div>
<p>&nbsp;</p>
<p>md形式に変換すると、例えば<strong><span style="color: #3366ff;">テキストエディタやGitHubから直接見れる</span></strong>ようになります。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-9-1024x639.jpg" alt="" width="760" height="474" class="aligncenter wp-image-19816" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-9-1024x639.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-9-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-9-768x479.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-9-1536x959.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-9-2048x1279.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-10-1024x639.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19817" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-10-1024x639.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-10-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-10-768x479.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-10-1536x959.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-10-2048x1279.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>BFFをGraphQLで作る場合</h2>
<p>次にもう一つの方法として、BFFをGraphQLで作る場合について解説していきますが、上記のREST APIを作る前までは同じです。</p>
<p>まずは以下のコマンドを実行し、必要なライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql</code></pre>
</div>
<p>&nbsp;</p>
<p>次に「src/app.module.ts」を以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/app.module.ts"><code>import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { ConfigService } from '@nestjs/config';
import { SampleModule } from './sample/sample.module';

// 開発環境のみPlaygroundを使えるようにするためのフラグを設定
const configService = new ConfigService();
const nodeEnv = configService.get&lt;string&gt;('NODE_ENV') ?? 'development';
const playgroundFlag = nodeEnv != 'production';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    GraphQLModule.forRoot&lt;ApolloDriverConfig&gt;({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      playground: playgroundFlag,
    }),
    SampleModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、GraphQLのAPIを作るための各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ nest g module sample
$ nest g service sample --no-spec
$ nest g resolver sample
$ cd src/sample
$ touch sample.model.ts
$ cd ../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルについて、それぞれ以下のように記述します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/sample/sample.model.ts"><code>// GraphQLのスキーマにおける型の定義
import { Field, Int, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class SampleModel {
  @Field(() =&gt; Int)
  id: number;

  @Field()
  title: string;

  @Field()
  description: string;

  @Field()
  createdAt: Date;

  @Field()
  updatedAt: Date;

  @Field(() =&gt; Date, { nullable: true })
  deletedAt: Date | null;
}</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※本来はスキーマ定義でバリデーションチェックを入れると思いますが、今回は省略しています。</span></p>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/sample/sample.service.ts"><code>import { Injectable } from '@nestjs/common';
import { SampleModel } from './sample.model';

@Injectable()
export class SampleService {
  /**
  * サンプルデータの取得
  * @returns サンプルデータ
  */
  // eslint-disable-next-line
  async getSampleData(): Promise&lt;SampleModel[]&gt; {
    const data = [
      {
        id: 1,
        title: 'タイトル1',
        description: '説明1',
        createdAt: new Date(),
        updatedAt: new Date(),
        deletedAt: null,
      },
      {
        id: 2,
        title: 'タイトル2',
        description: '説明2',
        createdAt: new Date(),
        updatedAt: new Date(),
        deletedAt: null,
      },
    ];

    return data;
  }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/sample/sample.resolver.ts"><code>import { Resolver } from '@nestjs/graphql';
import { Query } from '@nestjs/graphql';
import { SampleModel } from './sample.model';
import { SampleService } from './sample.service';

@Resolver()
export class SampleResolver {
  constructor(private readonly sampleService: SampleService) {}

  /**
  * サンプルクエリ
  * @returns サンプルモデル[]
  */
  @Query(() =&gt; [SampleModel], { nullable: true })
  async getSample(): Promise&lt;SampleModel[]&gt; {
    return await this.sampleService.getSampleData();
  }
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/sample/sample.resolver.spec.ts"><code>import { Test, TestingModule } from '@nestjs/testing';
import { SampleResolver } from './sample.resolver';
import { SampleService } from './sample.service';
import { AppModule } from '../app.module';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';

describe('SampleResolver', () =&gt; {
  let resolver: SampleResolver;
  let app: INestApplication;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
      providers: [SampleResolver, SampleService],
    }).compile();

    app = module.createNestApplication();
    await app.init();
    resolver = module.get&lt;SampleResolver&gt;(SampleResolver);
  });

  // 全テスト終了後の処理
  afterAll(async () =&gt; {
    await app.close();
  });

  it('should be defined', () =&gt; {
    expect(resolver).toBeDefined();
  });

  it('should return getSample all data', async () =&gt; {
    const query = `
      query TestGetSampleQuery {
        getSample {
          id
          title
          description
          createdAt
          updatedAt
          deletedAt
        }
      }
    `;

    const res = await request(app.getHttpServer())
      .post('/graphql')
      .send({ query });

    expect(res.body.data.getSample).toBeDefined();
    expect(res.body.data.getSample).toBeInstanceOf(Array);
    expect(res.body.data.getSample.length).toBeGreaterThan(0);
    // 1件目のデータを確認
    expect(res.body.data.getSample[0].id).toBe(1);
    expect(res.body.data.getSample[0].title).toBe('タイトル1');
    expect(res.body.data.getSample[0].description).toBe('説明1');
    expect(res.body.data.getSample[0].createdAt).toBeDefined();
    expect(res.body.data.getSample[0].updatedAt).toBeDefined();
    expect(res.body.data.getSample[0].deletedAt).toBeNull();
    // 2件目のデータを確認
    expect(res.body.data.getSample[1].id).toBe(2);
    expect(res.body.data.getSample[1].title).toBe('タイトル2');
    expect(res.body.data.getSample[1].description).toBe('説明2');
    expect(res.body.data.getSample[1].createdAt).toBeDefined();
    expect(res.body.data.getSample[1].updatedAt).toBeDefined();
    expect(res.body.data.getSample[1].deletedAt).toBeNull();
  });

  it('should return getSample selection field', async () =&gt; {
    const query = `
      query TestGetSampleQuery {
        getSample {
          id
          title
          description
        }
      }
    `;

    const res = await request(app.getHttpServer())
      .post('/graphql')
      .send({ query });

    expect(res.body.data.getSample).toBeDefined();
    expect(res.body.data.getSample).toBeInstanceOf(Array);
    expect(res.body.data.getSample.length).toBeGreaterThan(0);
    // 1件目のデータを確認
    expect(res.body.data.getSample[0].id).toBe(1);
    expect(res.body.data.getSample[0].title).toBe('タイトル1');
    expect(res.body.data.getSample[0].description).toBe('説明1');
    expect(res.body.data.getSample[0].createdAt).toBeUndefined();
    expect(res.body.data.getSample[0].updatedAt).toBeUndefined();
    expect(res.body.data.getSample[0].deletedAt).toBeUndefined();
    // 2件目のデータを確認
    expect(res.body.data.getSample[1].id).toBe(2);
    expect(res.body.data.getSample[1].title).toBe('タイトル2');
    expect(res.body.data.getSample[1].description).toBe('説明2');
    expect(res.body.data.getSample[1].createdAt).toBeUndefined();
    expect(res.body.data.getSample[1].updatedAt).toBeUndefined();
    expect(res.body.data.getSample[1].deletedAt).toBeUndefined();
  });
});</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テストコードを実行して確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run fix
$ npm run test</code></pre>
</div>
<p>&nbsp;</p>
<p>テスト実行後、以下のようになればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-11.png" alt="" width="761" height="401" class="aligncenter wp-image-19820" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-11.png 702w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-11-300x158.png 300w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカルサーバーを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run start:dev</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3000/graphql」を開き、GraphQLのPlaygroundが表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12.png" alt="" width="3024" height="1890" class="aligncenter wp-image-19821 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12.png 3024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12-300x188.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12-1024x640.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12-768x480.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12-1536x960.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-12-2048x1280.png 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<p>Playgroundでは画面右側の「SCHEMA」や「DOCS」のタブをクリックすると、それぞれドキュメントが確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13.jpg" alt="" width="3024" height="1884" class="aligncenter wp-image-19822 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13-1024x638.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13-768x478.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13-1536x957.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-13-2048x1276.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<p>次に画面の左側の入力フィールドに以下のクエリを記述し、画面中央のボタンを押してGraphQLのAPIを実行して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>query GetSampleData {
  getSample {
    id
    title
    description
    createdAt
    updatedAt
    deletedAt
  }
}</code></pre>
</div>
<p>&nbsp;</p>
<p>クエリ実行後、以下のように画面右側にデータが取得できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14.jpg" alt="" width="3024" height="1886" class="aligncenter size-full wp-image-19823" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14-1024x639.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14-768x479.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14-1536x958.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-14-2048x1277.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p><strong><span style="color: #ff0000;">※GraphQLのレスポンスのステータスコードは基本的に200を返す仕様のため、フロント側でエラーチェックをするにはエラー時のレスポンス結果にステータスコードなどを含める必要があるのはご注意下さい。</span></strong></p>
<p>&nbsp;</p>
<p>次にクエリを以下のように修正して再実行して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>query GetSampleData {
  getSample {
    id
    title
    description
  }
}</code></pre>
</div>
<p>&nbsp;</p>
<p>クエリ実行後、以下のように指定したフィールドのみ取得できていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15.jpg" alt="" width="3024" height="1884" class="aligncenter size-full wp-image-19824" srcset="https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15-1024x638.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15-768x478.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15-1536x957.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/02/nest-15-2048x1276.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<p>このように<strong><span style="color: #3366ff;">GraphQLを使えば必要なフィールドのみレスポンス結果として返すことが容易</span></strong>になります。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はNest.jsの使い方について解説しました。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Nest.jsはREST APIもGraphQLもどちらも対応することができるフレームワーク</strong></span>になっているので、TypeScript（JavaScript）でバックエンドAPIを作るのにおすすめです。</p>
<p>そしてBFFを開発するならGraphQLで開発していくのがいいとは思いますが、<strong><span style="color: #ff0000;">GraphQLは学習コストが高い（知見が無いメンバーが多いと開発コストが高くなる）というデメリットもある</span></strong>ため、どちらを使うかはプロジェクト規模などに応じてしっかり検討するようにして下さい。</p>
<p>ということで、これからNest.jsを試したい方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>各種SNSなど</strong></p>
<p>各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします！</p>
<ul>
<li> <a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">X（旧Twitter）</a></li>
<li> <a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">YouTube</a></li>
</ul>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-use-nestjs">Nest.jsでGraphQLなどのBFF（Backend for Frontend）を開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-use-nestjs/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Next.js v15にApolloを導入してGraphQLを試す！</title>
		<link>https://tomoyuki65.com/install-apollo-in-next-js-v15-and-try-graphql</link>
					<comments>https://tomoyuki65.com/install-apollo-in-next-js-v15-and-try-graphql#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Wed, 30 Oct 2024 13:50:58 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19540</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 気付けばNext.jsのv15がリリースされていましたが、フロントエンド開発の際にGraphQLを利用したいこともあると思います。 そんな私もG...</p>
The post <a href="https://tomoyuki65.com/install-apollo-in-next-js-v15-and-try-graphql">Next.js v15にApolloを導入してGraphQLを試す！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-8-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-19562" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-8-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-8-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>気付けばNext.jsのv15がリリースされていましたが、フロントエンド開発の際に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>GraphQL</strong></span>を利用したいこともあると思います。</p>
<p>そんな私もGraphQLについては言葉だけ聞いたことがあるぐらいでよくわからないことが多かったので、Next.jsに<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Apollo</strong></span>というGraphQLのライブラリを導入して試してみることにしました。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Next.js v15にApolloを導入してGraphQLを試す！</h2>
<p>まずはNext.jsのプロジェクトを作成しますが、作成するには<strong><span style="color: #ff0000;">事前にnodeおよびnpmのインストールが必要</span></strong>になります。</p>
<p>それらの詳細は割愛させていただきますが、もしまだインストールしてない方は先にそちらの準備をお願いします。</p>
<p>ではまず以下のコマンドを実行し、Next.jsのプロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npx create-next-app@latest graphql-sample</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、設定について聞かれるので以下のように選択（基本的にデフォルト設定）します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-1-1024x452.jpg" alt="" width="760" height="335" class="aligncenter wp-image-19543" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-1-1024x452.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-1-300x132.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-1-768x339.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-1.jpg 1056w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>プロジェクトの作成が完了後、以下のコマンドを実行してディレクトリを移動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ cd graphql-sample</code></pre>
</div>
<p>&nbsp;</p>
<p>作成されたプロジェクトをテキストエディタで開き、以下のように各種ファイルが作成されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-2-1024x657.png" alt="" width="760" height="488" class="aligncenter wp-image-19544" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-2-1024x657.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-2-300x192.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-2-768x492.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-2.png 1338w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<h3>各種ライブラリのインストール</h3>
<p>次に以下のコマンドを実行し、GraphQL用の各種ライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i graphql @apollo/client @apollo/server
$ npm i --legacy-peer-deps @as-integrations/next @graphql-tools/utils @graphql-tools/merge
$ npm i -D --legacy-peer-deps @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/typescript @graphql-codegen/typescript-resolvers</code></pre>
</div>
<p><strong><span style="color: #ff0000;">※2行目と3行目のライブラリはNext.js15に対応していない？感じでオプション「&#8211;legacy-peer-deps」が必要になりました。</span></strong></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、codegen用の設定ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch codegen.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを次のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="codegen.ts"><code>import { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
    schema: "apollo/documents/**/*.gql",
    documents: ["apollo/documents/**/*.gql"],
    generates: {
        "./apollo/__generated__/client/": {
            preset: "client",
            plugins: [],
            presetConfig: {
                gqlTagName: "gql",
            },
        },
        "./apollo/__generated__/server/resolvers-types.ts": {
            plugins: ["typescript", "typescript-resolvers"],
        },
    },
    ignoreNoDocuments: true,
};

export default config;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にcodegen実行用のコマンドを追加するため、package.jsonを以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-js" data-lang="JSON" data-file="package.json"><code>{

・・・
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "compile": "graphql-codegen",
    "watch": "graphql-codegen -w"
  },

・・・
}</code></pre>
</div>
<p>&nbsp;</p>
<h3>GraphQLのスキーマ定義用のファイルを作成</h3>
<p>次に以下のコマンドを実行し、GraphQLのスキーマ定義（<span>GraphQL APIのデータ構造と型を定義するもの</span>）用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p apollo/documents
$ cd apollo/documents
$ touch userSchema.gql
$ cd ../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="apollo/documents/userSchema.gql"><code>schema {
  query: Query
}

type Query {
  users: [User!]!
}

type User {
  id: Int
  name: String
}

query ALL_USERS {
  users {
    id,
    name
  }
}</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、スキーマ定義のファイルを作成します。成功すると「apollo/__generated__」配下に各種ファイルが作成されます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run codegen</code></pre>
</div>
<p>&nbsp;</p>
<h3>GraphQLのリゾルバー用のファイルを作成</h3>
<p>次に以下のコマンドを実行し、リゾルバー（<span>GraphQLサーバーがクライアントからのクエリを処理するために使用する関数</span>）用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p apollo/resolvers
$ cd apollo/resolvers
$ touch user.ts index.ts
$ cd ../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="apollo/resolvers/user.ts"><code>import { readFileSync } from "fs";
import { join } from "path";
import { IResolvers } from '@graphql-tools/utils'
import { Resolvers } from "../__generated__/server/resolvers-types";
import { User } from '../__generated__/client/graphql';

const userTypeDefs = readFileSync(
    join(process.cwd(), "apollo/documents/userSchema.gql"),
    "utf-8"
);

const userResolvers: IResolvers&lt;Resolvers&gt; = {
    Query: {
        users() {
            const USERS = []

            for (let i: number = 0; i &lt; 3; i++) {
                const USER: User = {
                    id: i,
                    name: `User-${i+1}`
                }

                USERS.push(USER)
            }

            return USERS
        },
    },
};

export { userTypeDefs, userResolvers };</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※実際にはこのリゾルバーの部分で外部APIからデータを取得し、スキーマ定義型に整形してreturnするようにします。</span></p>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="apollo/resolvers/index.ts"><code>import { mergeResolvers } from '@graphql-tools/merge'
import { mergeTypeDefs } from '@graphql-tools/merge';
import { userTypeDefs, userResolvers } from './user'

const typeDefs = mergeTypeDefs([
    userTypeDefs,
]);

const resolvers = mergeResolvers([
    userResolvers,
]);

export {typeDefs, resolvers};</code></pre>
</div>
</div>
<p>&nbsp;</p>
<h3>GraphQLサーバー用のファイルを作成</h3>
<p>次に以下のコマンドを実行し、GraphQLサーバー用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/app/api/graphql
$ cd src/app/api/graphql
$ touch route.ts
$ cd ../../../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを次のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/app/api/graphql/route.ts"><code>import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { ApolloServer } from "@apollo/server";
import { Resolvers } from "../../../../apollo/__generated__/server/resolvers-types";
import { typeDefs, resolvers } from '../../../../apollo/resolvers/index';
import { NextRequest } from 'next/server';

const apolloServer = new ApolloServer&lt;Resolvers&gt;({ typeDefs, resolvers });

const handler = startServerAndCreateNextHandler&lt;NextRequest&gt;(apolloServer, {
    context: async req =&gt; ({ req }),
});

export { handler as GET, handler as POST };</code></pre>
</div>
</div>
<p>&nbsp;</p>
<h3>GraphQLサーバーの起動</h3>
<p>次に以下のコマンドを実行し、Next.jsのローカルサーバーを起動させ、GraphQLサーバーを使えるようにします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run dev</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3000」にアクセスし、以下のようにNext.js v15のページが表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-3-1024x731.png" alt="" width="762" height="544" class="aligncenter wp-image-19549" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-3-1024x731.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-3-300x214.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-3-768x548.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-3-1536x1096.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-3-2048x1462.png 2048w" sizes="auto, (max-width: 762px) 100vw, 762px" />
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3000/api/graphql」にアクセスし、以下のようにGraphQLのUIページが表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-4-1024x738.png" alt="" width="761" height="549" class="aligncenter wp-image-19550" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-4-1024x738.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-4-300x216.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-4-768x553.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-4-1536x1107.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-4-2048x1476.png 2048w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p>&nbsp;</p>
<h3>GraphQLを実行して試す</h3>
<p>次にGraphQLを試してみますが、既にデフォルトでusersデータが取得するように設定されているため、画面右上の「<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎Query」をクリックして実行します。</p>
<p>実行後、画面右側にユーザーのリゾルバーで定義した値が取得されて表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-5-1024x737.jpg" alt="" width="761" height="548" class="aligncenter wp-image-19552" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-5-1024x737.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-5-300x216.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-5-768x553.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-5-1536x1106.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-5-2048x1475.jpg 2048w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p>&nbsp;</p>
<p>例えばユーザーのname項目だけ取得したい場合は、Queryの記述を以下のように修正して実行してみて下さい。</p>
<p>実行後、以下のようにname項目だけ取得できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-6-1024x736.jpg" alt="" width="761" height="547" class="aligncenter wp-image-19553" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-6-1024x736.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-6-300x216.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-6-768x552.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-6-1536x1104.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-6-2048x1472.jpg 2048w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p>&nbsp;</p>
<p>このように<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>GraphQLを使うことによって取得項目を柔軟に変えることができるのが一つのメリット</strong></span>になります。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Next.jsのフロント画面の方からApolloのGraphQLを使えるようにする</h2>
<p>次にNext.jsのフロントが画面からGraphQLでデータを取得し、それを画面に表示させてみます。</p>
<p>まずは以下のコマンドを実行し、GraphQL用のコンポーネントファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ cd apollo
$ touch client.ts
$ cd ..
$ mkdir -p src/components
$ cd src/components
$ touch ApolloClient.tsx User.tsx
$ cd ../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="apollo/client.ts"><code>import { ApolloClient, InMemoryCache } from "@apollo/client";

export const client = new ApolloClient({
    uri: "/api/graphql",
    cache: new InMemoryCache(),
});</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/components/ApolloClient.tsx"><code>"use client"

import { ApolloProvider } from "@apollo/client"
import { client } from "../../apollo/client"

export default function ApolloClient({ children }: React.PropsWithChildren) {
    return &lt;ApolloProvider client={client}&gt;{children}&lt;/ApolloProvider&gt;
};</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/components/User.tsx"><code>"use client"

import gql from 'graphql-tag'
import { useQuery } from "@apollo/client"

const USERS = gql(`
    query Query {
        users {
            id,
            name
        }
    }
`);

export default function User() {
    const { data, loading, error } = useQuery(USERS)
    if (loading) {
        return &lt;div&gt;読み込み中...&lt;/div&gt;
    }

    return (
        &lt;div&gt;
            {error &amp;&amp; &lt;div&gt;{error.message}&lt;/div&gt;}
            &lt;ul&gt;
                { data &amp;&amp;
                  data.users.map((v: any, i: number) =&gt; (
                      &lt;li key={String(i)}&gt;
                          { `ID: ${v.id}　名前: ${v.name}` }
                      &lt;/li&gt;
                  ))
                }
            &lt;/ul&gt;
        &lt;/div&gt;
    )
};</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に作成したApolloClientコンポーネントをトップのlayout.tsxに追加します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/app/layout.tsx"><code>import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import ApolloClient from "../components/ApolloClient";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) {
  return (
    &lt;html lang="en"&gt;
      &lt;body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      &gt;
        &lt;ApolloClient&gt;
          {children}
        &lt;/ApolloClient&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<h3>Next.jsにユーザー情報を表示させるページを追加</h3>
<p>次に以下のコマンドを実行し、ユーザー情報を表示させるページのファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/app/users
$ cd src/app/users
$ touch page.tsx
$ cd ../../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript"><code>import User from "../../components/User"

export default function Users() {
    return (
        &lt;User /&gt;
    );
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にNext.jsのサーバーを起動後、ブラウザで「http://localhost:3000/users」にアクセスし、以下のようにGraphQLからユーザー情報を取得して表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-7-1024x632.png" alt="" width="760" height="469" class="aligncenter wp-image-19557" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-7-1024x632.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-7-300x185.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-7-768x474.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-7-1536x947.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/apollo-7.png 1790w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はNext.jsのv15でApolloのGraphQLを試す方法についてご紹介しました。</p>
<p>おそらく<strong><span style="color: #ff0000;">中規模以上のフロントエンド開発をする際などはGraphQLを使った方がよいこともあると思う（逆に小規模開発では学習コストが高い）</span></strong>ので、基本的な部分は知っておくとよさそうです。</p>
<p>これからGraphQLを試したい方は、ぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>各種SNSなど</strong></p>
<p>各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします！</p>
<ul>
<li> <a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">X（旧Twitter）</a></li>
<li> <a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">YouTube</a></li>
</ul>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/install-apollo-in-next-js-v15-and-try-graphql">Next.js v15にApolloを導入してGraphQLを試す！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/install-apollo-in-next-js-v15-and-try-graphql/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>【無料】Cloudflare WorkersでAPIを定期実行できるぞ！</title>
		<link>https://tomoyuki65.com/how-to-use-cloudflare-workers</link>
					<comments>https://tomoyuki65.com/how-to-use-cloudflare-workers#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Thu, 10 Oct 2024 17:37:32 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19471</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 イベント駆動型の処理を作って定期実行させたくて色々調べていましたが、なんと無料で使えて使いやすそうなCloudflare Workersというの...</p>
The post <a href="https://tomoyuki65.com/how-to-use-cloudflare-workers">【無料】Cloudflare WorkersでAPIを定期実行できるぞ！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-29.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-19525" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-29.png 672w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-29-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p><strong>イベント駆動型の処理を作って定期実行</strong>させたくて色々調べていましたが、なんと<span style="color: #3366ff;"><strong>無料</strong></span>で使えて使いやすそうな<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Cloudflare Workers</strong></span>というのを見つけました！</p>
<p>例えば有名なやつだと<strong><span style="color: #ff0000;">AWSのLambda</span></strong>や<strong><span style="color: #ff0000;">Google CloudのCloud Functions</span></strong>などがあったりしますが、どちらも利用するには<strong><span style="color: #ff0000;">クレジットカード登録が必要</span></strong>だったりして結構ハードルが高いです。</p>
<p>ただ<strong><span style="color: #3366ff;">Cloudflare Workersならクレジットカードが不要でアカウントの作成</span></strong>ができ、<span style="color: #3366ff;"><strong>処理を定期実行させるのも簡単</strong></span>なので<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>結構おすすめ</strong></span>だと思います。</p>
<p>そのでこの記事では、Cloudflare Workersの使い方についてご紹介します。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>【無料】Cloudflare WorkersでAPIを定期実行できるぞ！</h2>
<p>まずはアカウント作成をするため、<a href="https://www.cloudflare.com/ja-jp/">Cloudflareの公式サイト</a>にアクセスし、画面中央の「無料で始める」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-1-1024x640.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19476" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-1-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-1-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-1-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-1-1536x959.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-1-2048x1279.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次にメールアドレスとパスワードを入力後、ロボットでないことを確認し、「サインアップ」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-2-1024x639.png" alt="" width="760" height="474" class="aligncenter wp-image-19477" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-2-1024x639.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-2-300x187.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-2-768x479.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-2-1536x959.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-2-2048x1278.png 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>これで入力したメールアドレス宛に確認メールが送信されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-3-1024x642.jpg" alt="" width="760" height="476" class="aligncenter wp-image-19478" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-3-1024x642.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-3-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-3-768x481.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-3-1536x963.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-3-2048x1284.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次に送信されたメール内容に記載のリンクをクリックし、認証処理を行います。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-4-1024x641.png" alt="" width="760" height="475" class="aligncenter wp-image-19479" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-4-1024x641.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-4-300x188.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-4-768x481.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-4-1536x962.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-4.png 1964w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>メール認証後、Cloudflareの画面に戻り、画面右上のメニューから「アカウントホーム」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-5-2-1024x639.png" alt="" width="755" height="471" class="aligncenter wp-image-19527" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-5-2-1024x639.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-5-2-300x187.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-5-2-768x479.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-5-2-1536x959.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-5-2-2048x1278.png 2048w" sizes="auto, (max-width: 755px) 100vw, 755px" />
<p>&nbsp;</p>
<p>これでCloudflareのアカウント作成が完了です。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-6-1024x640.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19481" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-6-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-6-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-6-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-6-1536x960.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-6-2048x1280.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<h2>Cloudflare Workers用CLIツールのインストールとログイン</h2>
<p>次にCloudflare WorkersのCLIツールをインストールしますが、<strong><span style="border-bottom: 2px solid #be3144;">今回はnpm用のライブラリを例として使います。</span></strong></p>
<p>尚、この記事ではnpmに関する詳細は割愛させていただきますので、まだインストールしてない方はvoltaなどのパッケージ管理ライブラリを利用してnodeのインストールおよびnpmを利用できるようにして下さい。</p>
<p>では以下のコマンドを実行し、Cloudflare WorkersのCLIツール「wrangler」をインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm i -g wrangler</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、wranglerコマンドのパスが通っているかを確認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ wrangler --version</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、上記で作成したCloudflareアカウントにログインします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ wrangler login</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで認証用の画面が表示されるので、画面下の「Allow」をクリックして認証します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-7-1024x641.jpg" alt="" width="760" height="476" class="aligncenter wp-image-19485" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-7-1024x641.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-7-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-7-768x481.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-7-1536x961.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-7-2048x1281.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>これでCLIツールからのログインが完了です。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-8-1024x640.png" alt="" width="760" height="475" class="aligncenter wp-image-19486" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-8-1024x640.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-8-300x187.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-8-768x480.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-8-1536x959.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-8-2048x1279.png 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>ログイン完了後にターミナルを確認すると質問をされますが、「no」を入力しておけばOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-9.png" alt="" width="978" height="284" class="aligncenter size-full wp-image-19489" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-9.png 978w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-9-300x87.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-9-768x223.png 768w" sizes="auto, (max-width: 978px) 100vw, 978px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>CLIツールでWorker（ワーカー）を作成</h2>
<p>次にWorker（ワーカー）とよばれるアプリケーションを作成します。</p>
<p>一応画面からも作れますが、<span style="color: #3366ff;"><strong>CLIツールから作った方が色々便利</strong></span>なため、以下のコマンドを実行して各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir cf &amp;&amp; cd cf
$ npm create cloudflare@latest</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、パッケージをインストールするか聞かれるので「y」を入力します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-10.png" alt="" width="762" height="274" class="aligncenter wp-image-19491" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-10.png 545w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-10-300x108.png 300w" sizes="auto, (max-width: 762px) 100vw, 762px" />
<p>&nbsp;</p>
<p>次に作成するアプリケーションについて色々聞かれるので、今回は以下のように入力や選択を行います。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-11-771x1024.jpg" alt="" width="760" height="1010" class="aligncenter wp-image-19492" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-11-771x1024.jpg 771w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-11-226x300.jpg 226w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-11-768x1020.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-11-1156x1536.jpg 1156w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-11.jpg 1302w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>・Step 1 of 3</p>
<table style="border-collapse: collapse; width: 100%;">
<tbody>
<tr>
<td style="width: 68.766404%; text-align: left;"><span style="font-size: 10pt;">In which directory do you want to create your application?</span></td>
<td style="width: 31.233596%; text-align: left;"><span style="font-size: 10pt;">sample</span></td>
</tr>
<tr>
<td style="width: 68.766404%; text-align: left;"><span style="font-size: 10pt;">What would you like to start with?</span></td>
<td style="width: 31.233596%; text-align: left;"><span style="font-size: 10pt;">Hello World example</span></td>
</tr>
<tr>
<td style="width: 68.766404%; text-align: left;"><span style="font-size: 10pt;">Which template would you like to use?</span></td>
<td style="width: 31.233596%; text-align: left;"><span style="font-size: 10pt;">Hello World Worker</span></td>
</tr>
<tr>
<td style="width: 68.766404%; text-align: left;"><span style="font-size: 10pt;">Which language do you want to use?</span></td>
<td style="width: 31.233596%; text-align: left;"><span style="font-size: 10pt;">TypeScript</span></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><span>・Step 2 of 3</span></p>
<table style="border-collapse: collapse; width: 100%;">
<tbody>
<tr>
<td style="width: 68.766404%; text-align: left;"><span style="font-size: 10pt;">Do you want to use git for version control?</span></td>
<td style="width: 31.233596%; text-align: left;"><span style="font-size: 10pt;">yes</span></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>・Step 1 of 3</p>
<table style="border-collapse: collapse; width: 100%; height: 14px;">
<tbody>
<tr style="height: 14px;">
<td style="width: 68.766404%; text-align: left; height: 14px;"><span style="font-size: 10pt;">Do you want to deploy your application?</span></td>
<td style="width: 31.233596%; text-align: left; height: 14px;"><span style="font-size: 10pt;">no</span></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>これで「sample」フォルダが新しく作成されます。合わせてコマンド「cd sample」でsampleディレクトリに移動します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-12-2.png" alt="" width="768" height="552" class="aligncenter size-full wp-image-19494" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-12-2.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-12-2-300x216.png 300w" sizes="auto, (max-width: 768px) 100vw, 768px" />
<p>&nbsp;</p>
<p>「sample」フォルダをテキストエディタで確認すると以下のようになります。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-13-1024x763.png" alt="" width="760" height="566" class="aligncenter wp-image-19495" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-13-1024x763.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-13-300x224.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-13-768x572.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-13-1536x1145.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-13.png 2026w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカルサーバーを起動して確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ wrangler dev</code></pre>
</div>
<p>&nbsp;</p>
<p>ローカルサーバー起動後、以下のように表示されるので、「b」を入力するとブラウザが開きます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-14.png" alt="" width="960" height="438" class="aligncenter size-full wp-image-19496" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-14.png 960w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-14-300x137.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-14-768x350.png 768w" sizes="auto, (max-width: 960px) 100vw, 960px" />
<p><span style="color: #ff0000;">※起動したサーバーを止めたい場合は「x」を入力して下さい。</span></p>
<p>&nbsp;</p>
<p>ブラウザで「Hello World!」が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-15.png" alt="" width="980" height="586" class="aligncenter size-full wp-image-19497" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-15.png 980w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-15-300x179.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-15-768x459.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-15-486x290.png 486w" sizes="auto, (max-width: 980px) 100vw, 980px" />
<p>&nbsp;</p>
<h2>トリガー起動時に実行される処理を追加して試す</h2>
<p>次にトリガー起動時に実行される処理を追加するため、「src/index.ts」を以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/index.ts"><code>/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `npm run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `npm run deploy` to publish your worker
*
* Bind resources to your worker in `wrangler.toml`. After adding bindings, a type definition for the
* `Env` object can be regenerated with `npm run cf-typegen`.
*
* Learn more at https://developers.cloudflare.com/workers/
*/

// 環境変数の定義
interface Env {
    ENV: string;
}

export default {
    async fetch(request, env, ctx): Promise&lt;Response&gt; {
        return new Response('Hello World!');
    },
    // トリガー起動用の処理を追加
    async scheduled(
        controller: ScheduledController,
        env: Env,
        ctx: ExecutionContext,
    ) {
        console.log("トリガー起動により実行されました！！");
        console.log(`ENV: ${env.ENV}`);
    },
} satisfies ExportedHandler&lt;Env&gt;;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカル環境用の環境変数ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch .dev.vars</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".dev.vars"><code>ENV=local</code></pre>
</div>
<p>&nbsp;</p>
<p>次にトリガーを起動させる条件を「wrangler.toml」に追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="wrangler.toml"><code>・・

# トリガー設定
[triggers]
crons = ["0 22 * * *"]

・・</code></pre>
</div>
<p><strong><span style="color: #ff0000;">※トリガー設定についてはcron形式で記述し、上記の「[&#8220;0 22 * * *&#8221;]」についてはJST時間（日本時間）で毎朝7時に起動する設定です。（UTC時間に+9時間するとJST時間となる）</span></strong></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、トリガーテスト用のオプション「&#8211;test-scheduled」を付けてサーバーを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ wrangler dev --test-scheduled</code></pre>
</div>
<p>&nbsp;</p>
<p>サーバー起動後、ログに記載されているURLを確認し、末尾に「/__scheduled」を追加したURL（下記の場合は「http://localhost:8787/__scheduled」）をブラウザから開きます。</p>
<img loading="lazy" decoding="async" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-16-1024x544.png" alt="" width="760" height="404" class="aligncenter wp-image-19503" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-16-1024x544.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-16-300x159.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-16-768x408.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-16.png 1122w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>ブラウザからURL「http://localhost:8787/__scheduled」を開くと下図のようになり、これでトリガー起動の処理が実行されます。</p>
<img loading="lazy" decoding="async" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-17-1024x529.png" alt="" width="760" height="393" class="aligncenter wp-image-19504" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-17-1024x529.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-17-300x155.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-17-768x397.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-17.png 1026w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>もう一度ターミナルを確認し、トリガー起動時の処理に追加したログ出力が表示されればOKです。</p>
<img loading="lazy" decoding="async" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-18-1024x541.png" alt="" width="760" height="402" class="aligncenter wp-image-19505" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-18-1024x541.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-18-300x159.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-18-768x406.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-18.png 1124w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>本番環境用の環境変数設定とデプロイ</h2>
<p>次に上記で作成したファイルを本番環境へデプロイしてみます。</p>
<p>まずは以下のコマンドを実行し、本番環境用の環境変数を設定します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ wrangler secret put ENV</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、環境変数「ENV」の値の入力を求められるので、今回は「prod」と入力してEnterを押します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-19.png" alt="" width="765" height="248" class="aligncenter wp-image-19506" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-19.png 734w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-19-300x97.png 300w" sizes="auto, (max-width: 765px) 100vw, 765px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、本番環境へデプロイします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ npm run deploy</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、ターミナルは以下のようになります。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-20.png" alt="" width="818" height="456" class="aligncenter size-full wp-image-19508" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-20.png 818w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-20-300x167.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-20-768x428.png 768w" sizes="auto, (max-width: 818px) 100vw, 818px" />
<p>&nbsp;</p>
<p>次にCloudflareの画面を開き、画面左のメニュー「Workers &amp; Pages」をクリックするとデプロイしたアプリケーションが表示されるので、「sample」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-21-1024x643.jpg" alt="" width="760" height="477" class="aligncenter wp-image-19510" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-21-1024x643.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-21-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-21-768x482.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-21-1536x964.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-21-2048x1285.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次にアプリ「sample」の詳細画面が表示されます。</p>
<p>環境変数やトリガー設定の確認をするため、タブの「設定」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-22-1024x641.jpg" alt="" width="760" height="476" class="aligncenter wp-image-19511" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-22-1024x641.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-22-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-22-768x481.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-22-1536x962.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-22-2048x1282.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>次に設定画面が表示され、上記で設定した環境変数やトリガーイベントが想定通り設定されていればOKです。あとはトリガーが起動するまで待って下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-23-1024x643.jpg" alt="" width="760" height="477" class="aligncenter wp-image-19512" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-23-1024x643.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-23-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-23-768x482.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-23-1536x964.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-23-2048x1285.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<h2>本番環境でトリガー起動の確認</h2>
<p>トリガーに設定した時間経過後、アプリ「sample」のメトリクス画面を確認し、下図のように想定通りトリガーが起動していればOKです。</p>
<p>そして、タブ「ログ」をクリックしてログ出力を確認します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-24-1024x557.jpg" alt="" width="761" height="414" class="aligncenter wp-image-19514" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-24-1024x557.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-24-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-24-768x418.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-24-1536x836.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-24-2048x1115.jpg 2048w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p>&nbsp;</p>
<p>ログ出力を確認し、テストした時と同様に想定通りのログが出力されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-25-1024x576.png" alt="" width="760" height="428" class="aligncenter wp-image-19515" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-25-1024x576.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-25-300x169.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-25-768x432.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-25.png 1280w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<h3>トリガーを手動実行したい場合</h3>
<p>また、トリガーを手動実行させたい場合は、アプリ詳細画面の右上にある「コードの編集」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-30-1024x640.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19530" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-30-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-30-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-30-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-30-1536x961.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-30-2048x1281.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>これでコード編集画面が開き、右側にあるタブ「スケジュール」を選択後、「スケジューリングされたイベントをトリガーする」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-31-1024x640.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19531" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-31-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-31-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-31-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-31-1536x960.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-31-2048x1280.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>これでトリガーを手動実行できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-32-1024x643.jpg" alt="" width="760" height="477" class="aligncenter wp-image-19532" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-32-1024x643.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-32-300x188.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-32-768x482.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-32-1536x964.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-32-2048x1286.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p><span style="color: #ff0000;">※トリガーを手動実行した場合、アプリ詳細画面にあるタブ「ログ」では出力されないようなので、その点はご注意下さい。</span></p>
<p>&nbsp;</p>
<h2>トリガーの削除</h2>
<p><strong><span style="color: #ff0000;">上記では日本時間の毎朝7時にトリガーを実行するように設定した</span></strong>ので、<strong><span style="color: #ff0000;">そのままだと明日以降も起動</span></strong>してしまいます。</p>
<p>そんなトリガー設定を消したい場合は、アプリの設定画面を開き、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>対象のトリガーのゴミ箱アイコンをクリック</strong></span>します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-26-1024x636.jpg" alt="" width="763" height="474" class="aligncenter wp-image-19517" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-26-1024x636.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-26-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-26-768x477.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-26-1536x955.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-26-2048x1273.jpg 2048w" sizes="auto, (max-width: 763px) 100vw, 763px" />
<p>&nbsp;</p>
<p>トリガー削除のポップアップが表示されるので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「削除」をクリック</strong></span>します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-27-1024x638.jpg" alt="" width="760" height="474" class="aligncenter wp-image-19518" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-27-1024x638.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-27-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-27-768x479.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-27-1536x957.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-27-2048x1276.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>トリガーイベントの設定が消えればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-28-1024x640.jpg" alt="" width="760" height="475" class="aligncenter wp-image-19519" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-28-1024x640.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-28-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-28-768x480.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-28-1536x960.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-28-2048x1279.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<h2>招待されたアカウントの方でアプリをデプロイする方法について</h2>
<p>Cloudflareを実務で使う場合は、自分の個人アカウントではなく、<strong><span style="color: #ff0000;">招待された方のアカウントでデプロイ作業等</span></strong>が必要になると思います。</p>
<p>その場合、Cloudflareにログイン後のURLか、「Workers &amp; Pages」の概要ページなどに表示されている<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>アカウントID</strong></span>を使います。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-33-1024x736.jpg" alt="" width="760" height="546" class="aligncenter wp-image-19535" srcset="https://tomoyuki65.com/wp-content/uploads/2024/10/cw-33-1024x736.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-33-300x216.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-33-768x552.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-33-1536x1104.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/10/cw-33-2048x1471.jpg 2048w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p>&nbsp;</p>
<p>アカウントIDを確認後、それを「wrangler.toml」に設定すれば、対象のアカウントに対してデプロイ等が可能です。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="wrangler.toml"><code>・・

# デプロイ先のアカウントIDを設定
account_id = "XXXXXXXXXXX"

・・</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はCloudflare Workersの使い方について解説しました。</p>
<p>Cloudflare Workersなら<span style="color: #3366ff;"><strong>クレカ不要で簡単に登録して無料プランを利用することができ、処理の定期実行も簡単に行うことが可能</strong></span>です。</p>
<p>イベント駆動型の処理を作って定期実行させたい方がいたら、ぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>各種SNSなど</strong></p>
<p>各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします！</p>
<ul>
<li> <a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">X（旧Twitter）</a></li>
<li> <a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">YouTube</a></li>
</ul>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-use-cloudflare-workers">【無料】Cloudflare WorkersでAPIを定期実行できるぞ！</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-use-cloudflare-workers/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Go言語（Golang）専門の技術ブログ「Golang-Tech」を開設！？</title>
		<link>https://tomoyuki65.com/notice-of-establishment-of-golang-tech</link>
					<comments>https://tomoyuki65.com/notice-of-establishment-of-golang-tech#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Thu, 26 Sep 2024 12:06:09 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19463</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 少し前になりますが、Googleが開発したプログラミング言語である「Go言語（Golang）」をはじめとし、その関連技術を専門的に取り扱う技術ブ...</p>
The post <a href="https://tomoyuki65.com/notice-of-establishment-of-golang-tech">Go言語（Golang）専門の技術ブログ「Golang-Tech」を開設！？</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/09/go240926-1.png" alt="" width="672" height="480" class="size-full wp-image-19469 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/09/go240926-1.png 672w, https://tomoyuki65.com/wp-content/uploads/2024/09/go240926-1-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>少し前になりますが、<span>Googleが開発したプログラミング言語である</span><strong>「<span style="border-bottom: 2px solid #be3144;">Go言語（Golang）</span>」</strong>をはじめとし、その関連技術を専門的に取り扱う技術ブログ<a href="https://golang.tomoyuki65.com" target="_blank" rel="noopener">「Golang-Tech」</a>を開設しました！</p>
<p><span style="color: #3366ff;"><strong>最近ようやくコンテンツが増えてきた</strong></span>ので、改めてこちらでもお知らせいたします！</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Go言語（Golang）専門の技術ブログ「Golang-Tech」を開設！？</h2>
<h3>Golang-Techで取り扱う内容について</h3>
<p>主な内容としては、Go言語（Golang）やGCP（Google Cloud Platform）などのバックエンドエンジニア向けの技術を中心に、最近話題の生成AI関連としてVertexAIやGeminiについても取り扱う予定です。</p>
<p>&nbsp;</p>
<h3>更新頻度について</h3>
<p>最近ようやくコンテンツが増えてきましたが、私がGolangに関する技術のインプットしたことについて、Golang-Techのコンテンツとしてアプトプットしていくため、<span style="color: #ff0000;"><strong>基本的な更新頻度は劇遅</strong></span>です！笑</p>
<p>少しずつですがコンテンツは増やしていくので、コンテンツが増えていった際にはぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はGo言語（Golang）専門の技術ブログ「Golang-Tech」についてご紹介しました。</p>
<p>Go言語については一部で人気があるものの、まだまだそれほど普及しているわけではないので、ネットに公開されている情報も少ないです。</p>
<p>ただこれからもっと需要は増える可能性があるので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Golang-Techを通じてGoに関する魅力的なコンテンツを提供</strong></span>できればと思います。</p>
<p>よければぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<div class="supplement boader">
<p style="text-align: center;"><span style="color: #808080;">＼ Go言語専門の技術ブログはこちら ／</span></p>
<div class="btn-wrap aligncenter rich_blue"><img loading="lazy" decoding="async" src="//ad.jp.ap.valuecommerce.com/servlet/gifbanner?sid=3371598&amp;pid=886856626" width="1" height="1" border="0" /><a href="https://golang.tomoyuki65.com">&gt;&gt; Golang-Tech</a></div>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/notice-of-establishment-of-golang-tech">Go言語（Golang）専門の技術ブログ「Golang-Tech」を開設！？</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/notice-of-establishment-of-golang-tech/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Laravel11の変更点を踏まえてバックエンドAPIを開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-develop-api-with-laravel11</link>
					<comments>https://tomoyuki65.com/how-to-develop-api-with-laravel11#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Mon, 20 May 2024 13:19:51 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19331</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 以前Laravel10でバックエンドAPI開発をする方法について記事を書きましたが、2024年3月にLaravel11がリリースされたので試して...</p>
The post <a href="https://tomoyuki65.com/how-to-develop-api-with-laravel11">Laravel11の変更点を踏まえてバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-12-min.png" alt="" width="672" height="480" class="size-full wp-image-19375 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-12-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-12-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://twitter.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>以前Laravel10でバックエンドAPI開発をする方法について記事を書きましたが、2024年3月に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Laravel11</strong></span>がリリースされたので試してみました。</p>
<p>この記事では以前書いた記事を参考にしつつ試した部分について、変更点を踏まえてAPIを開発する方法をまとめておきます。</p>
<p>&nbsp;</p>
<p><strong>Laravelの関連記事</strong><span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></span></p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/how-to-develop-api-with-laravel10"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2023/12/laravel10top-486x290.png" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Laravel10（PHP）でバックエンドAPIを開発する方法まとめ【OpenAPI仕様書・管理画面カスタマイズ】</p><span class="date gf">2023年12月29日</span></div></a></div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Laravel11の変更点を踏まえてバックエンドAPIを開発する方法まとめ</h2>
<p>まずは以下のコマンドを実行し、プロジェクトの作成を行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir laravel11
$ cd laravel11
$ curl -s "https://laravel.build/api?with=mysql"| bash</code></pre>
</div>
<p><span style="color: #ff0000;">※ディレクトリ名はapiとし、不要なものはインストールさせないようmysqlだけ指定しています。尚、コマンド実行にはDockerを使える必要があるため、まだの方は事前にご準備下さい。</span></p>
<p>&nbsp;</p>
<p><strong><span style="color: #ff0000;">最後にOSなどに設定しているパスワードを聞かれる</span></strong>ので、入力して完了させて下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-1.png" alt="" width="1244" height="122" class="size-full wp-image-19335 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-1.png 1244w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-1-300x29.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-1-1024x100.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-1-768x75.png 768w" sizes="auto, (max-width: 1244px) 100vw, 1244px" />
<p>&nbsp;</p>
<p>パスワード入力後、以下のようにプロジェクトが作成されれば完了です。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-2.png" alt="" width="1788" height="1264" class="size-full wp-image-19337 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-2.png 1788w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-2-300x212.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-2-1024x724.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-2-768x543.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-2-1536x1086.png 1536w" sizes="auto, (max-width: 1788px) 100vw, 1788px" />
<p>&nbsp;</p>
<h2>環境変数ファイル「.env」を修正</h2>
<p>次に環境変数用のファイル「.env」を次のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/.env"><code>・・・

APP_TIMEZONE=Asia/Tokyo

・・・

APP_LOCALE=ja
APP_FALLBACK_LOCALE=ja
APP_FAKER_LOCALE=ja_JP

・・・

APP_MAINTENANCE_STORE=file

・・・

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=l11_db
DB_USERNAME=l11_user
DB_PASSWORD=l11_password
DB_COLLATION=utf8mb4_bin

SESSION_DRIVER=file

・・・

QUEUE_CONNECTION=sync

CACHE_STORE=file

・・・</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テストDB用の環境変数ファイル「.env.testing」を作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ cp .env .env.testing</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイル「.env.testing」について、以下の項目をテスト用に修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/.env.testing"><code>・・・

APP_KEY=

・・・

DB_CONNECTION=mysql_test
DB_HOST=db_test
DB_PORT=3307
DB_DATABASE=l11_db_test
DB_USERNAME=l11_user_test
DB_PASSWORD=l11_password_test
DB_COLLATION=utf8mb4_bin

・・・</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<h2>DBの接続設定を修正</h2>
<p>次にDBの接続設定を修正するため、ファイル「api/config/database.php」を以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="api/config/database.php"><code>・・・

    'connections' =&gt; [

        ・・・

        'mysql' =&gt; [
            'driver' =&gt; 'mysql',
            'url' =&gt; env('DB_URL'),
            'host' =&gt; env('DB_HOST', '127.0.0.1'),
            'port' =&gt; env('DB_PORT', '3306'),
            'database' =&gt; env('DB_DATABASE', 'laravel'),
            'username' =&gt; env('DB_USERNAME', 'root'),
            'password' =&gt; env('DB_PASSWORD', ''),
            'unix_socket' =&gt; env('DB_SOCKET', ''),
            'charset' =&gt; env('DB_CHARSET', 'utf8mb4'),
            'collation' =&gt; env('DB_COLLATION', 'utf8mb4_unicode_ci'),
            'prefix' =&gt; '',
            'prefix_indexes' =&gt; true,
            'strict' =&gt; true,
            'engine' =&gt; null,
            'options' =&gt; extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA =&gt; env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        // テスト用DB
        'mysql_test' =&gt; [
            'driver' =&gt; 'mysql',
            'url' =&gt; env('DB_URL'),
            'host' =&gt; env('DB_HOST', '127.0.0.1'),
            'port' =&gt; env('DB_PORT', '3307'),
            'database' =&gt; env('DB_DATABASE', 'laravel'),
            'username' =&gt; env('DB_USERNAME', 'root'),
            'password' =&gt; env('DB_PASSWORD', ''),
            'unix_socket' =&gt; env('DB_SOCKET', ''),
            'charset' =&gt; env('DB_CHARSET', 'utf8mb4'),
            'collation' =&gt; env('DB_COLLATION', 'utf8mb4_unicode_ci'),
            'prefix' =&gt; '',
            'prefix_indexes' =&gt; true,
            'strict' =&gt; true,
            'engine' =&gt; null,
            'options' =&gt; extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA =&gt; env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

・・・</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にテスト実行用としてファイル「api/phpunit.xml」に「&lt;env name=&#8221;DB_CONNECTION&#8221; value=&#8221;mysql_test&#8221;/&gt;」を追加し、DB_DATABASE設定を「&lt;env name=&#8221;DB_DATABASE&#8221; value=&#8221;l11_db_test&#8221;/&gt;」に修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/phpunit.xml"><code>・・・
    
　　　&lt;php&gt;
        &lt;env name="APP_ENV" value="testing"/&gt;
        &lt;env name="APP_MAINTENANCE_DRIVER" value="file"/&gt;
        &lt;env name="BCRYPT_ROUNDS" value="4"/&gt;
        &lt;env name="CACHE_STORE" value="array"/&gt;
        &lt;env name="DB_CONNECTION" value="mysql_test"/&gt;
        &lt;env name="DB_DATABASE" value="l11_db_test"/&gt;
        &lt;env name="MAIL_MAILER" value="array"/&gt;
        &lt;env name="PULSE_ENABLED" value="false"/&gt;
        &lt;env name="QUEUE_CONNECTION" value="sync"/&gt;
        &lt;env name="SESSION_DRIVER" value="array"/&gt;
        &lt;env name="TELESCOPE_ENABLED" value="false"/&gt;
    &lt;/php&gt;

・・・</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>独自のDocker環境を構築</h2>
<p>次にLaravel11ではsailコマンドでDocker環境を構築できますが、実務を考慮して独自のDocker環境を構築するため、まずは以下のコマンドを実行して「docker-compose.yml」のファイル名を修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mv docker-compose.yml docker-compose.sail.yml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、各種ディレクトリやファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch compose.yml
$ mkdir docker
$ cd docker
$ mkdir local
$ cd local
$ mkdir php
$ mkdir mysql
$ cd mysql
$ touch my.cnf
$ cd ../php
$ touch Dockerfile
$ touch 000-default.conf
$ touch php.ini
$ cd ../../..</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成した各種ファイルについて、下記のようにそれぞれ記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/compose.yml"><code>services:
  api:
    build:
      context: .
      dockerfile: ./docker/local/php/Dockerfile
      container_name: api
    volumes:
      - .:/app
    ports:
      - '80:8080'
    depends_on:
      - db
      - db_test
  db:
    image: mysql:8.0.36
    container_name: db
    environment:
      MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
      MYSQL_ROOT_HOST: "%"
      MYSQL_DATABASE: '${DB_DATABASE}'
      MYSQL_USER: '${DB_USERNAME}'
      MYSQL_PASSWORD: '${DB_PASSWORD}'
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
    volumes:
      - db-data:/var/lib/mysql
      - ./docker/local/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - 3306:3306
  db_test:
    image: mysql:8.0.36
    container_name: db_test
    environment:
      MYSQL_ROOT_PASSWORD: 'l11_user_password'
      MYSQL_ROOT_HOST: "%"
      MYSQL_DATABASE: 'l11_db_test'
      MYSQL_USER: 'l11_user_test'
      MYSQL_PASSWORD: 'l11_user_password'
      MYSQL_TCP_PORT: 3307
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
    volumes:
      - ./docker/local/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    tmpfs:
      - /var/lib/mysql
    ports:
      - 3307:3307
    expose:
      - 3307
volumes:
  db-data:
    driver: local</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/docker/local/mysql/my.cnf"><code>[Date]
date.timezone = "Asia/Tokyo"

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin

[client]
default-character-set=utf8mb4</code></pre>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/docker/local/php/Dockerfile"><code>FROM php:8.3-apache

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_NO_INTERACTION 1
ENV COMPOSER_HOME /composer

WORKDIR /app

RUN apt-get update &amp;&amp; apt-get install -y \
    libzip-dev \
    &amp;&amp; docker-php-ext-install zip pdo_mysql

COPY --from=composer /usr/bin/composer /usr/bin/composer

COPY . /app
COPY ./docker/local/php/php.ini /usr/local/etc/php/php.ini
COPY ./docker/local/php/000-default.conf /etc/apache2/sites-available/000-default.conf

RUN composer install

RUN chmod 777 -R storage &amp;&amp; \
echo "Listen 8080" &gt;&gt; /etc/apache2/ports.conf &amp;&amp; \
a2enmod rewrite

CMD ["apache2-foreground"]</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/docker/local/php/000-default.conf"><code>&lt;VirtualHost *:8080&gt;
  ServerAdmin webmaster@localhost
  DocumentRoot /app/public/

  &lt;Directory /app/&gt;
    AllowOverride All
    Require all granted
  &lt;/Directory&gt;

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined
&lt;/VirtualHost&gt;</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/docker/local/php/php.ini"><code>zend.exception_ignore_args = off
expose_php = on
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL
display_errors = on
display_startup_errors = on
log_errors = on
error_log = /var/log/php/php-error.log
default_charset = UTF-8

[Date]
date.timezone = Asia/Tokyo

[mysqlnd]
mysqlnd.collect_memory_statistics = on

[Assertion]
zend.assertions = 1

[mbstring]
mbstring.language = Japanese</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※000-default.confやphp.iniは開発用の設定です。もし本番環境で使う際はカスタマイズして下さい。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナのビルドと起動を行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost」にアクセスし、下図のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-scaled.jpg" alt="" width="2560" height="1526" class="size-full wp-image-19353 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-scaled.jpg 2560w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-300x179.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-1024x611.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-768x458.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-1536x916.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-2048x1221.jpg 2048w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-3-486x290.jpg 486w" sizes="auto, (max-width: 2560px) 100vw, 2560px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>テスト用環境変数ファイル「.env.testing」のアプリケーションキーを設定</h2>
<p>次に以下のコマンドを実行し、テスト用の環境変数ファイル「.env.testing」のAPP_KEYを設定します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api php artisan key:generate --env=testing</code></pre>
</div>
<p>&nbsp;</p>
<h2>マイグレーションファイルの再作成</h2>
<p>次に<strong><span style="color: #ff0000;">既に作成されているマイグレーションファイルを全て削除</span></strong>し、以下のコマンドを実行して新しいユーザーテーブル用のマイグレーションファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api php artisan make:migration create_users_table</code></pre>
</div>
<p>&nbsp;</p>
<p>そして、新しく作成したマイグレーションファイルの中身は次のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="api/database/migrations/20XX_XX_XX_XXXXXX_create_users_table.php"><code>&lt;?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
    * Run the migrations.
    */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table-&gt;id();
            $table-&gt;string('uid')-&gt;unique();
            $table-&gt;integer('member_id')-&gt;unique()-&gt;nullable();;
            $table-&gt;string('last_name');
            $table-&gt;string('first_name');
            $table-&gt;string('email')-&gt;unique();
            $table-&gt;datetimes();
            $table-&gt;softDeletesDatetime();
            $table-&gt;unique(['email', 'deleted_at'], 'unique_email_deleted_at');
        });
    }

    /**
    * Reverse the migrations.
    */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<h2>既存のUserモデルの修正</h2>
<p>次は既に作成済みのUserモデルを以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="app/Models/User.php"><code>&lt;?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
    use HasFactory, Notifiable, SoftDeletes;

    // 日付フォーマットをY-m-d H:i:sに変更
    protected function serializeDate(\DateTimeInterface $date)
    {
        return $date-&gt;format('Y-m-d H:i:s');
    }

    protected $guarded = ['created_at', 'updated_at'];
}</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※ユーザーテーブルについて、実務では論理削除するのが基本になると思うので、その場合は「SoftDeletes」を使います。</span></p>
</div>
<p>&nbsp;</p>
<h2>API用のルーティングファイルを公開</h2>
<p>Laravel11からAPI用のルーティングファイルが非公開になっているため、以下のコマンドを実行して公開します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api php artisan install:api</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、最後に下図のようにマイグレーションを実行するか聞かれますが、noを入力してキャンセルし、追加されたマイグレーションファイルは今回は使わないので削除します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-4.png" alt="" width="1440" height="48" class="size-full wp-image-19359 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-4.png 1440w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-4-300x10.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-4-1024x34.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-4-768x26.png 768w" sizes="auto, (max-width: 1440px) 100vw, 1440px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>リポジトリパターンでAPIを作成する</h2>
<p>次にリポジトリパターンでAPIを作成するため、以下のコマンドを実行して各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p app/Repositories/User
$ mkdir -p app/Services
$ touch app/Repositories/User/UserRepository.php
$ touch app/Repositories/User/UserRepositoryInterface.php
$ touch app/Services/UserService.php</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルの中身は以下のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="UserRepositoryInterface.php"><code>&lt;?php

namespace App\Repositories\User;

use App\Models\User;

interface UserRepositoryInterface
{
    public function createUser(string $uid, string $last_name, string $first_name, string $email);
    public function saveUser(User $user);
    public function getAllUserWithTrashed();
    public function getUserFromUid(string $uid);
    public function deleteUser(User $user);
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="UserRepository.php"><code>&lt;?php

namespace App\Repositories\User;

use App\Models\User;

class UserRepository implements UserRepositoryInterface
{
    public function createUser(
        string $uid,
        string $last_name,
        string $first_name,
        string $email
    )
    {
        $user = new User();
        $user-&gt;uid = $uid;
        $user-&gt;last_name = $last_name;
        $user-&gt;first_name = $first_name;
        $user-&gt;email = $email;

        return $user;
    }

    public function saveUser(User $user)
    {
        $user-&gt;save();

        return $user;
    }

    public function getAllUserWithTrashed()
    {
        // 論理削除データも取得
        return User::withTrashed()-&gt;get();
    }

    public function getUserFromUid(string $uid)
    {
        return User::where('uid', $uid)-&gt;first();
    }

    public function deleteUser(User $user)
    {
        return $user-&gt;delete();
    }
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="UserService.php"><code>&lt;?php

namespace App\Services;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Repositories\User\UserRepositoryInterface as UserRepository;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Exceptions\HttpResponseException;
use \Symfony\Component\HttpFoundation\Response;

class UserService extends Controller
{
    public function __construct(UserRepository $userRepo)
    {
        $this-&gt;userRepositry = $userRepo;
    }

    public function createUser(Request $request)
    {
        try {
            DB::beginTransaction();

            $user = $this-&gt;userRepositry-&gt;createUser(
                        $request-&gt;uid,
                        $request-&gt;last_name,
                        $request-&gt;first_name,
                        $request-&gt;email
                    );

            $user = $this-&gt;userRepositry-&gt;saveUser($user);

            # 会員IDの設定（9桁）
            $user-&gt;member_id = $user-&gt;id + 100000000;

            $this-&gt;userRepositry-&gt;saveUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/createUserでエラー");
            throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()-&gt;json(['message' =&gt; 'OK'], Response::HTTP_CREATED);
    }

    public function getUsers(Request $request)
    {
        try {

            $users = $this-&gt;userRepositry-&gt;getAllUserWithTrashed();

        } catch (\Exception $e) {
            Log::error("UserService/getUsersでエラー");
            throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return $this-&gt;jsonResponse($users);
    }

    public function getUser(Request $request, string $uid)
    {
        try {

            $user = $this-&gt;userRepositry-&gt;getUserFromUid($uid);

        } catch (\Exception $e) {
            Log::error("UserService/getUserでエラー");
            throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return $this-&gt;jsonResponse($user);
    }

    public function updateUser(Request $request, string $uid)
    {
        try {
            DB::beginTransaction();

            $user = $this-&gt;userRepositry-&gt;getUserFromUid($uid);

            if (!is_null($request-&gt;name)) {
                $user-&gt;name = $request-&gt;name;
            }

            if (!is_null($request-&gt;email)) {
                $user-&gt;email = $request-&gt;email;
            }

            $this-&gt;userRepositry-&gt;saveUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/updateUserでエラー");
            throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()-&gt;json(['message' =&gt; 'OK']);
    }

    public function destroyUser(Request $request, string $uid)
    {
        try {
            DB::beginTransaction();

            $user = $this-&gt;userRepositry-&gt;getUserFromUid($uid);

            $this-&gt;userRepositry-&gt;deleteUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/destroyUserでエラー");
            throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()-&gt;json(['message' =&gt; 'OK']);
    }
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にjson出力時の日本語の文字化け対応をするため、共通処理として「api/app/Http/Controllers/Controller.php」に関数「jsonResponse」を追加して使えるようにしておきます。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="app/Http/Controllers/Controller.php"><code>&lt;?php

namespace App\Http\Controllers;

abstract class Controller
{
    public function jsonResponse($data, $code = 200)
    {
        return response()-&gt;json(
                   $data,
                   $code,
                   ['Content-Type' =&gt; 'application/json;charset=UTF-8', 'Charset' =&gt; 'utf-8'],
                   JSON_UNESCAPED_UNICODE
               );
    }
}</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※Laravel11からController.phpにあった「use Illuminate\Foundation\Auth\Access\AuthorizesRequests;」、「use Illuminate\Foundation\Validation\ValidatesRequests;」、「use Illuminate\Routing\Controller as BaseController;」が削除されました。abstract classに変わっているので直接インスタンス化できないのと、</span><span style="color: #ff0000;">「AuthorizesRequests」や「ValidatesRequests」を使いたい場合は、基本的には個別にコントローラーでuseする形になったようです。</span></p>
<p>&nbsp;</p>
<p>次に作成したサービスとリポジトリを使えるようにするため、「AppServiceProvider.php」の「register()」の部分に登録します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="app/Providers/AppServiceProvider.php"><code>&lt;?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
    * Register any application services.
    */
    public function register(): void
    {
        $this-&gt;app-&gt;bind('App\Services\UserService');
        $this-&gt;app-&gt;bind('App\Repositories\User\UserRepositoryInterface', 'App\Repositories\User\UserRepository');
    }

    /**
    * Bootstrap any application services.
    */
    public function boot(): void
    {
        //
    }
}</code></pre>
</div>
<p>&nbsp;</p>
<h3>ユーザーコントローラーの作成</h3>
<p>次に以下のコマンドを実行し、ユーザーコントローラーを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api php artisan make:controller Api/UserController</code></pre>
</div>
<p>&nbsp;</p>
<p>次にユーザーコントローラーの中身は次のように記述します。</p>
<p><span style="color: #ff0000;">※リポジトリパターンでは業務ロジックはサービスに寄せるため、コントローラーはスッキリした記述になります。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="app/Http/Controllers/Api/UserController.php"><code>&lt;?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\UserService;

class UserController extends Controller
{
    public function __construct(UserService $userService)
    {
        $this-&gt;userService = $userService;
    }

    public function create(Request $request)
    {
        return $this-&gt;userService-&gt;createUser($request);
    }

    public function users(Request $request)
    {
        return $this-&gt;userService-&gt;getUsers($request);
    }

    public function user(Request $request, string $uid)
    {
        return $this-&gt;userService-&gt;getUser($request, $uid);
    }

    public function update(Request $request, string $uid)
    {
        return $this-&gt;userService-&gt;updateUser($request, $uid);
    }

    public function delete(Request $request, string $uid)
    {
        return $this-&gt;userService-&gt;destroyUser($request, $uid);
    }
}</code></pre>
</div>
<p>&nbsp;</p>
<h3>ルーティングの追加</h3>
<p>次にユーザーコントローラーのルーティングを追加するため、「routes/api.php」を以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="api/routes/api.php"><code>&lt;?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\UserController;

// 今回は不要なのでコメントアウト
// Route::get('/user', function (Request $request) {
// return $request-&gt;user();
// })-&gt;middleware('auth:sanctum');

Route::prefix('v1')-&gt;group(function() {
    Route::post('/user', [UserController::class, 'create']);
    Route::get('/users', [UserController::class, 'users']);
    Route::get('/user/{uid}', [UserController::class, 'user']);
    Route::put('/user/{uid}', [UserController::class, 'update']);
    Route::delete('/user/{uid}', [UserController::class, 'delete']);
});</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<h3>PostmanでAPIの動作確認</h3>
<p>次にPostmanでAPIの動作確認をします。（細かい部分は割愛させていただきます。）</p>
<p>まずはユーザー作成のAPIを実行し、下図のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5.jpg" alt="" width="2546" height="1666" class="size-full wp-image-19362 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5.jpg 2546w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5-300x196.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5-1024x670.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5-768x503.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5-1536x1005.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-5-2048x1340.jpg 2048w" sizes="auto, (max-width: 2546px) 100vw, 2546px" />
<p>&nbsp;</p>
<p>次にユーザー取得APIを実行し、下図のように対象のユーザーを取得できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6.jpg" alt="" width="2544" height="1666" class="size-full wp-image-19363 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6.jpg 2544w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6-300x196.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6-1024x671.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6-768x503.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6-1536x1006.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-6-2048x1341.jpg 2048w" sizes="auto, (max-width: 2544px) 100vw, 2544px" />
<p>&nbsp;</p>
<p>次にユーザー情報更新APIを実行し、下図のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7.jpg" alt="" width="2542" height="1668" class="size-full wp-image-19364 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7.jpg 2542w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7-1024x672.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7-768x504.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7-1536x1008.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-7-2048x1344.jpg 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>次にもう一度ユーザー取得APIを実行し、ユーザー情報が更新されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8.jpg" alt="" width="2540" height="1664" class="size-full wp-image-19365 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8-1024x671.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8-768x503.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8-1536x1006.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-8-2048x1342.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にユーザー削除APIを実行し、下図のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9.jpg" alt="" width="2542" height="1672" class="size-full wp-image-19366 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9.jpg 2542w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9-1024x674.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9-768x505.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9-1536x1010.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-9-2048x1347.jpg 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>次にもう一度ユーザー取得APIを実行し、下図のようにデータが取得できなければOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10.jpg" alt="" width="2546" height="1668" class="size-full wp-image-19367 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10.jpg 2546w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10-1024x671.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10-768x503.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10-1536x1006.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-10-2048x1342.jpg 2048w" sizes="auto, (max-width: 2546px) 100vw, 2546px" />
<p>&nbsp;</p>
<p>次に全てのユーザー取得APIを実行し、対象ユーザーが表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11.jpg" alt="" width="2536" height="1664" class="size-full wp-image-19368 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11-1024x672.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11-768x504.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11-1536x1008.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-11-2048x1344.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>デフォルトでAuthServiceProviderが無い場合の認証機能の追加について</h2>
<p>Laravel10では認証機能の処理は「api/app/Providers/AuthServiceProvider.php」に追加しましたが、Laravel11ではデフォルトでこのファイルが存在しません。</p>
<p>Laravel11ではデフォルトで「api/app/Providers/AppServiceProvider.php」しかありませんが、このファイルに認証機能を追加しても同様に認証機能を動作させることができました。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP"><code>&lt;?php

namespaceApp\Providers;

use Illuminate\Support\ServiceProvider;

// Firebaseの認証機能用
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\Exceptions\HttpResponseException;
use \Symfony\Component\HttpFoundation\Response;
use Kreait\Firebase\Contract\Authas FirebaseAuth;
use App\Models\User;

classAppServiceProviderextendsServiceProvider
{

・・・
    /**
    * Bootstrap any application services.
    */
    public function boot(): void
    {
        // Firebaseによる認証
        Auth::viaRequest('firebase', function (Request $request) {
            $idToken = $request-&gt;header('Authorization');
            if (empty($idToken)) {
                throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Bad Request'], Response::HTTP_BAD_REQUEST));
            }

            $idToken = str_replace('Bearer ', '', $idToken);
            $firebaseAuth = app(FirebaseAuth::class);
            try {
                $verifiedIdToken = $firebaseAuth-&gt;verifyIdToken($idToken);
            } catch (\Exception $e) {
                throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Unauthorized'], Response::HTTP_UNAUTHORIZED));
            }

            $uid = $verifiedIdToken-&gt;claims()-&gt;get('sub');
            $email = $verifiedIdToken-&gt;claims()-&gt;get('email');

            // DBからユーザー情報取得（論理削除データも含む）
            $user = User::withTrashed()-&gt;where('uid', $uid)-&gt;first();

            if (!empty($user-&gt;deleted_at)) {
                throw new HttpResponseException(response()-&gt;json(['message' =&gt; 'Bad Request'], Response::HTTP_BAD_REQUEST));
            }

            if (empty($user)) {
                $user = new User();
                $user-&gt;uid = $uid;
                $user-&gt;email = $email;
            }

            return $user;
        });
    }
}</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※boot()の部分に追加したらLaravel10の時と同様に動作させることができました。</span></p>
<p>&nbsp;</p>
<h2>テストフレームワーク「Pest」の追加について</h2>
<p>今回の方法ではデフォルトのテストフレームワークとして「PHPUnit 」がインストールされていますが、「Pest」を導入することも可能です。</p>
<p>ただし、<strong><span style="color: #ff0000;">2024年5月時点ではバージョンの兼ね合いでライブラリのインストールにエラー</span></strong>がでたので、<strong><span style="color: #ff0000;">Pestを導入したい場合はPHPUnitのバージョンのダウングレードが必要</span></strong>です。</p>
<p>PHPUnitをダウングレードしたい場合は、「api/composer.json」の<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>phpunit/phpunitのバージョンを「10.5.17」に修正</strong></span>して下さい。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/composer.json"><code>・・・

    "require-dev": {
        "fakerphp/faker": "^1.23",
        "laravel/pint": "^1.13",
        "laravel/sail": "^1.26",
        "mockery/mockery": "^1.6",
        "nunomaduro/collision": "^8.0",
        "pestphp/pest-plugin-laravel": "^2.4",
        "phpunit/phpunit": "10.5.17",
        "spatie/laravel-ignition": "^2.4"
    },

・・・</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>修正後、以下のコマンドを実行して更新します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api composer update</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Pestのインストールと初期化を行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api composer require pestphp/pest-plugin-laravel --dev
$ docker compose exec api ./vendor/bin/pest --init</code></pre>
</div>
<p>&nbsp;</p>
<p><span>初期化の際に聞かれる質問については「no」を入力して進めればOKです。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-13.png" alt="" width="1320" height="354" class="size-full wp-image-19378 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-13.png 1320w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-13-300x80.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-13-1024x275.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-13-768x206.png 768w" sizes="auto, (max-width: 1320px) 100vw, 1320px" />
<p>&nbsp;</p>
<p><span>これで設定用ファイル「api/tests/Pest.php」が作成されるので、「Illuminate\Foundation\Testing\RefreshDatabase::class,」部分のコメントアウトを外し、テスト実行後にDBをリフレッシュさせる設定を有効にしておきます。</span></p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="api/tests/Pest.php"><code>・・・

uses(
    Tests\TestCase::class,
    Illuminate\Foundation\Testing\RefreshDatabase::class,
)-&gt;in('Feature');

・・・</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、テストを実行してみます。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api php artisan test</code></pre>
</div>
<p>&nbsp;</p>
<p><span>テスト実行後、既にあるサンプルファイルのテストが正常終了すればOKです。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-14.png" alt="" width="992" height="296" class="size-full wp-image-19380 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-14.png 992w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-14-300x90.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-14-768x229.png 768w" sizes="auto, (max-width: 992px) 100vw, 992px" />
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、ユーザーAPI用のテストファイルを作成します。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api php artisan pest:test UserTest</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次に作成したファイルの中身を次のように記述します。</span></p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-php" data-lang="PHP" data-file="api/tests/Feature/UserTest.php"><code>&lt;?php

use Illuminate\Support\Str;
use App\Models\User;

test('ユーザーが新規作成され、ステータス201で正常終了すること', function () {
    // API実行
    $path = "/api/v1/user";
    $uid = Str::random(10);
    $last_name = "test";
    $first_name = "taro";
    $email = "test-taro@example.com";
    $body = [
        'uid' =&gt; $uid,
        'last_name' =&gt; $last_name,
        'first_name' =&gt; $first_name,
        'email' =&gt; $email
    ];
    $response = $this-&gt;post($path, $body);

    // 検証
    expect($response-&gt;status())-&gt;toBe(201);
    $this-&gt;assertDatabaseHas(User::class, [
        'uid' =&gt; $uid,
        'last_name' =&gt; $last_name,
        'first_name' =&gt; $first_name,
        'email' =&gt; $email,
    ]);
});</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p><span>テストを実行し、以下のように全てのテストがpassすればOKです。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-15.png" alt="" width="1292" height="392" class="size-full wp-image-19381 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/l11-15.png 1292w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-15-300x91.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-15-1024x311.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/l11-15-768x233.png 768w" sizes="auto, (max-width: 1292px) 100vw, 1292px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はLaravel11でのAPI開発方法についてまとめました。</p>
<p>Laravel10と比べると<strong><span style="color: #3366ff;">ファイル数が減ってスッキリ</span></strong>しているので、以前よりも<strong><span style="color: #3366ff;">シンプルに開発を始められる</span></strong>のが良さそうです。</p>
<p>もちろん変わっている部分もありますが、<strong></strong>基本的には以前と同様に開発を進められるので、少しキャッチアップすれば問題なさそうです。</p>
<p>そんな感じで、これからLaravel11を使う方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>各種SNSなど</strong></p>
<p>各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします！</p>
<ul>
<li> <a href="https://twitter.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">X（旧Twitter）</a></li>
<li> <a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">YouTube</a></li>
</ul>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-develop-api-with-laravel11">Laravel11の変更点を踏まえてバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-develop-api-with-laravel11/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Rails7で最小構成からバックエンドAPIを開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-develop-an-api-from-a-minimal-configuration-with-rails7</link>
					<comments>https://tomoyuki65.com/how-to-develop-an-api-from-a-minimal-configuration-with-rails7#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Mon, 13 May 2024 14:47:58 +0000</pubDate>
				<category><![CDATA[プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=19266</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 私が最初に学び始めたフレームワークとしてはRailsですが、その後の実務ではLaravel（PHP）を使ってきました。 そんな私が久しぶりにRa...</p>
The post <a href="https://tomoyuki65.com/how-to-develop-an-api-from-a-minimal-configuration-with-rails7">Rails7で最小構成からバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-14-min.png" alt="" width="672" height="480" class="size-full wp-image-19327 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-14-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-14-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://twitter.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>私が最初に学び始めたフレームワークとしてはRailsですが、その後の実務ではLaravel（PHP）を使ってきました。</p>
<p>そんな私が<span style="color: #ff0000;"><strong>久しぶりにRailsを触ってみたいと思ったのと、合わせてRailsの最小構成からAPIを開発する方法を試したかった</strong></span>ので、やってみることにしました。</p>
<p>そこでこの記事では、私が久しぶりに試したRails7の最小構成でバックエンドAPIを開発する方法についてまとめておきます。</p>
<p>&nbsp;</p>
<p><strong>Railsの関連記事</strong><span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></span></p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/how-to-develop-a-web-application-with-spa"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2022/11/spa2211-1-min-486x290.png" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">SPA構成のWebアプリケーションを開発する方法まとめ【Docker・NextJS（React）・Vercel・Rails7（APIモード）・AWS ECS（Fargate）】</p><span class="date gf">2022年11月22日</span></div></a></div>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/how-to-customize-a-rails-tutorial-to-create-a-portfolio"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2022/08/pc220813-4-min-486x290.png" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Railsチュートリアルをカスタマイズしてポートフォリオを作成する方法【Docker・Rails7・CircleCI対応】</p><span class="date gf">2022年8月15日</span></div></a></div>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/rails-tutorial-6-reference-material"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2022/07/rt220716-1-min-486x290.png" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Railsチュートリアル（第6版）を進めるための参考資料</p><span class="date gf">2022年7月16日</span></div></a></div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Rails7で最小構成からバックエンドAPIを開発する方法まとめ</h2>
<p>まず以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir rails_sample
$ cd rails_sample
$ mkdir api
$ cd api
$ touch Gemfile
$ touch Gemfile.lock
$ touch .env
$ touch compose.yml
$ mkdir docker
$ cd docker
$ mkdir local
$ cd local
$ touch Dockerfile
$ cd ../..</code></pre>
</div>
<p>&nbsp;</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/Gemfile"><code>source "https://rubygems.org"
gem "rails"
</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※railsのバージョンを指定していないため、最新版がインストールされます</span></p>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/.env"><code>RAILS_ENV=development
BUNDLER_VERSION=2.5.10
LANG=C.UTF-8
TZ=Asia/Tokyo
PORT=3010</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/compose.yml"><code>services:
  api:
    container_name: rails-s-api
    build:
      context: .
      dockerfile: ./docker/local/Dockerfile
      args:
        - RAILS_ENV=${RAILS_ENV}
        - BUNDLER_VERSION=${BUNDLER_VERSION}
        - LANG=${LANG}
        - TZ=${TZ}
        - PORT=${PORT}
    volumes:
      - .:/api
    ports:
      - 3010:${PORT}
    env_file:
      - ./.env
    command: bundle exec puma -C "config/puma.rb"</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※環境変数は「.env」で設定し、それを「args:」からDockerfile内へ流しています</span></p>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/docker/local/Dockerfile"><code>## ビルドステージ
# 2024年5月時点の最新安定版Rubyの軽量版「alpine」
FROM ruby:3.3.1-alpine AS builder

# 環境変数
ARG RAILS_ENV
ARG BUNDLER_VERSION
ARG LANG
ARG TZ
ARG PORT

# インストール可能なパッケージ一覧の更新
RUN apk update &amp;&amp; \
    apk upgrade &amp;&amp; \
    # パッケージのインストール（ビルド時のみ使う）
    apk add --virtual build-packs --no-cache \
            alpine-sdk \
            build-base \
            curl-dev \
            mysql-dev \
            tzdata

# 作業ディレクトリの指定
WORKDIR /app

# ローカルにあるGemfileとGemfile.lockを
# コンテナ内のディレクトリにコピー
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

# bundlerのバージョンを固定する
RUN gem install bundler -v $BUNDLER_VERSION
RUN bundle -v

# bunlde installを実行する
RUN bundle install --jobs=4

# build-packsを削除
RUN apk del build-packs


## マルチステージビルド
# 2024年5月時点の最新安定版Rubyの軽量版「alpine」
FROM ruby:3.3.1-alpine

# 環境変数
ARG RAILS_ENV
ARG BUNDLER_VERSION
ARG LANG
ARG TZ
ARG PORT

# インストール可能なパッケージ一覧の更新
RUN apk update &amp;&amp; \
    apk upgrade &amp;&amp; \
    # パッケージのインストール（--no-cacheでキャッシュ削除）
    apk add --no-cache \
            bash \
            mysql-dev \
            tzdata \
            gvim \
            # rails newでgitを使用する（ファイル作成後は不要）
            git \
            # M系チップMacの場合に「gcompat」が必要
            gcompat

# 作業ディレクトリの指定
WORKDIR /api

# ビルドステージからファイルをコピー
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . /api

# サーバー起動
EXPOSE 3010
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※Dockerfileではマルチステージビルドで構築し、コンテナサイズの軽量化を図っています。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナのビルドおよびRailsファイルの作成を行います。<span style="color: #ff0000;">（ファイル作成時に.gitignoreは作成されて欲しいのでgit関連はスキップしていませんが、apiディレクトリ直下に.gitファイルは不要なので最後のコマンドで削除しています。）</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache
$ docker compose run --rm api rails new . -f -T -d mysql --api \
$ --skip-action-mailer --skip-action-mailbox --skip-action-text \
$ --skip-active-job --skip-active-storage --skip-action-cable \
$ --skip-asset-pipeline --skip-javascript --skip-hotwire --skip-jbuilder
$ rm -r .git</code></pre>
</div>
<p><span style="color: #ff0000;">※rails newコマンドのオプションについて、「-f」はファイルを上書き、「-T」はテストコード関連をスキップ、「-d mysql」はDBにMySQLを指定、「&#8211;api」はAPIモード（そのままだとCookieやCORS設定などが使えないので注意）、それ以降の&#8211;skip〜も不要なものをスキップ。尚、オプションを「-f -T -d mysql &#8211;api &#8211;minimal」にしてもほぼ同じ結果になりますが、この場合は「bootsnap」もスキップされます。</span></p>
<p>&nbsp;</p>
<p>成功すると以下のようにRailsの各種ファイルが作成されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-2.png" alt="" width="1768" height="1264" class="size-full wp-image-19276 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-2.png 1768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-2-300x214.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-2-1024x732.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-2-768x549.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-2-1536x1098.png 1536w" sizes="auto, (max-width: 1768px) 100vw, 1768px" />
<p>&nbsp;</p>
<p>次に一旦DBのMySQLの設定はスキップしたいため、一時的なDB設定として使えるnulldbのgem「activerecord-nulldb-adapter」をインストールします。</p>
<p>以下のようにファイル「api/Gemfile」の「group :development, :test do」の部分に「gem &#8216;activerecord-nulldb-adapter&#8217;」を追記します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/Gemfile"><code>・・

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ]

  # 一時的なDB設定用にnulldbのgemを追加
  gem "activerecord-nulldb-adapter"
end

・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次に「api/config/database.yml」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/config/database.yml"><code>・・

default: &amp;default
  # 一時的なDB設定としてnulldbを指定
  # adapter: mysql2
  adapter: nulldb
  encoding: utf8mb4
  pool: &lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;
  username: root
  password:
  host: localhost

・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次にコンテナを再ビルドし、コンテナを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3010」にアクセスし、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1.png" alt="" width="2578" height="1770" class="size-full wp-image-19272 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1.png 2578w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-300x206.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-1024x703.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-768x527.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-1536x1055.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-2048x1406.png 2048w" sizes="auto, (max-width: 2578px) 100vw, 2578px" />
<p>&nbsp;</p>
<h2>hello worldを出力するAPIを作成</h2>
<p>次にhello worldを試すため、テキスト「hello world !!」を出力するAPIを作成してみます。</p>
<p>ファイル「api/app/controllers/application_controller.rb」、「api/config/routes.rb」それぞれ以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/controllers/application_controller.rb"><code>class ApplicationController &lt; ActionController::API
  def hello
    render plain: "hello world !!"
  end
end</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/config/routes.rb"><code>Rails.application.routes.draw do

・・

  # API用のルーティング設定
  scope "api"do
    scope "v1"do
      get "/hello", to: "application#hello"
    end
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3010/api/v1/hello」にアクセスし、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-3.png" alt="" width="754" height="422" class="size-full wp-image-19286 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-3.png 754w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-3-300x168.png 300w" sizes="auto, (max-width: 754px) 100vw, 754px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>テストコード用にRSpecの設定</h2>
<p>次にテストを実施できるようにするため、RSpecを導入します。</p>
<p>以下のようにファイル「api/Gemfile」の「group :development, :test do」の部分にテスト関連のgemを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/Gemfile"><code>・・

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ]

  # 一時的なDB設定用にnulldbのgemを追加
  gem "activerecord-nulldb-adapter"

  # テスト用のGemを「group :development, :test」内に追加
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "shoulda-matchers"
end

・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、RSpecをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api bundle install
$ docker compose exec api rails g rspec:install</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次にRSpecの初期設定として「api/.rspec」に「–color」と「–format documentation」を追加します。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/.rspec"><code>--require spec_helper
--color
--format documentation</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テスト実行用コマンドのエイリアスを設定します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api bundle binstubs rspec-core</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次に不要なspecファイルが無駄に作られないようにするため、「api/config/application.rb」にジェネレータの設定を追加します。</span></p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/config/application.rb"><code>module Api
  class Application &lt; Rails::Application

・・・

  # ジェネレータの設定を追加
  config.generators do |g|
    g.test_framework :rspec,
      fixtures: false,
      helper_specs: false,
      view_specs: false,
      routing_specs: false
    end
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に<strong><span style="color: #ff0000;">環境変数に「RAILS_ENV=development」を設定した場合はRSpecの実行時の環境変数も「development」になってしまう</span></strong>ため、<span>「api/spec/rails_helper.rb」を以下のように修正します。</span></p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/spec/rails_helper.rb"><code>・・・

# RSpec実行時の環境変数「RAILS_ENV」の値が「test」になるように修正
# ENV['RAILS_ENV'] ||= 'test'
ENV['RAILS_ENV'] = 'test'

・・・</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、RSpecを実行して確認します。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api bin/rspec</code></pre>
</div>
<p><span style="color: #ff0000;">※binstubを設定していない場合はコマンド「bundle exec rspec」を使います。</span></p>
<p>&nbsp;</p>
<p><span>テスト実行後、以下のようにRSpecが実行できればOKです。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-4.png" alt="" width="900" height="166" class="size-full wp-image-19288 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-4.png 900w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-4-300x55.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-4-768x142.png 768w" sizes="auto, (max-width: 900px) 100vw, 900px" />
<p>&nbsp;</p>
<h2>RSpecでhello worldを出力するAPIのテストコードを作成</h2>
<p>次に上記で作成したAPIのテストコードを作成するため、以下のコマンドを実行してテストコード用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api rails g rspec:request application</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「api/spec/requests/applications_spec.rb」を以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/spec/requests/applications_spec.rb"><code>require 'rails_helper'

RSpec.describe "Applications", type: :request do
  # テスト用APIの検証
  describe "GET /api/v1/hello" do
    # レスポンスのステータス検証
    it "ステータス200で正常終了すること" do
      get hello_path
      expect(response).to have_http_status(:success)
    end

    # レスポンスの出力内容の検証
    it "「hello world !!」を出力すること" do
      get hello_path
      expect(response.body).to eq("hello world !!")
    end
  end
end</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次に<span>以下のコマンドを実行し、RSpecを実行してテスト結果を確認します。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api bin/rspec</code></pre>
</div>
<p>&nbsp;</p>
<p>テスト実行後、以下のような結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-5.png" alt="" width="896" height="282" class="size-full wp-image-19289 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-5.png 896w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-5-300x94.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-5-768x242.png 768w" sizes="auto, (max-width: 896px) 100vw, 896px" />
<p>&nbsp;</p>
<h2><span id="Rails7">Railsのタイムゾーンを日本に変更する</span></h2>
<p>次にRailsのタイムゾーンを日本（Asia/Tokyo）に変更するため<span>、「api/config/application.rb」に設定を追加します。</span></p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/config/application.rb"><code>module Api
  class Application &lt; Rails::Application

・・・

  # タイムゾーン設定を日本にする
  config.time_zone = "Asia/Tokyo"
  config.active_record.default_timezone = :local
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナを再起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>RailsのAPIモードでCookieを使えるようにする</h2>
<p><strong><span style="color: #ff0000;">RailsのAPIモードはデフォルトではCookieを使えない</span></strong>ため、使えるようにしておきます。</p>
<p>まずはファイル「api/Gemfile」でコメントアウトされている「gem &#8220;rack-cors&#8221;」の部分をコメントを外して有効にします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/Gemfile"><code>・・

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
gem "rack-cors"

・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、gemをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api bundle install</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「api/config/initializers/cors.rb」、「api/config/application.rb」、「api/app/controllers/application_controller.rb」をそれぞれ以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby"><code>・・・

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "http://localhost:3000"

    resource "/api/v1/*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※api/config/initializers/cors.rbはクロスオリジンに関する設定ファイルで、</span><span style="color: #ff0000;">originsにはフロントエンド側のドメインを設定します。</span></p>
<p>&nbsp;</p>
<p>次に「Cookies」を使えるようにするため<span>、「api/config/application.rb」に設定を追加します。</span></p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/config/application.rb"><code>module Api
  class Application &lt; Rails::Application

・・・

  # Cookiesを利用するための設定
  config.middleware.use ActionDispatch::Cookies
  config.action_dispatch.cookies_same_site_protection = :none
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次にファイル「api/app/controllers/application_controller.rb」で「ActionController::Cookies」読み込み、上記で作成したAPIでCookieを設定するよう修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby"><code>class ApplicationController &lt; ActionController::API
  # Cookiesを読み込む
  include ActionController::Cookies

  def hello
    # レスポンスにCookieを設定
    cookies[:hello] = {
      value: "hello",
      expires: 90.day.from_now,
      path: '/',
      httponly: true,
      secure: true
    }

    render plain: "hello world !!"
  end
end</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3010/api/v1/hello」にアクセスし、ブラウザの開発者用モードで「Application &gt; Cookies &gt; http://localhost:3010」を確認してCookieがあればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6.png" alt="" width="2446" height="986" class="size-full wp-image-19294 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6.png 2446w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6-300x121.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6-1024x413.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6-768x310.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6-1536x619.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-6-2048x826.png 2048w" sizes="auto, (max-width: 2446px) 100vw, 2446px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>DBとしてMySQLを追加する</h2>
<p>次にDBとしてMySQLを追加します。ファイル「api/.env」、「api/compose.yml」、「api/config/database.yml」をそれぞれ以下のように修正します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/.env"><code>・・・


MYSQL_ROOT_PASSWORD=password</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※api/.envにMySQLのルートパスワードの設定を追加します。</span></p>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/compose.yml"><code>services:
  api:
    container_name: rails-s-api
    build:
      context: .
      dockerfile: ./docker/local/Dockerfile
      args:
        - RAILS_ENV=${RAILS_ENV}
        - BUNDLER_VERSION=${BUNDLER_VERSION}
        - LANG=${LANG}
        - TZ=${TZ}
        - PORT=${PORT}
    volumes:
      - .:/api
    ports:
      - 3010:${PORT}
    env_file:
      - ./.env
    depends_on:
      - db
    command: bundle exec puma -C "config/puma.rb"
  # DBに関する設定
  db:
    container_name: rails-s-db
    image: mysql:8.0.36
    env_file:
      - ./.env
    ports:
      - 3306:3306
    volumes:
      - ./tmp/db:/var/lib/mysql</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※api/compose.ymlにDB用のコンテナ設定を追加します。</span></p>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/config/database.yml"><code>・・・

default: &amp;default
  # adapterを初期設定に戻す
  adapter: mysql2
  encoding: utf8mb4
  # コレクションを指定
  collation: utf8mb4_bin
  pool: &lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;
  # 以下をコメントアウト
  # username: root
  # password:
  # host: localhost

development:
  &lt;&lt;: *default
  database: api_development
  # DBへの接続情報を追加
  username: root
  password: &lt;%= ENV["MYSQL_ROOT_PASSWORD"] %&gt;
  host: db

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  &lt;&lt;: *default
  database: api_test
  # DBへの接続情報を追加
  username: root
  password: &lt;%= ENV["MYSQL_ROOT_PASSWORD"] %&gt;
  host: db

・・・</code></pre>
</div>
</div>
<p><span style="color: #ff0000;">※api/config/database.ymlにDBへの接続設定を追加修正します。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナを再起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、DBを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api rails db:create</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3010」にアクセスし、エラーにならず正常に表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1.png" alt="" width="2578" height="1770" class="size-full wp-image-19272 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1.png 2578w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-300x206.png 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-1024x703.png 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-768x527.png 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-1536x1055.png 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-1-2048x1406.png 2048w" sizes="auto, (max-width: 2578px) 100vw, 2578px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>ユーザーテーブルとデータを操作するAPIを作成する</h2>
<p>次はユーザーテーブルを作成し、データを操作するCRUD処理のAPIを作成してみます。</p>
<p>データ削除時は論理削除を行いたいため、まずはGemfileにgem「discard」を追加します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="api/Gemfile"><code>・・・

# 論理削除用
gem "discard"

</code>・・・</pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行してgemをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api bundle install</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ユーザーテーブル用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api rails g model User uid:string member_id:integer last_name:string first_name:string email:string discarded_at:datetime</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成されたマイグレーションファイルとモデルファイルについて、それぞれ以下のように修正します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/db/migrate/yyyymmddhhmmss_create_users.rb"><code>class CreateUsers &lt; ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :uid, null: false
      t.integer :member_id
      t.string :last_name, null: false
      t.string :first_name, null: false
      t.string :email, null: false

      t.timestamps
      t.datetime :discarded_at
    end
    add_index :users, :uid, unique: true
    add_index :users, :member_id, unique: true
    add_index :users, :email, unique: true
    add_index :users, :discarded_at
    add_index :users, [:email, :discarded_at], unique: true, name: 'unique_email_discarded_at'
  end
end</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/models/user.rb"><code>class User &lt; ApplicationRecord
  # 論理削除設定
  include Discard::Model
  default_scope -&gt; { kept }

  # バリデーション
  validates :uid, presence: true, uniqueness: true, length: { maximum: 255 }
  validates :member_id, uniqueness: true, length: { maximum: 9 }
  validates :last_name, presence: true, length: { maximum: 255 }
  validates :first_name, presence: true, length: { maximum: 255 }
  validates :email, presence: true, uniqueness: { scope: :discarded_at }, length: { maximum: 255 }

  # 日付項目のフォーマット変換
  def f_created_at
    created_at.strftime('%Y/%m/%d %H:%M:%S')
  end

  def f_updated_at
    updated_at.strftime('%Y/%m/%d %H:%M:%S')
  end

  def f_discarded_at
    if discarded_at
      discarded_at.strftime('%Y/%m/%d %H:%M:%S')
    else
      nil
    end
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、マイグレーションを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api rails db:migrate
$ docker compose exec api rails db:migrate RAILS_ENV=test</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、サービス層を構築するための各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ cd app
$ mkdir services
$ cd services
$ mkdir user
$ cd user
$ touch create_user_service.rb
$ touch get_a_user_service.rb
$ touch update_user_service.rb
$ touch delete_user_service.rb
$ touch get_users_with_discarded_service.rb
$ cd ../../..</code></pre>
</div>
<p><span style="color: #ff0000;">※業務ロジックを全てコントローラーに書くのはよくないので、サービス層を作ってコントローラーから呼び出すようにします。ただし、全てをサービス層に寄せるのもよくないので、処理の流れやDBのやり取りなどはサービス層に寄せつつ、コアなルールはモデルに寄せるようにした方がいいです。</span></p>
<p>&nbsp;</p>
<p>次に作成した各種サービスファイルをそれぞれ以下のように記述します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/services/user/create_user_service.rb"><code>class User::CreateUserService

  def self.call(...)
    new(...).call
  end

  def initialize(params)
    @params = params
  end

  def call
    @user = User.new(@params)

    # トランザクション
    ActiveRecord::Base.transaction do
      # 一時保存
      @user.save!

      # 会員IDの設定（9桁）
      @member_id = 100000000 + @user.id
      @user.member_id = @member_id
      @user.save!
    end

    # 戻り値
    @user

  rescue =&gt; e
    nil
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/services/user/get_a_user_service.rb"><code>class User::GetAUserService

  def self.call(...)
    new(...).call
  end

  def initialize(uid)
    @uid = uid
  end

  def call
    @user = User.where(uid: @uid).first
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/services/user/update_user_service.rb"><code>class User::UpdateUserService

  def self.call(...)
    new(...).call
  end

  def initialize(params, uid)
    @params = params
    @uid = uid
  end

  def call
    @user = User.where(uid: @uid).first

    # トランザクション
    ActiveRecord::Base.transaction do
      @user.update!(@params)
    end

    # 戻り値
    @user

  rescue =&gt; e
    nil
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/services/user/delete_user_service.rb"><code>class User::DeleteUserService

  def self.call(...)
    new(...).call
  end

  def initialize(uid)
    @uid = uid
  end

  def call
    ActiveRecord::Base.transaction do
      @user = User.where(uid: @uid).first
      @user.discard!
    end

    # 戻り値
    @user

  rescue =&gt; e
    nil
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/services/user/get_users_with_discarded_service.rb"><code>class User::GetUsersWithDiscardedService

  def self.call(...)
    new(...).call
  end

  def initialize()
  end

  def call
    @users = User.with_discarded
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナを再起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache 
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ユーザーコントローラーを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec api rails g controller users</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したユーザーコントローラーを次のように記述します。</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/app/controllers/users_controller.rb"><code>class UsersController &lt; ApplicationController
  def create
    # サービスを実行
    res = User::CreateUserService.call(create_params)

    # 実行結果によりレスポンスを設定
    if res
      render json: res, status: :created
    else
      render json: { message: "会員登録ができませんでした。" }, status: :internal_server_error
    end
  end

  def get_user
    # パスパラメータの取得
    uid = params[:uid]

    # サービスを実行
    user = User::GetAUserService.call(uid)

    # レスポンス設定
    render json: user
  end

  def update
    # パスパラメータの取得
    uid = params[:uid]

    # サービスを実行
    res = User::UpdateUserService.call(update_params, uid)

    # 実行結果によりレスポンスを設定
    if res
      render json: res
    else
      render json: { message: "会員情報の更新ができませんでした。" }, status: :internal_server_error
    end
  end

  def delete
    # パスパラメータの取得
    uid = params[:uid]

    # サービスを実行
    res = User::DeleteUserService.call(uid)

    # 実行結果によりレスポンスを設定
    if res
      render json: { message: "OK" }, status: :ok
    else
      render json: { message: "退会処理ができませんでした。" }, status: :internal_server_error
    end
  end

  def get_users_with_discarded
    # サービスを実行
    users = User::GetUsersWithDiscardedService.call()

    # レスポンス設定
    render json: users
  end

  private
    # ストロングパラメータ
    def create_params
      params.require(:user).permit(:uid, :last_name, :first_name, :email)
    end

    def update_params
      params.require(:user).permit(:last_name, :first_name, :email)
    end
end</code></pre>
</div>
</div>
</div>
<p><span style="color: #ff0000;">※Railsでは登録や更新時には対象の項目以外を更新させないようにするため、ストロングパラメータを使います。</span></p>
<p>&nbsp;</p>
<p>次にルーティングファイルにルートを追加します。</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="api/config/routes.rb"><code>Rails.application.routes.draw do

・・

  # API用のルーティング設定
  scope "api"do
    scope "v1"do
      get "/hello", to: "application#hello"

      post "/user", to: "users#create"
      get "/user/:uid", to: "users#get_user"
      put "/user/:uid", to: "users#update"
      delete "/user/:uid", to: "users#delete"
      get "/users/with-discarded", to: "users#get_users_with_discarded"
    end
  end
end</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>これでCRUD処理をするAPIの準備が整ったので、Postmanを使って試してみます。（Postmanについての詳細は割愛させていただきます。）</p>
<p>まずはユーザー作成用の「http://localhost:3010/api/v1/user」をPOSTで実行し、下図のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7.jpg" alt="" width="2540" height="1576" class="size-full wp-image-19315 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7-1024x635.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7-768x477.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7-1536x953.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-7-2048x1271.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にユーザー取得用の「http://localhost:3010/api/v1/user/XYZ123abc1」をGETで実行し、下図のようにユーザー情報が取得できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8.jpg" alt="" width="2542" height="1576" class="size-full wp-image-19316 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8.jpg 2542w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8-1024x635.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8-768x476.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8-1536x952.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-8-2048x1270.jpg 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>次にユーザー情報更新用の「http://localhost:3010/api/v1/user/XYZ123abc1」をPUTで実行し、下図のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9.jpg" alt="" width="2542" height="1582" class="size-full wp-image-19317 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9.jpg 2542w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9-1024x637.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9-768x478.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9-1536x956.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-9-2048x1275.jpg 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>次にもう一度ユーザー取得用の「http://localhost:3010/api/v1/user/XYZ123abc1」をGETで実行し、下図のように対象項目が更新されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10.jpg" alt="" width="2538" height="1580" class="size-full wp-image-19318 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10.jpg 2538w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10-1024x637.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10-768x478.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10-1536x956.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-10-2048x1275.jpg 2048w" sizes="auto, (max-width: 2538px) 100vw, 2538px" />
<p>&nbsp;</p>
<p>次にユーザーを論理削除する「http://localhost:3010/api/v1/user/XYZ123abc1」をDELETEで実行し、下図のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11.jpg" alt="" width="2542" height="1572" class="size-full wp-image-19319 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11.jpg 2542w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11-1024x633.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11-768x475.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11-1536x950.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-11-2048x1267.jpg 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>次にもう一度ユーザー取得用の「http://localhost:3010/api/v1/user/XYZ123abc1」をGETで実行し、対象データが存在せずnullが取得できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12.jpg" alt="" width="2536" height="1578" class="size-full wp-image-19320 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12-300x187.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12-1024x637.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12-768x478.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12-1536x956.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-12-2048x1274.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次に削除済みデータも含めて全てのユーザーを取得する「http://localhost:3010/api/v1/users/with-discarded」をGETで実行し、下図のようにユーザー情報が取得でき、discarded_atに削除日が設定されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13.jpg" alt="" width="2534" height="1668" class="size-full wp-image-19321 aligncenter" srcset="https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13-300x197.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13-1024x674.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13-768x506.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13-1536x1011.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2024/05/rails-13-2048x1348.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はRails7の最小構成でバックエンドAPIを開発する方法についてまとめました。</p>
<p>久しぶりに触ってみた感じ、やはり<span style="color: #3366ff;"><strong>Railsは色々なところが自動化されていて、使いやすいフレームワーク</strong></span>だなと改めて思いました。</p>
<p>また<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>私自身が実務経験を経てレベルアップしたこともあり、以前よりもいい感じでコードを書けるようになってきたのが実感</strong></span>できて良かったです。</p>
<p>過去にもRailsの記事は書いてきましたが、今回ご紹介した方法の方がシンプルでいい感じに作り始めることができると思うので、これから試す方はぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>各種SNSなど</strong></p>
<p>各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします！</p>
<ul>
<li> <a href="https://twitter.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">X（旧Twitter）</a></li>
<li> <a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">YouTube</a></li>
</ul>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-develop-an-api-from-a-minimal-configuration-with-rails7">Rails7で最小構成からバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-develop-an-api-from-a-minimal-configuration-with-rails7/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
