🐧

Remult + SvelteKitが良さげ

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

環境

  • macOS 13.6.8
  • node 22.0.0

手順

Svelteプロジェクトの構築

  • 任意のディレクトリで以下のコマンドで、Sveltekitプロジェクトを作成します。
# 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!
# 移動
cd sample
# パッケージインストール
npm i
# 開発サーバー起動確認。
npm run dev

Remultインストール

  • 次に以下のコマンドで、型安全なCRUD APIを構築するためにRemultをインストールします。
$ npm i remult --save-dev
  • 次にtsconfig.jsoncompilerOptionsに以下を追加します。
    • Remultではデコレーターベースでエンティティを書いていくため。
"experimentalDecorators": true

APIルートの作成

  • 次に以下のコマンドで、APIルートを作成するためのディレクトリおよびファイルを作成します。
    • Sveltekitではroutes配下に+serverファイルを作成することでAPIルートを定義できます。
# apiディレクトリの作成
mkdir src/routes/api
# [...remult]ディレクトリの作成
mkdir "src/routes/api/[...remult]"
# サーバーファイルの作成
touch "src/routes/api/[...remult]/+server.ts"
  • 作成した+server.tsを以下の内容に修正します。
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: 本に紐づくカテゴリ情報
  • 以下のコマンドで、定義するためのディレクトリおよびファイルを作成します。
mkdir src/shared
touch src/shared/Book.ts
touch src/shared/Category.ts
  • 作成後、まずCategory.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だけ等、細かく設定できる。
      • 他のオプションはこちら
    • @Field.タイプ: データの型。
    • Validators: API側でのバリデーション。
  • 次にBook.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ではこうしたデータベース間の関連性をシンプルなデコレータで簡単に定義できます。
      • 他のリレーションはこちら

エンティティの登録

  • Remultではエンティティを登録するだけでCRUDやページング等を備えたAPIが自動生成されます。
  • 上記で作成したroutes/api/[...remult]/+server.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が実行可能なことを確認します。
npm run dev
# データを入れていないので、まだ空
$ curl -X GET http://localhost:5173/api/books

[]

Swaggerの用意

  • 作成したAPIの「各種リクエストのテスト」「エンドポイント一覧やスキーマ・パラメータの確認」を簡単に行うべくSwaggerを利用します。
  • Remultには作成したAPIのOpen API Documentを自動生成する機能があるので、その機能で作成されたものをSwaggerに設定します。
  • まず以下のコマンドでswagger uiライブラリをインストールします。
npm i swagger-ui
npm i --save-dev @types/swagger-ui
  • 次にOpen API Documentをフロントエンドに渡すためにroutes/api/[...remult]/+server.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にアクセスできるようにします。
# 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を以下の内容に修正します。
import { _openApiDoc } from '../api/[...remult]/+server';

export const load = async () => {
	return {
    // svelteファイルでdata.apiDocのように利用できるようになる。
		apiDoc: _openApiDoc
	};
};
  • 次にsrc/routes/docs/+page.svelteを以下の内容に修正します。
<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

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

img

img

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

img

img

データ

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

フロントでの表示

  • ここまででエンティティ定義だけで簡単に最適なAPIを作成できました。
  • 最後にそのAPIをフロントエンドで利用して画面に表示することまで行います。
  • src/routes/+page.svelteファイルを以下の内容に修正します。
<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関連操作を行う。
      • 一覧はこちら
    • remult.repo(エンティティ名).find(): 取得。
      • デフォルトでは100件取得。
      • その他limitやwhereなど。
    • repo.find({include}): データに含めるリレーションエンティティ
    • remult.repo(エンティティ名).insert(): 追加。
  • 以下のような表示になり、タイトルやカテゴリを設定して登録できることを確認します。

img

  • 以上です。

まとめ

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

参考

Discussion