😸

TiDBローカル環境構築⑦

に公開

TiDBのローカル環境の構築を7回目行なっていきます。
前回TiDB環境のフロント構築が出来ましたが、今回はNestJS → Django API → TiDB に登録する構築を行っていきます。

コンテナ環境のツリー構成

追加ファイルはsrc/www/web/app/src/app.service.tsのみになります。

.
├── .env.api
├── .env.web
├── docker-compose.yml
└── src
    ├── infra
    │   └── db
    │       ├── Dockerfile
    │       ├── data
    │       │   └── test_db.sql
    │       ├── docker-compose.yml
    │       └── entrypoint.sh
    └── www
        ├── api
        │   ├── Dockerfile
        │   ├── docker-compose.yml
        │   ├── entrypoint.sh
        │   ├── project
        │   │   ├── core
        │   │   │   ├── __init__.py
        │   │   │   ├── admin.py
        │   │   │   ├── apps.py
        │   │   │   ├── migrations
        │   │   │   │   ├── 0001_initial.py
        │   │   │   │   └── __init__.py
        │   │   │   ├── models.py
        │   │   │   ├── serializers.py
        │   │   │   ├── urls.py
        │   │   │   └── views.py
        │   │   ├── manage.py
        │   │   └── project
        │   │       ├── __init__.py
        │   │       ├── asgi.py
        │   │       ├── settings.py
        │   │       ├── urls.py
        │   │       └── wsgi.py
        │   └── requirements.txt
        └── web
            ├── Dockerfile
            ├── app
            │   ├── package.json
            │   ├── src
            │   │   ├── app.controller.ts
            │   │   ├── app.module.ts
            │   │   ├── app.service.ts
            │   │   └── main.ts
            │   └── tsconfig.json
            ├── docker-compose.yml
            └── docker-entrypoint.sh

ソースコードを作成

フォームからのAPI処理を行います。


これは NestJS のルートモジュールで、HttpModule をインポートして HTTP 通信機能を有効化し、AppController と AppService をアプリに登録しています。

./src/www/web/app/src/app.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [HttpModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}



これは NestJS のサービスクラスで、HttpService を使って Django API (/api/customers/) に name と email を POST 送信し、レスポンスデータを返します。

./src/app.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

const DJANGO_API_BASE = process.env.API_BASE || 'http://api:8000';

@Injectable()
export class AppService {
  constructor(private readonly http: HttpService) {}

  async createCustomer(name: string, email: string) {
    const DJANGO_API_BASE = process.env.API_BASE || 'http://api:8000';
    const url = `${DJANGO_API_BASE}/api/customers/`;
    const response = await firstValueFrom(
      this.http.post<any>(url, { name, email }, { headers: { 'Content-Type': 'application/json' } })
    );
    return response.data;
  }
}



NestJSのコントローラで、トップにHTMLフォームを返し、POSTされた名前・メールをAppService経由でDjango APIへ送ってTiDBに登録するハンドラを実装している。

./src/app.controller.ts
import { Controller, Get, Header, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Header('Content-Type', 'text/html; charset=utf-8')
  getHello(): string {
    return `
      <!DOCTYPE html>
      <html>
        <head><meta charset="UTF-8"><title>Hello Nest</title></head>
        <body>
          <h1>Hello World from NestJS!</h1>
          <p>下のフォームから Django API 経由で TiDB に登録します。</p>
          <form method="POST" action="/submit">
            <div>
              <label>名前: <input name="name" required /></label>
            </div>
            <div>
              <label>メール: <input name="email" type="email" required /></label>
            </div>
            <button type="submit">登録</button>
          </form>
        </body>
      </html>
    `;
  }

  @Post('/submit')
  @Header('Content-Type', 'text/html; charset=utf-8')
  async submit(@Body('name') name: string, @Body('email') email: string) {
    try {
      const created = await this.appService.createCustomer(name, email);
      return `
        <html><body>
          <h2>登録完了</h2>
          <pre>${JSON.stringify(created, null, 2)}</pre>
          <p><a href="/">戻る</a></p>
        </body></html>
      `;
    } catch (e) {
      return `
        <html><body>
          <h2>登録失敗</h2>
          <pre>${String(e)}</pre>
          <p><a href="/">戻る</a></p>
        </body></html>
      `;
    }
  }
}



これは環境変数の設定ファイルになります。

.env.web
PORT=3000
API_TOKEN=
API_BASE=http://api:8000



hostの許可を行いました。

./src/www/api/project/project/settings.py
from pathlib import Path
import os

from django.db.backends.base.base import BaseDatabaseWrapper
BaseDatabaseWrapper.check_database_version_supported = lambda self: None

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = 'django-insecure-7$hqwi26z!jmm9-omhajuwy3rop&i)+&@4keai-*y9hzndz4ls'

DEBUG = True

