NestJSとSvelteKitを使用したアプリケーションの構築
はじめに
まず、この構成になぜと疑問に思う方も多いでしょう。
実はこの構築は、最近転職した会社が採用している環境の内の一つなのです。
筆者もメインはNext.jsを利用することが多く、初めて聞いた時は結構驚きました。
今回この環境を作った経緯としては入社前に作ってしまって使用感を知ろうと思ったからです。
実はこの珍しい開発環境は入社を決めた決め手の一つだったりします。
とはいっても、実はSvelteはSapperが主要フレームワークの時から触っており、その書きやすさは知っています。
非常に見通しがよく書けるので、おすすめです。
この記事ではNestJSからHello Worldを送信するAPIを作成し、SvelteKit側で画面に表示するのをゴールに環境を構築していきます。
リポジトリは以下になります。
https://github.com/iidadaiti/hello-svnext
使用技術
- SvelteKit
- NestJS
- Docker
Dockerの構築
コンテナ環境にする事でローカルの環境を(ほぼ)汚す事なく構築できます。
また、コンテナは移植や削除も簡単に行える為、今回のようなお試し環境を構築するには最適です。
さらに、vscodeを使用している場合は、Dev Containerを利用することでさらに幸せになれます。
この記事ではコンテナ環境の構築と、Dev Containerの設定を記載していますが、詳しく説明はしないのでご了承ください。
今回のDev Contaier構成です。
なお、compose.yaml
はまだ空のファイルになります。
.
├── .devcontainer/
│ ├── Dockerfile
│ ├── compose.yaml
│ └── devcontainer.json
├── .dockerignore
└── compose.yaml
FROM node:22.2.0-bookworm-slim AS base
ENV PNPM_HOME "/pnpm"
ENV PATH "$PNPM_HOME:$PATH"
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT 0
RUN corepack enable \
&& corepack enable npm pnpm
FROM base AS dev
WORKDIR /app
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends \
tini \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
ENTRYPOINT ["/usr/bin/tini", "--"]
USER node
services:
dev:
build:
context: .
dockerfile: .devcontainer/Dockerfile
target: dev
volumes:
- .:/app:cached
tty: true
command: /bin/sh -c "sleep infinity"
{
"name": "${localWorkspaceFolderBasename}",
"dockerComposeFile": [
"../compose.yaml",
"./compose.yaml"
],
"service": "dev",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker",
"eamodio.gitlens"
]
}
}
}
# vscode
.vscode/
.devcontainer/
*.code-workspace
# misc
.DS_Store
*.md
今回の設定ではdockerを立ち上げた際にcorepackが自動で有効になります。
また、PID1問題を回避する為にTiniを使用しています。
作成できたら、Dev Containerからdevコンテナに入ります。
以降の作業は全てこのコンテナで行います。
パッケージマネージャーの選択
今回pnpmを使用したいので、corepackを利用してパッケージマネージャーを指定します。
corepackはDocker側で有効化しているので、以下のコマンドを入力するだけです。
corepack use pnpm@latest
コマンドを打つと、package.json
とpnpm-lock.yaml
ファイルが作成され、package.json
の中には使用するパッケージマネージャーが記載されていると思います。
{
"packageManager": "***"
}
これで、パッケージマネージャーを設定できました。
npmやyarnを間違えて実行しても、エラーが出力されるようになります。
Sveltekitのセットアップ
次はフロントエンドのセットアップを行います。
以下のコマンドを入力します。
pnpm create svelte@latest app
質問がされますので、答えていきます。
今回の構成は以下のように答えてテストツールなど全部込み込みにしました。
create-svelte version 6.1.2
┌ 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, Add Playwright for browser testing, Add Vitest for unit testing
│
└ Your project is ready!
完了するとapp
ディレクトリが作成されます。
編集しやすいように拡張機能を追加しておきましょう。
{
"name": "${localWorkspaceFolderBasename}",
"dockerComposeFile": [
"../compose.yaml",
"./compose.yaml"
],
"service": "dev",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "svelte.svelte-vscode",
"ms-azuretools.vscode-docker",
"eamodio.gitlens"
]
}
}
}
次に、dockerに追加されたくないファイルを追加します。
+# dependencies
+node_modules/
+.pnpm-store/
+
+# build
+build/
+.svelte-kit/
+package/
+
# vscode
.vscode/
.devcontainer/
*.code-workspace
# misc
.DS_Store
*.md
今回のプロジェクトに合わせて数箇所修正を行います。
まずはパッケージをインストールします
pnpm --prefix app install
次に、このままではローカルから接続できないので、app/vite.config.ts
に以下の一行を追加します。
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
+ server: { host: true },
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});
次に、デフォルトではnpmを利用する設定になっているので、pnpmに変更します。
{
...
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
- "test": "npm run test:integration && npm run test:unit",
+ "test": "pnpm run test:integration && pnpm run test:unit",
...
},
...
}
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
- command: 'npm run build && npm run preview',
+ command: 'pnpm run build && pnpm run preview',
port: 4173
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;
これで設定が終わったので、Sveltekitを動作させるコンテナを作成します。
FROM node:22.2.0-bookworm-slim AS base
ENV PNPM_HOME "/pnpm"
ENV PATH "$PNPM_HOME:$PATH"
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT 0
RUN corepack enable \
&& corepack enable npm pnpm
FROM base as fetcher
WORKDIR /app
COPY ./app/pnpm-lock.yaml /app
RUN pnpm fetch --prod ./
FROM base AS deps
WORKDIR /app
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY ./app/package.json /app
RUN pnpm install --frozen-lockfile
FROM base AS dev
WORKDIR /app
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends \
tini \
&& rm -rf /var/lib/apt/lists/*
COPY ./app /app
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY /app/node_modules /app/node_modules
EXPOSE 5173
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "dev"]
USER node
services:
app:
build:
context: .
dockerfile: ./app/Dockerfile
target: dev
volumes:
- ./app:/app:cached
ports:
- 5173:5173
tty: true
コンテナをリビルドすると、appコンテナが立ち上がるようになります。
フロントエンドのURLはデフォルトでhttp://localhost:5173/になっています。
NestJSのセットアップ
以下を実行します。
実行してしばらくするとapi
ディレクトリが作成されます。
pnpm dlx @nestjs/cli@latest new api --strict -p pnpm
dockerに追加されたくないファイルを追加します。
# dependencies
node_modules/
.pnpm-store/
# build
build/
.svelte-kit/
package/
+dist/
# vscode
.vscode/
.devcontainer/
*.code-workspace
# misc
.DS_Store
*.md
作成したNestJSのプロジェクト用のコンテナを追加します。
FROM node:22.2.0-bookworm-slim AS base
ENV PNPM_HOME "/pnpm"
ENV PATH "$PNPM_HOME:$PATH"
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT 0
RUN corepack enable \
&& corepack enable npm pnpm
FROM base as fetcher
WORKDIR /app
COPY ./api/pnpm-lock.yaml /app
RUN pnpm fetch --prod ./
FROM base AS deps
WORKDIR /app
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY ./api/package.json /app
RUN pnpm install --frozen-lockfile
FROM base AS dev
WORKDIR /app
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends \
tini \
&& rm -rf /var/lib/apt/lists/*
COPY ./api /app
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY /app/node_modules /app/node_modules
EXPOSE 3000
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "start:debug"]
USER node
services:
app:
build:
context: .
dockerfile: ./app/Dockerfile
target: dev
volumes:
- ./app:/app:cached
ports:
- 5173:5173
tty: true
+ api:
+ build:
+ context: .
+ dockerfile: ./api/Dockerfile
+ target: dev
+ volumes:
+ - ./api:/app:cached
+ ports:
+ - 3000:3000
+ tty: true
これで、コンテナをリビルドすると、apiコンテナが立ち上がるようになります。
また、http://localhost:3000/を立ち上げると、Hello World!が返ってきていると思います。
タスクの修正
これでクライアントとサーバーサイドのセットアップが完了したのですが、
タスクの実行にわざわざ目的のディレクトリに行ってからコマンドを打つ必要があり、面倒です。
そもそも、テスト環境をコンテナに乗せていないので、クライアント側のテスト(playgroundを利用したテスト)に失敗します。
なのでここでは、プロジェクトディレクトリ上でタスクを実行できるようにします。
(パッケージマネージャーの切り替え)[#パッケージマネージャーの切り替え]を行なった際に。package.json
が作成されているはずなので、そこにタスクを記述していきます。
今回は以下のように設定しました。
{
+ "scripts": {
+ "install": "pnpm --prefix app install && pnpm --prefix api install",
+ "dev": "pnpm run --prefix app dev && pnpm run --prefix api start:debug",
+ "build": "pnpm run --prefix app build && pnpm run --prefix api build",
+ "start": "pnpm run --prefix app preview && pnpm run --prefix api start:prod",
+ "lint": "pnpm run --prefix app lint && pnpm run --prefix api lint",
+ "test": "pnpm run --prefix app test && pnpm run --prefix api test",
+ "format": "pnpm run --prefix app format"
+ },
...
}
app側のユニットテストで使用されているvitestは、デフォルトでwatchモードのため、このままではタスクが順番に処理されません。
テストした後に止まるように修正します。
{
...
"scripts": {
...
"format": "prettier --write .",
"test:integration": "playwright test",
- "test:unit": "vitest"
+ "test:unit": "vitest run"
},
....
}
続いて、テスト環境をdevコンテナに追加します。
FROM node:22.2.0-bookworm-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable npm yarn pnpm
WORKDIR /workspaces
FROM base AS dev
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qq --no-install-recommends \
tini \
- && rm -rf /var/lib/apt/lists/*
+ && rm -rf /var/lib/apt/lists/* \
+ && pnpm dlx playwright install-deps
ENTRYPOINT ["/usr/bin/tini", "--"]
USER node
また、初回テストの際はplaywrightのインストールが必要です。
pnpm --prefix app exec playwright install
このように書き換えると、テスト実行時に自動でインストールしてくれます。
{
...
"scripts": {
...
"format": "prettier --write .",
- "test:integration": "playwright test",
+ "test:integration": "playwright install && playwright test",
"test:unit": "vitest run"
},
....
}
ちなみに、筆者の環境ではテストのセットアップがタイムアウトし、さらにFirefox以外のテストもタイムアウトするので、以下のようにしています。
{
...
"scripts": {
...
"format": "prettier --write .",
- "test:integration": "playwright test",
+ "test:integration": "playwright install && pnpm run build && playwright test",
"test:unit": "vitest run"
},
....
}
import { devices } from '@playwright/test';
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
- command: 'pnpm run build && pnpm run preview',
+ command: 'pnpm run preview',
port: 4173
},
testDir: 'tests',
+ projects: [{ name: 'Desktop Firefox', use: { ...devices['Desktop Firefox'] } }],
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;
APIの呼び出し
いよいよ、appコンテナからapiコンテナを呼び出してみましょう。
まず、作成したNestJSがCORS接続できるように設定します。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ app.enableCors();
await app.listen(3000);
}
bootstrap();
SvelteKit側ではFetchするだけです。
import type { PageLoad } from './$types'
export const ssr = false
export const load: PageLoad = async ({ fetch }) => {
try {
const res = await fetch('http://localhost:3000')
const text = await res.text()
return { text }
} catch (error) {
console.error(error);
if (error instanceof Error) {
return { text: error.message }
}
return { text: 'failed to fetch' }
}
}
+<script lang="ts">
+ import type { PageData } from './$types';
+
+ export let data: PageData;
+</script>
<h1>Welcome to SvelteKit</h1>
+<h2>{data.text}</h2>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
これで、ページにHello Worldができるようになりました。
お疲れ様です!
終わりに
今回は直接APIを叩きましたが、実際の環境では直接呼ばずにGraphqlやtRPCなどを利用して型情報を持ったAPIのやり取りをすると思います。
今回の環境ではほぼ触りませんでしたがNestJSも非常に扱いやすそうなフレームワークだと感じました。
こんな環境に触れる機会があって本当に恵まれました!
Discussion