Open6

TypeScript の便利そうなライブラリを使って「つもり貯金」のWebアプリを作る

funwarioisiifunwarioisii

いろいろなキャッチアップが目的

使ってみる

  • Auth0
  • react-hook-form
  • zod
  • Next.js(App Router)

あたりをキャッチアップしていく

funwarioisiifunwarioisii

データベース周りについて。
普段は Rails と Ridgepole を使っているけど、今回はサーバサイドもTypeScript で。
ActiveRecord から離れるのやだなぁなど。

データベースについて悩んだけど、アプリをVercel にホスティングするので Vercel Postgres を使ってみる。
スキーマ定義は sqldef を使っていく
https://github.com/k0kubun/sqldef

funwarioisiifunwarioisii

つもり貯金の目標金額をデータベースでは次のように定義した

create table objectives
(
    id           serial primary key,
    name         varchar(255) not null,
    description  text,
    target_value integer      not null,
    user_id      text,
    archived_at  timestamp,
    created_at   timestamp    not null default now(),
    updated_at   timestamp    not null default now()
);

一方でTypeScript で扱うため以下のように定義している。

const objectiveSchema = z.object({
  id: z.number(),
  name: z.string(),
  description: z.string(),
  targetValue: z.number(),
  userId: z.string(),
  archivedAt: z.date().optional(),
  createdAt: z.date(),
  updatedAt: z.date(),
});

この場合、 archived_at と archivedAt が一致しないため zod でパースできなくて不便
ORM も含めて勉強しようとするとちょっと荷が重いかなぁと思ったのでコンバータのような関数を入れていくことにする。

ちなみに ChatGPT に聞いたら最初に言われたのは ORM 入れろだった。嫌どす。

funwarioisiifunwarioisii

Zod での消耗がある……。
たとえば DB から取り出した値は先程の通り。type Objective = z.infer<typeof objectiveSchema>; とできる。
一方これは DB -> API のレイヤで、 API -> React でまた変わってしまう。
たとえば createdAt が String 型になってしまう。そのためそのまま objectiveSchema.parse(data) みたいなことはできないわけだ。
正しいけどそこまでちゃんとやらんでもなぁという気がしつつ。

DTOみたいなのがあれば良いんだろうと思いながらも一旦は schema.parse({ ...data, createdAt: ..., }) と手で直していく。

funwarioisiifunwarioisii

API層で認証したいので Auth0 の getSession を使いながらやっていく。

ところで package.json はこんな感じ。
TSは 5.2 で動いている。

  "dependencies": {
    "@auth0/nextjs-auth0": "^3.2.0",
    "@vercel/postgres": "^0.5.1",
    "bcrypt": "^5.1.1",
    "next": "^13.5.6",
    "react": "^18",
    "react-dom": "^18",
    "react-hook-form": "^7.48.2",
    "result-type-ts": "^2.1.3",
    "ts-pattern": "^5.0.5",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/bcrypt": "^5.0.2",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.0.1",
    "prettier": "3.0.3",
    "typescript": "^5"
  }

このとき、どうやらauth0 のバージョンの問題か型が不整合で route.ts をやれなかったので一旦 route.js にした。
次のようなエラーが出ていた。

 ⨯ TypeError: Cannot read properties of undefined (reading 'set')

https://nextjs.org/docs/app/building-your-application/routing/route-handlers を読むと

TypeScript Warning: Response.json() is only valid from TypeScript 5.2. If you use a lower TypeScript version, you can use NextResponse.json() for typed responses instead.

とかあるので、まぁこの辺の話なのかなと思う。深掘りせずさっと .js に逃げてきたらうまく動いた。

後この辺は middleware でなんとかする領域な気もする。

funwarioisiifunwarioisii

一通り作業をしてデプロイすると次のエラーが…。

https://github.com/vercel/next.js/discussions/57606

Failed to compile.
src/app/layout.tsx
Type error: Module '"next/dist/lib/metadata/types/metadata-interface.js"' has no exported member 'ResolvingViewport'.
error: script "build" exited with code 1 (SIGHUP)
Error: Command "bun run build" exited with 1