ALLOWED_HOSTS = []

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'core',
]
# 追加
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost,api,web").split(",")

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 = 'project.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 = 'project.wsgi.application'

DATABASES = {
  "default": {
    "ENGINE": "django.db.backends.mysql",
    "NAME": os.getenv("DB_NAME"),
    "USER": os.getenv("DB_USER"),
    "PASSWORD": os.getenv("DB_PASSWORD"),
    "HOST": os.getenv("DB_HOST","tiup-playground"),
    "PORT": os.getenv("DB_PORT","4000"),
    "OPTIONS": {
      "charset": "utf8mb4",
      "init_command": "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci'",
    },
  }
}

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',
    },
]


LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_TZ = True

STATIC_URL = 'static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'



Django の .env 設定で、アプリのシークレットキーやデバッグ有効化、許可ホスト、MySQL(TiDB)接続情報を環境変数として定義しています。

.env.api
DJANGO_SECRET_KEY=devsecret
DJANGO_DEBUG=1
DJANGO_ALLOWED_HOSTS=*

DB_ENGINE=django.db.backends.mysql
DB_NAME=test
DB_USER=root
DB_PASSWORD=
DB_HOST=tiup-playground
DB_PORT=4000
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,api,web



Django REST Framework で CustomerViewSet を customers エンドポイントに紐付け、basename を指定してルーティングを定義しています。

./src/www/api/project/core/urls.py
from rest_framework.routers import DefaultRouter
from .views import CustomerViewSet

router = DefaultRouter()
router.register("customers", CustomerViewSet, basename="customers")

urlpatterns = router.urls



NestJS プロジェクトの依存関係に HTTP 通信ライブラリの axios と @nestjs/axios を追加した package.json です。

./src/www/web/app/package.json
{
  "name": "nest-hello-app",
  "version": "0.1.0",
  "private": true,
  "description": "Minimal NestJS Hello World (HTML) app for Docker bind mount",
  "license": "MIT",
  "main": "dist/main.js",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "start": "node dist/main.js",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
  },
  "dependencies": {
    "@nestjs/axios": "^4.0.1", # 追加
    "@nestjs/common": "^10.0.0",
    "@nestjs/core": "^10.0.0",
    "@nestjs/platform-express": "^10.0.0",
    "axios": "^1.11.0", # 追加
    "reflect-metadata": "^0.2.0",
    "rxjs": "^7.8.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@types/node": "^20.11.0",
    "prettier": "^3.2.0",
    "rimraf": "^5.0.5",
    "ts-node": "^10.9.2",
    "typescript": "^5.4.0"
  }
}

検証

ここまでで環境構築ができたので、この状態で以下のコマンドを実行します。

docker-compose up -d



コンテナが立ち上がりますが、Nestの立ち上がりに時間が多少かかります。
Docker Desktopで以下のログが出てくるまで待機します。

http://127.0.0.1:3000/にアクセスすると以下の画面が表示されます。



立ち上がったのが確認できたらDjangoの環境のapiコンテナにアクセスします。

docker exec -it ubuntu-dev-container sh



アクセス後以下のコマンドを実行します。

python manage.py runserver 0.0.0.0:8000


http://127.0.0.1:3000/
上記にアクセスしそれぞれの入力を行い登録を行います。



登録を行うと以下の画面が表示されます。



Djangoのサーバー
ログも以下のように出力されてます。

よく出たエラーと対処

  • Invalid HTTP_HOST header: 'api:8000' → ALLOWED_HOSTSにapi(web)を追加。環境変数で管理が楽。
  • Not Found: /api/api/customers/→ 二重に/apiをルーティングしていた。API_BASEを http://api:8000 にし、コード側で /api/customers/ を足す。
  • Router with basename "customer" is already registered.→ 同じ ViewSet を重複登録している。core/urls.py の router.register は1回だけにし、必要なら basename を変える。
  • Cannot find module '@nestjs/axios'→ npm install @nestjs/axios axios を忘れずに。firstValueFrom は <any> などで型付け。

まとめ

  • NestJSからDjangoREST APIを経由してTiDBにデータ登録する環境を構築。
  • NestJS側ではHttpModuleとHttpServiceを使ってフォーム送信データをDjango APIに POST。
  • Django側はDefaultRouterでCustomerViewSetを/api/customers/に紐付け、TiDBに接続。
  • 環境変数でALLOWED_HOSTSやDB接続情報を管理し、ホスト名エラーやルーティングミスを解消。
  • 依存関係に@nestjs/axiosとaxiosを追加し、firstValueFrom でレスポンスを取得。

以上で TiDB のローカル環境構築は完了です。
今後は TiDB に限らず、さまざまなことに取り組んでいきたいと思います。
ローカル環境構築に関しては、GitHub で公開するかどうかはまだ未定です。

コラボスタイル Developers

Discussion