Remult + SvelteKitが良さげ

- Web開発において汎用性かつ機能性の高い最適なAPIの開発やドキュメントの作成は非常に重要です。
- 今回は「型安全なAPIの自動生成・容易なリアルタイムデータ同期」が可能な[Remult](https://remult.dev/)とFWの[Sveltekit](https://kit.svelte.dev/)を利用して素早くAPIやWebアプリを作成する方法を記録いたします。

## 環境
- macOS 13.6.8
- node 22.0.0

## 手順

### Svelteプロジェクトの構築
- 任意のディレクトリで以下のコマンドで、Sveltekitプロジェクトを作成します。

```shell
# svelteインストール
$ npm create svelte@latest sample

Need to install the following packages:
create-svelte@6.4.0
Ok to proceed? (y) y

create-svelte version 6.4.0

┌  Welcome to SvelteKit!
│
◇  Which Svelte app template?
│  Skeleton project
│
◇  Add type checking with TypeScript?
│  Yes, using TypeScript syntax
│
◇  Select additional options (use arrow keys/space bar)
│  Add ESLint for code linting, Add Prettier for code formatting
│
└  Your project is ready!
```

```shell
# 移動
cd sample
# パッケージインストール
npm i
# 開発サーバー起動確認。
npm run dev
```

### Remultインストール
- 次に以下のコマンドで、型安全なCRUD APIを構築するためにRemultをインストールします。

```shell
$ npm i remult --save-dev
```

- 次に`tsconfig.json`の`compilerOptions`に以下を追加します。
    - **Remultではデコレーターベースでエンティティを書いていくため。**

```ts
"experimentalDecorators": true
```

### APIルートの作成
- 次に以下のコマンドで、APIルートを作成するためのディレクトリおよびファイルを作成します。
    - **Sveltekitではroutes配下に`+server`ファイルを作成することでAPIルートを定義できます。**

```shell
# apiディレクトリの作成
mkdir src/routes/api
# [...remult]ディレクトリの作成
mkdir "src/routes/api/[...remult]"
# サーバーファイルの作成
touch "src/routes/api/[...remult]/+server.ts"
```

- 作成した`+server.ts`を以下の内容に修正します。

```js
import { remultSveltekit } from 'remult/remult-sveltekit'

// +serverファイルでexportできるのは「HTTP対応関数」「頭にアンダースコアがついたもの」のみ。
export const _api = remultSveltekit({})

// HTTPメソッド対応関数をエクスポートすることでAPIを作成できる。
export const { GET, POST, PUT, DELETE } = _api
```

### エンティティの定義
- セットアップは完了したので、次にエンティティを定義していきます。
- Remultではデータのエンティティ(スキーマ)ベースであり、この定義をもとにAPIのクエリやドキュメント等の自動生成を行います。
- 今回は以下を定義して本の管理を行えるものにします。
    - Book: 本の情報
    - Category: 本に紐づくカテゴリ情報
- 以下のコマンドで、定義するためのディレクトリおよびファイルを作成します。

```shell
mkdir src/shared
touch src/shared/Book.ts
touch src/shared/Category.ts
```

- 作成後、まず`Category.ts`の中身を以下の内容に修正します。

```ts
import { Entity, Fields, Validators } from 'remult';

@Entity('categories', {
	allowApiCrud: true
})
export class Category {
	@Fields.autoIncrement()
	id!: string;

	@Fields.string({
		validate: Validators.required
	})
	name: string = '';

	@Fields.createdAt()
	createdAt?: Date;

	@Fields.updatedAt()
	updatedAt?: Date;
}
```

- 上記のコードの簡単な説明は以下です。
    - @Entity('テーブル名'): DBのテーブルに対応します。
    - allowApiCrud: エンティティに対してCRUD操作を許可するか。
        - **READだけやDELETEだけ等、細かく設定できる。**
        - 他のオプションは[こちら](https://remult.dev/docs/ref_entity.html)
    - @Field.タイプ: データの型。
        - [一覧はこちら](https://remult.dev/docs/field-types)
    - Validators: API側でのバリデーション。
        - [詳細はこちら](https://remult.dev/docs/validation)

- 次に`Book.ts`の中身を以下の内容に修正します。

```ts
import { Entity, Fields, Relations, Validators } from 'remult';
import { Category } from './Category';

@Entity('books', {
	allowApiCrud: true
})
export class Book {
	@Fields.autoIncrement()
	id!: string;

	@Fields.string({
		validate: Validators.required
	})
	title: string = '';

	@Relations.toOne(() => Category)
	category?: Category;

	@Fields.createdAt()
	createdAt?: Date;

	@Fields.updatedAt()
	updatedAt?: Date;
}
```

- 上記のコードの簡単な説明は以下です。
    - @Relations.toOne(() => Entity名): 多対一のリレーションの定義。
        - Remultではこうしたデータベース間の関連性をシンプルなデコレータで簡単に定義できます。
        - 他のリレーションは[こちら](https://remult.dev/docs/entity-relations)

### エンティティの登録
- Remultではエンティティを登録するだけでCRUDやページング等を備えたAPIが自動生成されます。
- 上記で作成した`routes/api/[...remult]/+server.ts`を以下の内容に修正します。

```ts
import { remultSveltekit } from 'remult/remult-sveltekit';
import { Book } from '../../../shared/Book';
import { Category } from '../../../shared/Category';

export const _api = remultSveltekit({
  // エンティティティの登録
	entities: [Book, Category]
});

export const { GET, POST, PUT, DELETE } = _api;
```

- 登録後、以下のコマンドを実行してひとまずGET APIが実行可能なことを確認します。

```shell
npm run dev
```

```shell
# データを入れていないので、まだ空
$ curl -X GET http://localhost:5173/api/books

[]
```

### Swaggerの用意
- 作成したAPIの「各種リクエストのテスト」「エンドポイント一覧やスキーマ・パラメータの確認」を簡単に行うべくSwaggerを利用します。
- Remultには作成したAPIの**Open API Documentを自動生成する機能**があるので、その機能で作成されたものをSwaggerに設定します。
- まず以下のコマンドでswagger uiライブラリをインストールします。

```shell
npm i swagger-ui
npm i --save-dev @types/swagger-ui
```

- 次にOpen API Documentをフロントエンドに渡すために`routes/api/[...remult]/+server.ts`を以下の内容に修正します。

```ts
import { remultSveltekit } from 'remult/remult-sveltekit';
import { Book } from '../../../shared/Book';
import { Category } from '../../../shared/Category';

export const _api = remultSveltekit({
	entities: [Book, Category]
});

// 追加。アンダースコアをつけてフロントからimportできるようにする。
export const _openApiDoc = _api.openApiDoc({
	title: 'SAMPLE'
});

export const { GET, POST, PUT, DELETE } = _api;
```

- 次にswagger uiを表示するためのルーティングとして以下でディレクトリおよびファイルを作成します。
    - **今回は`localhost:5173/docs`にアクセスした時にswagger uiにアクセスできるようにします。**

```shell
# swagger uiパス
mkdir src/routes/docs
# indexページ
touch src/routes/docs/+page.svelte
# サーバーからのデータを取得してフロントで扱うためのファイル
touch src/routes/docs/+page.server.ts
```

- 次に`src/routes/docs/+page.server.ts`を以下の内容に修正します。

```ts
import { _openApiDoc } from '../api/[...remult]/+server';

export const load = async () => {
	return {
    // svelteファイルでdata.apiDocのように利用できるようになる。
		apiDoc: _openApiDoc
	};
};
```

- 次に`src/routes/docs/+page.svelte`を以下の内容に修正します。

```html
<script lang="ts">
	import { onMount } from 'svelte';
	import SwaggerUI from 'swagger-ui';
	import 'swagger-ui/dist/swagger-ui.css';
	import type { PageData } from './$types';

  // +page.server.tsからのデータ
	export let data: PageData;

	onMount(async () => {
		SwaggerUI({
      // remultで作成されたdocumentを設定
			spec: data.apiDoc,
			dom_id: '#swagger-ui-container'
		});
	});
</script>

<svelte:head>
	<title>SwaggerUI</title>
</svelte:head>

<div id="swagger-ui-container" />
```

- 修正後、`npm run dev`して`localhost:5173/docs`にアクセスしてswagger uiが表示されていることを確認します。

![img](https://github.com/user-attachments/assets/3a0b7cae-5ea0-483e-92ef-175764cd2c87)

- 以下で自動生成されたAPIは単純なCRUDだけでなくソートやページング等も備えていて、また自動生成されたOpen API Documentはスキーマやexampleの定義も行なっていることを確認します。

![img](https://github.com/user-attachments/assets/be3c5982-30de-473b-bd4e-cfc851f1e963)

![img](https://github.com/user-attachments/assets/24308b7b-d5a0-4ba3-b26f-005804925b0b)

- 以下でリクエストのテストも正常に行えることを確認します。

![img](https://github.com/user-attachments/assets/4a1c2135-c2fa-4dbf-bbe7-f3a9b64890d5)

![img](https://github.com/user-attachments/assets/93161c72-c445-4954-adac-c4d128655aa0)

### データ
- RemultではデフォルトのDBはローカルの`db/テーブル名.json`に格納される。
- DBはjsonだけでなく、もちろんMySQLやPostgresなどに対応していて[こちらを参考に設定可能。](https://remult.dev/docs/databases.html)
- つまり、開発環境ではプレーンなjsonファイルを利用して、本番環境はPostgresを利用するといったことが可能。

### フロントでの表示
- ここまででエンティティ定義だけで簡単に最適なAPIを作成できました。
- 最後にそのAPIをフロントエンドで利用して画面に表示することまで行います。
- `src/routes/+page.svelte`ファイルを以下の内容に修正します。

```html
<script lang="ts">
	import { remult } from 'remult';
	import { onMount } from 'svelte';
	import { Book } from '../shared/Book';
	import { Category } from '../shared/Category';

	let books: Book[] = [];
	let categories: Category[] = [];
	let newBookTitle = '';
	let selectedCategory: Category;

	onMount(async () => {
		[books, categories] = await Promise.all([
			// 本取得。リレーションのカテゴリもレスポンスに含める。
			remult.repo(Book).find({
				include: {
					category: true
				}
			}),
			// カテゴリ全取得
			remult.repo(Category).find()
		]);
	});

	// 登録
	const addBook = async () => {
		const newBook = await remult.repo(Book).insert({
			title: newBookTitle,
			category: selectedCategory
		});
		books = [...books, newBook];
		newBookTitle = '';
	};
</script>

<main>
	<form on:submit|preventDefault={addBook}>
		<input id="title" bind:value={newBookTitle} required />
		<select id="category" bind:value={selectedCategory}>
			<option value="">カテゴリを選択</option>
			{#each categories as category}
				<option value={category}>{category.name}</option>
			{/each}
		</select>
		<button type="submit">登録</button>
	</form>
	{#each books as book (book.id)}
		<p>{book.title} {book.category?.name || 'カテゴリなし'}</p>
	{/each}
</main>
```

- 上記のコードの簡単な説明は以下です。
    - remult.repo(エンティティ名): エンティティのCRUD操作。
        - リポジトリパターンを採用していて、repoを通してCRUD関連操作を行う。
        - 一覧は[こちら](https://remult.dev/docs/ref_repository.html)
    - remult.repo(エンティティ名).find(): 取得。
        - デフォルトでは100件取得。
        - その他limitやwhereなど。
    - repo.find({include}): データに含めるリレーションエンティティ
    - remult.repo(エンティティ名).insert(): 追加。

- 以下のような表示になり、タイトルやカテゴリを設定して登録できることを確認します。

![img](https://github.com/user-attachments/assets/9aea306b-047d-48de-9389-3287b9645735)

- 以上です。

## まとめ
- RemultのAPIやドキュメントの自動生成とSveltekitの洗練さによって、データやビジネスロジックに集中でき、より高速で堅牢なアプリケーションを構築できると感じました。

## 参考
- https://remult.dev/docs/
- https://kit.svelte.jp/docs/introduction