NestJS+Svelteを使って簡単なSSRを試してみた

はじめに

エンジニアの松原です。普段の業務ではバックエンド開発業務が多く、たまにはフロントエンドのこともしたいと思いつつ、新しいWebフレームワークにも触れたいと考えたため、 バックエンドとしてRails風にMVCベースの設計を持っているNestJS(厳密にいうと、NestJSはViewはデフォルトでは持っていませんが)、描画エンジンとしてフロントエンドのWebフレームワークであるSvelteを組み合わせて Hello World! をやってみました。

尚、今回はとっかかりとしてのコードしか書いておらず、ちゃんとSSRを行うにはSvelteをTypeScript対応にしたり、Webpackで依存するパッケージをバンドルするなどの手続きが必要になります。 以降フォローできればと思います。

また、今回はPCに node (v16以降、と npm ) が入っていることを前提に記事を書いています。

NestJSのセットアップ

NestJSのCLIが必要なので、npmからインストールしておきます

npm install -g nest

NestJSのプロジェクト作成を作成する

CLIから新規のプロジェクトを作成します

nest new example-my-project

その際、パッケージマネージャーを何にするか聞いてくることがありますので、選択します(矢印キーで操作して選びます) 今回はyarnを使用したいので、yarnを選んでいます

⚡  We will scaffold your app in a few seconds..

CREATE example-my-project/.eslintrc.js (665 bytes)
CREATE example-my-project/.prettierrc (51 bytes)
CREATE example-my-project/nest-cli.json (118 bytes)
CREATE example-my-project/package.json (2002 bytes)
CREATE example-my-project/README.md (3340 bytes)
CREATE example-my-project/tsconfig.build.json (97 bytes)
CREATE example-my-project/tsconfig.json (546 bytes)
CREATE example-my-project/src/app.controller.spec.ts (617 bytes)
CREATE example-my-project/src/app.controller.ts (274 bytes)
CREATE example-my-project/src/app.module.ts (249 bytes)
CREATE example-my-project/src/app.service.ts (142 bytes)
CREATE example-my-project/src/main.ts (208 bytes)
CREATE example-my-project/test/app.e2e-spec.ts (630 bytes)
CREATE example-my-project/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? (Use arrow keys)
> 

プロジェクトディレクトリに移動します。

cd example-my-project

READMEが自動生成されていますので、package.jsonと合わせて、自分の情報に書き換えます。

Svelteのパッケージをインストールする

サーバーサイドでhtmlレンダリングを行うためにSvelteの設定を追加していきます。 まずは必要パッケージをインストールします。それぞれ svelte はSvelte本体、 svelte-check はSvelteのテンプレートの記述を診断するCLIツールになります。

yarn install -D svelte svelte-check

SvelteをViewEngineとして設定する

NestJsのControllerからhtmlのレンダリング処理を自分でコントロールしたい場合、描画処理の手続きを記述する必要があります。 今回はSvelteのテンプレートを使うため、Svelteのモジュールに処理を実行させるコードを書きます。 src/ 配下に svelte-view-engine.ts というファイル名を追加し、以下のコードを追加します。

import 'svelte/register';

interface NodeCallback<T> {
  (err: any, result?: undefined | null): void;
  (err: undefined | null, result: T): void;
}

export function renderWithViewEngine(filePath: string, options: any, next: NodeCallback<any>) {
  const component = require(filePath).default;
  const { html } = component.render(options);
  next(null, html);
}

src/main.ts にこのコードを反映させます。

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { resolve } from 'path';
import { AppModule } from './app.module';
import { renderWithViewEngine } from './svelte-view-engine';

async function bootstrap() {
  // const app = await NestFactory.create(AppModule); // <- 変更前の記述
  // 以降を追加
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.engine('svelte', renderWithViewEngine);
  app.setViewEngine('svelte');
  app.setBaseViewsDir(resolve('./src/views'));
  // ここからは以前と同様
  await app.listen(3000);
}
bootstrap();

Svelteのテンプレートをレンダリングできるようにする

Svelteのテンプレートを作成します。 src/ 配下に views というディレクトリを作成し、 Layout.svelte を作成し、以下のコードを追加します。 このファイルはhtmlを作成する際のひな型になります。

<script>
   export let title;
</script>

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

同じく Home.svelte を作成し、以下のコードを追加します。先ほどの Layout.svelte をベースにして、 <slot /> の箇所に Home.svelte の内容が流し込まれます。

<script>
   import Layout from './Layout.svelte';
   export let message = "";
   export let title = "";
</script>

<Layout title={title}>
    <h1>{message}</h1>
</Layout>

これらのテンプレートを使ってレンダリングするためには、コントローラー側に対象のビュー設定を行うためのメソッドデコレータを指定する必要があります。 src/app.controller.ts を書き換えます。新たに Render のメソッドデコレータを追加し、描画するテンプレート ( この場合は Home -> Home.svelte という対応関係) を指定します。

import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('Home')
  getHello(): object {
    return {
      message: this.appService.getHello(),
      title: 'Home'
    };
  }
}

実行してみる

以下のコマンドを実行し、NestJSを開発モードでローカル実行してみます。

yarn start:dev

以下のようにNestJSのサービスが稼働していれば大丈夫です。

[10:33:31] Starting compilation in watch mode...

[10:33:33] Found 0 errors. Watching for file changes.

[Nest] 28600  - 2022/**/** **:**:**     LOG [NestFactory] Starting Nest application...
[Nest] 28600  - 2022/**/** **:**:**     LOG [InstanceLoader] AppModule dependencies initialized +26ms   
[Nest] 28600  - 2022/**/** **:**:**     LOG [RoutesResolver] AppController {/}: +5ms
[Nest] 28600  - 2022/**/** **:**:**     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 28600  - 2022/**/** **:**:**     LOG [NestApplication] Nest application successfully started +3ms

http://localhost:3000 にアクセスします。

無事NestJS + Sveleteで Hello World! ができました。

まとめ

今回はじめてNestJSを触りましたが、NestJSコード構造は、Railsの設計にも若干似ており、(RubyとTypeScriptという開発言語の違いはありますが)Rails開発で培ってきた経験がそのまま利用できるイメージがあります。
また、NestJSにはTypeScriptで利用できる強力なデコレータが用意されており、JavaのSpringのアノテーションにも似ており、メンテナンス性を考えて丁寧に設計されていることが良く分かります。
これまでがっつりMVCベースのWebフレームワークを触ってこられた方は使いやすいWebフレームワークなのかなと思います。

公式サイト(英語)のOverviewを見ていくと、どういったWebフレームワークなのか理解を深められると思いますので、NestJSに興味を持っていただいた方は是非お読みください。

docs.nestjs.com

SvelteはReactやVueと似たようなものかと考えていましたが、実際触ってみるとPugのようなテンプレートエンジンに近いイメージを持ちました。独特な構文が少ないため、学習コストもReactやVueと比べると非常に少ないと思います。
今回はアドホックなやり方でSvelteを使っているため、ブラウザ上のロジックが動作しない(動的バインディングや関数が動作しない)のですが、本来のSvelteはSvelteはReactやVueと同様にブラウザ上にロジックを持たせることができるWebフレームワークのようですので、Svelteが気になる方は公式サイトをぜひ除いてみてください。
公式の対話型チュートリアルを通して、実際にコードに触れながら学ぶことができますので、空き時間にポチポチ触ってみると良い時間つぶしになりそうです。

svelte.jp

次回以降の記事では具体的なWebアプリケーションとして動作する仕組みを試していければと考えております。