Next.js + TypeScript で TypeORM を動かす

リンクラフトでエンジニアとして働いている渡辺です。リンクラフトアドベントカレンダーの9日目を担当します。

さて、Next.js いいですね。バックからフロントまで一貫して書けるのでプロジェクトがコンパクトになりました。
ここしばらく Next.js を使う機会が多かったので、DB にアクセスしてそれを表示するまでを簡単に紹介してみようかと思います。
JavaScript で DB にアクセスしようとすると一番メジャーなのは Prisma でしょうか？ただ、RDB を対象にするとちょっと役者不足が否めません。 やっぱり SQL と同じように書きたい。というわけで TypeORM を使用します。

環境は Next.js+TypeScript+TypeORM です。
RDB および Node.js の環境は別途用意してください。今回は MySQL を使用します。
DB にアクセスできる設定を整えて、ページで DB にアクセスを行い画面に表示するまでを目標とします。

各バージョンは以下の通りです。

- Next.js v15
- TypeORM v0.3.20
- Node.js v22.11.0
- MySQL v8.3

## プロジェクトの準備

まずプロジェクトを作成します。

```bash
npx create-next-app --typescript next-ts-typeorm
cd next-ts-typeorm
```

今回は`create-next-app`の時`import alias`だけYesにしています。

続けて必要なモジュールを追加します。

```bash
npm install typeorm reflect-metadata mysql dotenv
npm install -D ts-node tsx
```

`tsx`はシーダー用です。シーダーが不要ならインストール不要です。

## 設定ファイル

各設定ファイルに設定を追加していきます。

`tsconfig.json`の`compilerOptions`に以下を追加します。

```:tsconfig.json
"experimentalDecorators": true
```

`package.json`に以下を追加します。

```:package.json
"type": "module",
```

同じく`package.json`の`scripts`に以下を追加します。

```:package.json
"typeorm": "typeorm-ts-node-esm -d ./src/models/datasources/migrationDataSource.ts",
"migration:generate": "npm run typeorm migration:generate --pretty src/models/migrations/${npm_config_name}",
"migration:run": "npm run typeorm migration:run",
"seeder": "npx tsx ${npm_config_src}",
```

`npm run migration:generate --name=[マイグレーション名]`でマイグレーションファイルを作成し、`npm run migration:run`でマイグレーションを実行できるようにします。
`npm run seeder --src=[シーダーのファイル]`でシーダーを実行できるようにしますがオマケです。

`.env`ファイルを作成し DB の設定を追加します。各々の環境にあったものを設定してください。

```:.env
DATABASE_TYPE=mysql
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USERNAME=test_user
DATABASE_PASSWORD=hogehoge
DATABASE_NAME=test_db
```

## DB まわりの設定

Entity を作成します。
この Entity がデータベースの設定そのものになり、データを取得する際の基本的な単位になります。

```typescript:src/models/entities/User.ts
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({
    type: 'varchar',
    length: 10,
  })
  name!: string;

  @Column({
    type: 'varchar',
    length: 20,
  })
  address!: string;

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;
}
```

続いて DataSource の作成を行います。

```typescript:src/models/datasources/getOptions.ts
import { DataSourceOptions } from "typeorm";

export default function getOptions(): DataSourceOptions {
  return {
    type: process.env.DATABASE_TYPE,
    host: process.env.DATABASE_HOST,
    port: Number(process.env.DATABASE_PORT),
    username: process.env.DATABASE_USERNAME,
    password: process.env.DATABASE_PASSWORD,
    database: process.env.DATABASE_NAME,
  } as DataSourceOptions;
}
```

```typescript:src/models/datasources/migrationDataSource.ts
import { DataSource, DataSourceOptions } from 'typeorm';
import dotenv from 'dotenv';
import getOptions from './getOptions.js';

dotenv.config();

let options = getOptions();

export default new DataSource({
  ...options,
  entities: ['src/models/entities/*.ts'],
  migrations: ['src/models/migrations/*.ts'],
} as DataSourceOptions);
```

```typescript:src/models/datasources/executionDataSource.ts
import { DataSource, DataSourceOptions } from 'typeorm';
import dotenv from 'dotenv';
import getOptions from './getOptions';
import { User } from '../entities/User';

dotenv.config();

let options = getOptions();

export default async function executionDataSource() {
  const datasource = new DataSource({
    ...options,
    entities: [User],
  });

  if (!datasource.isInitialized) {
    await datasource.initialize();
  }

  return datasource;
}
```

