frourioのAPI開発が快適すぎました
Typescriptのフルスタックフレームワークであるfrourio
を使用して、趣味で個人CMSを作ってみました。
型定義やAPI開発が大変はかどったので、備忘録の意味も含め記事に残しておこうと思います。
CMSってどんなやつ作ったの?
今回作成したのは、Webサイト構築用のCMSです。
コンポーネントをJson形式で保存し、そのデータを元にWebページのレンダリングを行います。
モチベーションのほとんどは何か作りたい欲で、自分でツールを作りたい!というのが大きなところを占めています笑
主な機能は以下の通りです。
・記事作成
・コンテンツ作成
・予約投稿
・OAuthでのログイン
型安全なAPI
一番最初はフロントエンドをNextjs、バックエンドをDjangoで作成していました。
初学者ゆえ勉強も兼ねていたので、少しだけ触ったことのあるフレームワークを選定したのが大きな理由です。
しかし、API周りの型定義で開発が苦しくなっていきました。
以下はフロントから投稿を取得する一例です。
useSWR
にて取得データへ型付けを行なっていますが、取得データの検証を行うことはできません。
export const usePost = (id: string) => {
const proxy = '/api/proxy';
const prefix = '/__admin/post';
const url = `${proxy}${prefix}?id=${id}`
const { data, mutate, error } = useSWR<Post>(url, fetcher)
return {
post: data,
mutate: mutate,
isLoading: !error && !data,
isError: error
}
}
フォーマット用の関数を作成することで、より型安全に使用することはできます。
しかし、APIの仕様や投稿の型が変わるたびに修正する必要が出てくるため、少し面倒だなあと思っていました。
export const usePost = (id: string) => {
const proxy = '/api/proxy';
const prefix = '/__admin/post';
const url = `${proxy}${prefix}?id=${id}`
const { data, mutate, error } = useSWR<Post>(url, fetcher)
return {
post: formatPost(data),
mutate: mutate,
isLoading: !error && !data,
isError: error
}
}
const formatPost = (data: any): Post => {
if (!data) {
return null
}
return {
id: data.id ?? '',
title: data.title ?? '',
slug: data.slug ?? '',
elements: data.elements ?? ''
}
}
frourio
に組み込まれているaspida
はこういった型問題を簡単にすることができます。
リクエストを送るクライアントはエンドポイントを文字列ではなく、プロパティで指定をしてリクエストを送ることが可能です。
プロパティを指定することで、文字列によるエンドポイント指定ではできなかった型精査を行うことができるようになります。
const id = 'abc'
// dataの型は Any となる
const {body: data} = await fetch (`http://localhost:8000/api/post?id=${id}`)
.then(r => r.json());
// dataの型は Post となる
const {body: data} = apiClient.api.post({ query: { id } })
サーバー側はエンドポイントをディレクトリ・ファイルを用いて直感的に定義することが可能です。
server/api
配下にディレクトリを作成することで、そのディレクトリのルートをエンドポイントとすることができます。
また、ディレクトリ作成時に自動的にエンドポイントの型ファイルindex.ts
とコントローラーファイルcontroller.ts
が作成されます。
// index.ts
export type Methods = {
get: {
query: {
id: Post['id']
}
resBody: Post | null
}
put: {
reqBody: Post
resBody: APIResult
}
delete: {
reqBody: Post['id']
resBody: APIResult
}
}
// controller.ts
export default defineController(() => ({
get: async ({ query }) => ({ status: 200, body: await getPost(query.id) }),
put: async ({ body }) => {
const isValid = await isValidPost(body)
if (isValid.status === 'invalid') {
return {
status: 200,
body: { status: 'failed', exception: isValid.exception }
}
}
await updatePost(body.id, body)
return { status: 200, body: { status: 'success' } }
},
delete: async ({ body }) => {
await deletePost(body)
return { status: 200, body: { status: 'success' } }
}
}))
controller.ts
のそれぞれのメソッドの返り値は、index.ts
で定義したresBody
に沿っていないと型エラーとなります。
これにより、サーバー側は型安全なapiを定義しやすくなり、またそれを利用するクライアント側は安心してリクエストを送ることができます。
サーバーとクライアントで型共有
frourioはORMとして、prisma
を採用しています。
prismaにてモデルを定義することで、そのモデルの型を自動的に定義することが可能です。
また、定義された型はサーバー・クライアント両側で使用でき、DB定義を正として開発を進められるため、「DBのモデル変えて、サーバー・クライアントそれぞれ型ファイル書き換えて。。。」という面倒なことをしなくて良くなりました。
datasource db {
provider = "postgresql"
url = env("API_DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id String @id @default(cuid())
title String?
slug String
publish Boolean @default(false)
}
/**
* Model Post
*
*/
export type Post = {
id: string
title: string | null
slug: string
publish: boolean
}
終わりに
今回は簡単ですが、frourioを使って便利だなあと思った点を書いてみました。
特にaspida
を初めて使用した時は、あまりの便利さにびっくりしました。。
Discussion