SvelteKit で JSON ベースの API を TypeScript で実装

SvelteKit で JSON ベースの API を TypeScript で実装する方法を紹介します。クライアントサイド、サーバーサイド両方の実装例を紹介していますが、特にサーバーサイドの TypeScript の型を中心に紹介します。

やりたいこと

SvelteKit で JSON ベースの API を作ろうとしています。JavaScript での実装は SvelteKit のチュートリアル API routes / GET handlers • Svelte Tutorial を見れば分かりますが、TypeScript での書き方がわからなかったので調べました。

環境

OS
Microsoft Windows 21H2
Node.js
18.16.0
svelte
4.0.5
@sveltejs/kit
1.20.4

SvelteKit での JSON ベースの API の実装

クライアントサイド

以下のページを /src/route/todo/+page.svelte として作成します。ボタンを押すと POST リクエストが飛ぶ簡単なページです。

/src/route/todo/+page.svelte
<button on:click={handlerClick}>Request</button>

<script lang="ts">
const handlerClick = async () => {
  await fetch("/api/todo", {
    method: "POST",
    body: JSON.stringify({ id: 5 })
  });
}
</script>

サーバーサイド

以下のサーバー側のエンドポイントを /src/route/api/todo/+server.ts として作成します。POST されてきた body をそのまま返すやまびこエンドポイントです。/src/route 以下のパスがクライアントサイドでリクエストする URL パスと一致していることに注意してください。

/src/route/api/todo/+server.ts
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
	return new Response(JSON.stringify(body));
};

チュートリアルにも以下記載がありますが、対応する HTTP メソッドである POST 関数を作成しエクスポートしています

+server.js ファイルを追加し、そこで HTTP メソッド GETPUTPOSTPATCHDELETE に対応する関数をエクスポートすることで、 API ルート(API routes) を作成することもできます。

さてこの HTTP メソッドに対応する関数ですが、RequestHandler (Types • Docs • SvelteKit) という型が用意されています。

RequestHandlerRequestEvent (Types • Docs • SvelteKit) を引数で受け取り、Web API 標準の Response (Response - Web API | MDN) を返す型となっています。

RequestEvent は Web API 標準の Request (Request - Web API | MDN) や URL (URL - Web API | MDN) 等が含まれており、ここでは request のみ使うので分割代入で取得しています。

なお、RequestHandler は本来パスパラメータとパスの型を示す 2 つのジェネリクスを持っているのですが、ここでは指定しておりません。この理由は次項の Generated Type で説明します。

ちなみに SvelteKit の json() (Modules • Docs • SvelteKit) を使用することで、JSON 文字列に変換された Response オブジェクトが得られるので、以下のような書き方でもよいです。

/src/route/api/todo/+server.ts
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
  return json(body);
};

Generated Type

/src/route/api/todo/+server.ts の 1 行目に注目してください。./$types からインポートしていますが、/src/routes/api/todo ディレクトリに ./$types.d.ts ファイルはありません。では一体何をインポートしているのでしょう。

SvelteKit はコンパイル後の JavaScript を /.svelte-kit/generated ディレクトリに生成しますが、そのほかに TypeScript の型も /.svelte-kit/types に生成します。

/.svelte-kit/types/src/routes 以下には /src/routes 以下と同じディレクトリ構成が生成されています。

/.svelte-kit/types/src/route/api/todo を見てみましょう。$types.d.ts が生成されています。

/.svelte-kit/types/src/route/api/todo/$type.d.ts
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type RouteParams = {  }
type RouteId = '/api/todo';

export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

ここで RequestHandler 型がエクスポートされています。RequestHandler は元々パスパラメータとパスを示す 2 つのジェネリクスを持っているのですが、ここでそれぞれの型を渡した上でエクスポートされているため、RequestHandler を作成するたびにパスパラメータやパスの型を定義する必要がなくなります。

さて次に何故 /.svelte-kit/types/src/route/api/todo/$types.d.ts/src/route/api/todo/+server.ts から同じディレクトリにあるように見えるかについてですが、TypeScript の rootDirs で実現しています。

Using rootDirs, you can inform the compiler that there are many “virtual” directories acting as a single root. This allows the compiler to resolve relative module imports within these “virtual” directories, as if they were merged in to one directory. ー TypeScript: TSConfig Reference - Docs on every TSConfig option

/.svelte-kit/tsconfig.json を見てみると以下設定があります。

/.svelte-kit/tsconfig.json
		"rootDirs": [
			"..",
			"./types"
		],

これにより、/.svelte-kit/tsconfig.json から見た .. であるプロジェクトルート / 以下と ./svelte-kit/types/ 以下はマージされたようになり、/.svelte-kit/types/src/route/api/todo//src/route/api/todo/ は同じディレクトリであるかのように扱うことができます。

あとがき

SvelteKit は最近人気なようですが日本語の情報がまだまだ少なく調べるのは結構大変ですが、公式の情報が充実しているのでしっかり探せば十分開発できそうだなと思いました。しばらく SvelteKit で遊んでみようと思います。