DataSource から DB にアクセスしていきますが、マイグレーション用と実行用とで分けています。
Next.js で TypeORM を使ううえで一番大事な箇所です。ここさえ分かればあとは問題ないと言えます。

問題になるのが`entities: ['src/models/entities/*.ts'],`としているところで、この書き方は実行時にファイルを探しに行くかたちになります。マイグレーション時は`ts-node`を使用して実行するので問題ないのですが、Next.js 側から動かす場合は一度 JavaScript に変換し、そこから`.ts`のファイルを探しに行こうとしてしまうのでエラーになってしまいます。そのため実行用ではトランスパイル時にファイルが特定できるように`entities: [User],`のような書き方にしています。
Entity が増えるたびにメンテナンスを行う必要があるので少し面倒ですが、`entities/index.ts`を自動で用意できるのであれば`import * as Entities from '../entities'`から`entities: Object.values(Entities),`とすることでメンテナンスを不要にすることはできます。
今回は対象外とします。

また、プログラムから利用する場合は初期化が必要になるので、実行用は関数にして初期化処理を入れています。

Entity を作成した以下のコマンドを実行します。

```bash
npm run migration:generate --name=InitialSchema
```

`src/models/migrations`配下に`1731035020271-InitialSchema.ts`のようなファイルが生成されます。
中身は`src/models/entities/User.ts`の内容に対応した CREATE 文になっているはずです。
続いて以下のコマンドを実行します。

```bash
npm run migration:run
```

DB にテーブルが作成されていることを確認してください。

DB 設定の最後にシーダーを作成します。
シーダーはデフォルトでは提供されていません。
単純にデータを登録するだけの処理を書きます。

```typescript:src/models/seeders/01_users_seeder.ts
import { exit } from 'process';
import datasource from '../datasources/migrationDataSource';
import { User } from '../entities/User';
import { EntityManager } from 'typeorm';

const data = [
  { name: '名前1', address: '住所1' },
  { name: '名前2', address: '住所2' },
  { name: '名前3', address: '住所3' },
  { name: '名前4', address: '住所4' },
];

(async () => {
  if (!datasource.isInitialized) {
    await datasource.initialize();
  }

  await datasource.transaction(async (entityManager: EntityManager) => {
    const repo = entityManager.getRepository(User);
    await repo.createQueryBuilder().insert().into(User).values(data).execute();
  });

  exit(0);
})();
```

以下のコマンドを実行してください。

```bash
npm run seeder --src=src/models/seeders/01_users_seeder.ts
```

テーブルにデータが追加されていることを確認してください。

## 画面への表示

ページのファイルを作成します。

```typescript:src/app/test1/page.tsx
import datasource from '@/models/datasources/executionDataSource';
import { User } from '@/models/entities/User';

export default async function Test1() {
  const rep = (await datasource()).getRepository(User);
  const data = await rep.createQueryBuilder().getMany();

  return (
    <>
      <h1>Test1</h1>
      <table border={1}>
        <thead>
          <tr>
            <th>名前</th>
            <th>住所</th>
          </tr>
        </thead>
        <tbody>
          {data.map((datum, i) => (
            <tr key={i}>
              <td>{datum.name}</td>
              <td>{datum.address}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}
```

最後に以下コマンドでサーバを起動し、`http://localhost:3000/test1`を開いてください。

```bash
npm run dev
```

![20241121_next-ts-typeorm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940856/644f6a72-ea8e-c0ad-3fde-ac035a5a1750.png)

シーダーで登録したデータが表示されていれば完成です。

- - -

**一緒に働く仲間を募集中です！**

リンクラフト株式会社では、組織拡大に伴い積極的な採用活動を行っています。
少しでも興味がある方はぜひご連絡ください。

▽会社ホームページ
https://lincraft.co.jp/
▽Instagram
https://www.instagram.com/lincraft.inc/
▽ご応募はこちらより
https://lincraft.co.jp/recruit

※カジュアル面談も受付中です。ご希望の方はHPのお問い合わせフォームよりご連絡ください。