🕌

aspida/frourioは何をしているのか。極小プロジェクトに導入して理解するハンズオン

2022/02/10に公開

極小というのは local minimum という意味です。

サーバーはたった1ファイルの index.ts のみ、フロントエンドは create-vite のテンプレートの状態から解説します。とても簡単なので是非見ていってください。

Aspida と Frourio

フロントエンドとHTTP APIサーバーを用意 (スキップ可能)

ここは本質ではないため、 git clone https://github.com/frouriojs/learn-aspida-frourio-handson としてしまっても問題ない。ただし、こちらは pnpm を用いているため、事前に npm i -g pnpm などでインストールしておく必要がある。

clone してすすめるかたは #まずはAspidaを導入 まで飛ばしていただいて構わない。

さて、以下はテンプレートと同様に pnpm を使う。これは主に pnpm が提供するモノレポサポートの恩恵を得るためだが、 npm でも問題ない。(その場合は適宜読み替えてほしい)

フロントエンドは create-vite を使って簡単なものを用意する。

mkdir learn-frourio
cd learn-frourio
pnpm init -y
your_editor pnpm-workspace.yaml

pnpm-workspace.yaml は以下のように編集。

packages:
  - 'front'
  - 'server'

これで learn-frourio/front/learn-frourio/server/ がモノレポの対象になる。

次に vite によるフロントエンドをテンプレートから生成。

pnpm create vite -- --template react-ts front
pnpm i

続いて、サーバーを用意。たったの1ファイルなので安心してほしい。

mkdir server
cd server
pnpm init -y
pnpm i fastify fastify-cors
pnpm i typescript ts-node @types/node -w  # こちらはルートに追加している
your_editor index.ts

index.ts を以下のように編集する。

server/index.ts
import FastifyCors from 'fastify-cors'
import Fastify from 'fastify'

const fastify = Fastify();

fastify.register(FastifyCors, {})

fastify.get('/', (req, reply) => {
  reply.send({hello: 'world'})
})

fastify.listen(8888, '0.0.0.0')
pnpm exec ts-node index.ts

# 別のターミナルで
curl localhost:8888

JSONが正しくみれれば成功。

ルーティングが一個だとわかりにくいので、もう一つ実装しておきます。

server/index.ts
fastify.get('/', (req, reply) => {
  reply.send({hello: 'world'})
})

+ fastify.get('/hi', (req, reply) => {
+   reply.send({hello: 'how are you?'})
+ })

fastify.listen(8888)

また、ブラウザコンソールで fetch("http://localhost:8888") をしておいて到達を確認しておいてください。

まずはAspidaを導入

さて、フロントエンドから先程作ったサーバーにアクセスしたいです。fetch("http://localhost:8888") のように書いていってもいいですが、型がほしいです。そこでAspidaを導入します。

cd ../front
pnpm i aspida @aspida/fetch

開発サーバーを立ち上げておいても良いです。

pnpm run dev -- --host

デフォルトで http://localhost:3000 にアクセスすれば見れると思います。

まず型定義を書きます。 mkdir -p api/hi として、 api/index.tsapi/hi/index.ts を以下の内容に編集します。

api/index.ts,api/hi/index.ts
export type Methods = {
  get: {
    resBody: {
      hello: string;
    }
  }
}
pnpm exec aspida

api/$api.ts が生成されます。これには、先程の index.ts の内容が集約されています。

touch ./src/api-client.ts
your_editor ./src/api-client.ts

front/src/api-client.ts を以下のように編集します。

front/src/api-client.ts
import $api from '../api/$api';
import aspida from '@aspida/fetch';

export const api = $api(aspida(undefined, {
  baseURL: 'http://localhost:8888',
}));

aspida に渡した undefined はデフォルト引数( globalfetch オブジェクト)を使わせるためです。fetchを渡しても良いです。

これだけで完了しました。 front/src/api-client.ts の下の方で api. などと書くとパスが補完されると思います。

front/src/App.tsx を以下のように編集してください。

front/src/App.tsx
+ import {api} from './api-client'

function App() {
  const [count, setCount] = useState(0)
+  const [greeting, setGreeting] = useState('loading...')

+  useEffect(() => {
+    api.$get().then(res => {
+      setGreeting(res.hello);
+    })
  }, [])

これで greeting ステートにfetchした値が入ります。

あとはすきなところに表示しましょう。

front/src/App.tsx
-        <p>Hello Vite + React!</p>
+        <p>{greeting}</p>

サーバーにFrourioを導入

さて、今度はサーバーも先程定義した api/.../index.ts の型を保証した状態で開発したいです。
そこで次は frourio を導入します。 aspida も必要になるので aspida も入れておきます。 (フロントからは消しても良いです、 @aspida/fetch はランタイムとして必要なので残してください。)

cd ../server
pnpm i -D frourio aspida
mkdir api
pnpm exec frourio --watch

# 別のターミナルで
mkdir api/hi

さて、frourio を実行すると$server.tsが、mkdir を実行すると、api/ 配下に index.ts, controller.ts, $relay.ts が出現したと思います。
index.ts は先程の aspida で使ったような型定義を書く場所です。先程のfront/api/.../内の型定義をこちらに書き写しておいてください。その後消して問題ないです。

controller.ts にはサーバーの実装を書きます。次のように書き換えます。

server/api/controller.ts
import {defineController} from './$relay'

export default defineController(() => ({
-  get: () => (...)
+  get: () => ({status: 200, body: {hello: 'world'}})
}))

先程書いた index.ts の型に違反する形で書くとエラーになります。
api/hi/controller.ts のほうも書き換えておいてください。

最後にaspidaを実行します。これで先程と同じように api/$api.ts が生成されます。

frourio --watch の方は終了しても問題ありません。

index.tsfrourio で書いた定義を使うように書き換えます。

server/index.ts
import Fastify from 'fastify'
+ import $server from './$server';

const fastify = Fastify();

fastify.register(FastifyCors, {})

- fastify.get('/', (req, reply) => {
-   reply.send({hello: 'world'})
- })
- 
- fastify.get('/hi', (req, reply) => {
-   reply.send({hello: 'how are you?'})
- })
+ $server(fastify);

fastify.listen(8888, '0.0.0.0')

最後に、front/src/api-client.ts を書き換えます。APIの型定義をおいている位置が変わったためです。

front/src/api-client.ts
-import $api from '../api/$api';
+import $api from '../../server/api/$api';

さて、 pnpm exec ts-node index.ts を再起動します。

フロントエンドからみて問題なく動いていれば成功です。

PR: この内容で 【frourio LT会】TypeScriptでフルスタックWeb開発!初心者歓迎! に登壇する予定です。そこではさらに、各ファイルのより詳しい説明や frourio の嬉しさについて話すつもりdす。

GitHubで編集を提案

Discussion