✏️
Nest.jsに関するTips
背景
ここ数ヶ月興味本位でNest.jsを使ってのAPI基板開発を始めまして、
その際、効率的な開発環境を整備するにはドキュメントに記載のないような一手間加える必要があったりだったので、
その辺りをユースケースに沿ってまとめてみる。
新たなTipsが発見された場合には随時更新していく予定である。
改訂履歴
- 2022/02/23 初版投稿
- 2022/02/24 typeorm-seedingの選定経緯を追記
前提
使用ツール
記述に関して
-
**
:任意のパス
Tips
rake task
のようなタスクランナーを構成したい
Railsの- 以下のように定義ファイルを記述し、
node
/ts-node
で実行する
Point
- testingモジュールを使用する
事前準備
こちらに沿って@nestjs/testing
をインストールする
実装
src/**/**.ts
import { Test } from '@nestjs/testing';
import { RequestMethod } from '@nestjs/common';
import { getRepository } from 'typeorm';
import { AppModule } from '~/app.module';
const bootstrap = async (): Promise<void> => {
// ***** Applicatioinモジュールの読み込み ***** //
const modules = Reflect.getMetadata('imports', AppModule);
const controllers = Reflect.getMetadata('controllers', AppModule);
const providers = Reflect.getMetadata('providers', AppModule);
const testingModule = await Test.createTestingModule({
imports: modules,
controllers: controllers,
providers: providers,
}).compile();
const app = await testingModule.createNestApplication();
// 前処理など
app.setGlobalPrefix('api', {
exclude: [{ path: 'healthz', method: RequestMethod.GET }],
});
// ***** Repositoryの読み込み ***** //
const repository = getRepository(/* <任意のEntity> */);
// ***** 行いたい処理 ***** //
// 明示的に終了させないと終了後にプロンプトが起動しない
process.exit(0);
};
bootstrap();
package.json(該当部のみ抜粋)
"scripts":{
"task": "yarn build && ts-node dist/tasks/**/**.js"
}
実行
$ yarn task
補足
- Applicatioinモジュールの読み込み
- Repositoryの読み込み
これらは別個の処理となるのでどちらかのみ(or どちらも未定義)でも動きます。
ormconfig
を一元管理する
- ormconfigをTSファイルとして定義し、これらを読み込むように各導線に設定を記述する
Point
- ormconfigをTSファイルとして定義
- 同ファイルにexportの導線を2つ用意
実装
ormconfig.ts
export const ormconfig = () => ({
database: {
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USER_NAME,
password: process.env.DB_PASSWORD,
database: process.env.NODE_ENV === 'test' ? 'database_test' : 'database',
entities: ['./dist/**/*.entity{.ts,.js}'],
migrations: ['./dist/**/migrations/*{.ts,.js}'],
seeds: ['./src/spec/seeds/**/*{.ts,.js}'],
factories: ['./src/spec/factories/**/*{.ts,.js}'],
synchronize: false,
connectTimeout: 30 * 1000,
logging: process.env.NODE_ENV === 'development',
cli: {
entitiesDir: './src/**',
migrationsDir: './src/migrations',
},
bigNumberStrings: false,
autoLoadEntities: process.env.NODE_ENV === 'test',
},
});
export default ormconfig().database;
app.module.ts(該当部のみ抜粋)
imports: [
forwardRef(() =>
ConfigModule.forRoot({
・・・
load: [ormconfig], // ConfigModuleで読み込む
}),
),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return configService.get('database'); // forRootAsyncで動的に取り込む
},
}),
...
]
package.json(該当部のみ抜粋)
"scripts":{
"typeorm": "npx ts-node ./node_modules/.bin/typeorm -f ./src/**/ormconfig.ts",
"migration:run": "yarn build && yarn typeorm migration:run",
}
実行
$ yarn migration:run
補足
-
ormconfig
= Typeormの設定ファイル - 普通に使うと、アプリケーションorCLIからとで読み込み階層などの制約がぶつかり、jsonファイルを分ける必要が出てくる
テストにて、RailsのFactoryBotのようなノリでケースを書きたい
-
typeorm-seedingを使用する
- ちなみに要件を満たすライブラリは他にも存在するが、2022/02/23時点で最新版が新しめかつFactoryBotに近い書き方ができるため採用している。(但しそれでも2021/09が最新リリースとなりあまり更新が活発で無さそうなのが懸念...)
Point
-
typeorm-seeding
はコミュニティ主導になる前のfaker.js
に依存してしまっている...- faker.jsについての経緯について詳しくはこちら
- そのため一部処理を最新版の
faker.js
にラップして使う
事前準備
以下インストール
-
typeorm-seeding
- コミュニティ主導後の
faker.js
実装
faker.jsをラップしたhelper
src/**/spec/helpers/**.helper.ts
import { ObjectType } from 'typeorm';
import { define as defineOrigin } from 'typeorm-seeding';
import { faker } from '@faker-js/faker';
import { instanceToPlain } from 'class-transformer';
export const define = <Entity, Context>(
Entity: ObjectType<Entity>,
factoryFn: (faker) => Entity,
): void => {
defineOrigin(Entity, (_faker): Entity => {
// typeorm-seedingに依存しているfakerのバージョンがdeprecateかつ使用できない関数があるため、
// 最新のバージョンでラップする
return factoryFn(faker);
});
};
Factory
src/**/spec/factories/**.ts
import { faker as fakerOrigin } from '@faker-js/faker';
import { define } from '**/helpers/**.helper';
import { User } from '**/**.entity'; // 例
define(User, (faker: typeof fakerOrigin): User => {
const user = new User();
user.firstName = faker.name.firstName();
user.lastName = faker.name.lastName();
user.email = faker.internet.email();
return user;
});
export default User;
Spec
**/**.spec.ts
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import {
factory,
tearDownDatabase,
useRefreshDatabase,
useSeeding,
} from 'typeorm-seeding';
import * as request from 'supertest';
import { AppModule } from '**/app.module';
import { User } from '**/**.entity';
describe('Users', () => {
let app: INestApplication;
beforeAll(async () => {
const modules = Reflect.getMetadata('imports', AppModule);
const controllers = Reflect.getMetadata('controllers', AppModule);
const providers = Reflect.getMetadata('providers', AppModule);
const moduleRef = await Test.createTestingModule({
imports: modules,
controllers: controllers,
providers: providers,
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
beforeEach(async () => {
// DBに接続&内部のデータをクリア
await useRefreshDatabase({
root: './src/configs/', // ormconfigの配置パスを指定(ここでは例として別項の設定が前提の記述にしている)
configName: 'ormconfig.ts', // ormconfigのファイル名を指定(ここでは例として別項の設定が前提の記述にしている)
});
// factories loaded
await useSeeding();
});
afterAll(async () => {
// 後処理
await app.close();
await tearDownDatabase();
});
it('GET /api/user', async () => {
const user = await factory(User)().create();
const serialize = (object: any): string => {
return JSON.stringify(instanceToPlain(object));
};
const res = await request(app.getHttpServer()).get('/user');
expect(res.status).toEqual(HttpStatus.OK);
expect(res.body).toEqual(JSON.parse(serialize(user)));
});
});
ormconfig.ts(該当部かつ差異のある部分のみ抜粋)
export const ormconfig = () => ({
database: {
...,
database: process.env.NODE_ENV === 'test' ? 'database_test' : 'database', // テスト実行時のdatabaseを指定、ここではNODE_ENVで環境を分けている
seeds: ['./src/**/spec/seeds/**/*{.ts,.js}'], // seedの機能も使用する場合は指定
factories: ['./src/**/spec/factories/**/*{.ts,.js}'], // factoryファイルのパス
autoLoadEntities: process.env.NODE_ENV === 'test', // trueにしないと動かない(理由は不明、、)
...
},
});
export default ormconfig().database;
後は通常のフローでテストを実行すればOK。
補足
- Nest.jsが標準でjestを使った機構を組み込んでいるので、jestを前提とした記述としている
-
typeorm-seeding
からimportする関数の引数としてfaker-jsのインスタンスが渡されるためラップ処理を行ったが、内部的に使用されているかは不明..- ただ基本デバッグ時にしか使用しない(と思っている)ので、本番運営を見越した場合の懸念にはならないと考えている
テスト時に認証処理をモック化させたい
Now writing...
後書き
Nest.jsはTS標準のため楽にTSの記述で書けるのが良いが、
ライブラリとして痒いところに手が届きにくい仕様となっているのが個人的に惜しいと思ったりしている。
上記のTipsに関してより良い手法や、そもそもこんなことしなくていいよみたいなのがもしあったらコメントいただけたら嬉しいです。
Discussion
ちょうどfactory/seeder実装していたので参考になります!!
下記ご存知だったら教えていただきたいです。
factoryを動かそうとしたところ、エラーが出まして・・・
configでは、dist/src/factories/**/*.{ts|js}みたいに定義しています。
コメントありがとうございます!
そう言っていただけて嬉しいです!
エラー文を拝見しまして、推測にはなってしまいますが、
コンパイル周りの設定に不備があるのかなと感じました。
(記事に関連する設定やtypeorm周りのエラーではなく)
自分もimport周りで不備が起きたことがあり、
その際はfrom以下を「相対パス」で指定したら直りまして、よろしければ併せてご参考にいただけたらと。
このような回答で力になれていたら幸いです。。
ありがとうございます!
docker環境なのでCOPYしたファイルのパスなど中心に確認してみます!