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 をアプリに登録しています。
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 送信し、レスポンスデータを返します。
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に登録するハンドラを実装している。
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>
`;
}
}
}
これは環境変数の設定ファイルになります。
PORT=3000
API_TOKEN=
API_BASE=http://api:8000
hostの許可を行いました。
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)接続情報を環境変数として定義しています。
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 を指定してルーティングを定義しています。
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 です。
{
"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
登録を行うと以下の画面が表示されます。
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 で公開するかどうかはまだ未定です。
Discussion