Webサービスを作るときのテンプレートを作った

2024 / 02 / 16

Edit

週末に自分がよく使っている技術をまとめたら反応が良かったので、テンプレートを作りました。 なにかWebサービスを作るときに、自分はこれらのライブラリを基本的には入れます。

ベースはcreate-next-appとなりますが、そこで生成された状態だと認証もDBも何もありません。 しかし、サービスを作るにあたって必要なケースがほとんどです。 このテンプレートには特定のライブラリを入れると毎回書かないといけない項目等を事前に作っておき、 開発に集中できる仕組みを作るのがゴールとなります。また、例を示しつつ削除するコード量を最小限に抑えます。

主にNext.js固有のハマるポイントや環境構築などめんどくさいけど毎回書いている点をカバーします。

  • linterと関連があるVSCode, pre-commit等の設定
  • NextAuthに指定されたDB Schemaの作成やAPI routeの設置
  • 開発、テストのDBのセットアップ
  • DBを利用したunit, e2eの環境構築
  • CI環境の構築

何で作るか?

JavaScript 界隈は、scaffoldingのツールはYoPlopなどがあります。最近だと、Turborepoにもgenが入りました。

今回はあえてそれらを使わず、GitHub Templateを利用しようと思います。 本当は質問形式にしたほうがプロジェクト名など絶対に変わる値や利用しないパッケージはファイルを配置しないみたいな柔軟な方が利用者にとってはよいのかもしれません。 (過去、今回同様にYoのWebサイト作成generatorを作ったことがあります)

選んだ理由は単純ですが、

  • 自分がGitHub Templateを利用したことがなかった
  • 何も実行ライブラリを入れずにGitHubでリポジトリを作るときにボタン押すだけで良いため他の人にも配布しやすい
  • 質問等もなく簡素な分、メンテナンスコストが減る

中身を見る

以下のリポジトリがGitHub Templateとなり、誰でもこのテンプレートを利用し新しい開発が行えます。

GitHub - hiroppy/web-app-template: A minimal template for web app including DataBase, Google Auth, and frontend infrastructure 🎃 A minimal template for web app including DataBase, Google Auth, and frontend infrastructure 🎃 - hir...

改めて見ると、設定ファイル量が多いですね。。

テンプレートを作ってやらなくて良くなった一例を紹介します。

VSCodeにおけるBiome, Prettier, Prismaの言語振り分け
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[prisma]": {
    "editor.defaultFormatter": "Prisma.prisma"
  }
}
PlaywrightのPage object models雛形

Page object models(通称ポム) はroleを隠蔽することにより、値が変わっても影響範囲を最小限に抑えテストの保守性を高めます。

export class Base {
  protected page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async init() {
    await this.page.goto("http://localhost:3000");
    await this.page.waitForLoadState("networkidle");
  }

  async addItem(content: string) {
    expect(
      await this.page
        .getByRole("link", { name: "Add an item" })
        .getAttribute("href"),
    ).toBe("/create");
    await this.page.getByRole("link", { name: "Add an item" }).click();

    await this.page.getByLabel("New Memo").fill(content);
    await this.page.keyboard.press("Enter");
    await this.page.waitForLoadState("networkidle");
    await this.page.goto("/");
  }
}
NextAuthが要求するPrisma Schema

NextAuthが認証情報やアカウント情報を保持するためには、指定されたスキーマが必要です。

model Account {
  id                 String    @id @default(cuid())
  userId             String
  providerType       String
  providerId         String
  providerAccountId  String
  refreshToken       String?
  accessToken        String?
  accessTokenExpires DateTime?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt
  user               User      @relation(fields: [userId], references: [id])

  @@unique([providerId, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  userId       String
  expires      DateTime
  sessionToken String   @unique
  accessToken  String   @unique
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  user         User     @relation(fields: [userId], references: [id])
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  accounts      Account[]
  sessions      Session[]
}

model VerificationRequest {
  id         String   @id @default(cuid())
  identifier String
  token      String   @unique
  expires    DateTime
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@unique([identifier, token])
}
PrismaのスキーマからED図を作成
generator erd {
  provider = "prisma-erd-generator"
  theme = "forest"
  output = "ERD.md"
  includeRelationFromFields = true
}
HMRでPrismaがクラッシュする対応

HMR(Hot Module Replacement)でコネクションプールが枯渇する問題がある。 この方法だと、動くけどVSCodeの補完でこのファイルが出てこなくて厳しいが公式の対応を今は待つしかない。

import { PrismaClient } from "@prisma/client";

export let prisma: PrismaClient;

if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
} else {
  if (!global.prisma) {
    global.prisma = new PrismaClient();
  }

  prisma = global.prisma;
}
NextAuthのAPI route設置

api/auth/[...nextauth]に置くのが推奨される。

import { options } from "@/app/_clients/nextAuth";
import NextAuth from "next-auth/next";

const handler = NextAuth(options);

export { handler as GET, handler as POST };

この一例だけでも、毎回手で書くのがめんどくさくなりますね。 まだ、Docker Composeやe2eでのGoogle認証回避等もあります。

FAQ

Biome, Prettier, ESLintの役割分担は?

もともとBiomeの導入をした理由は、ESLintをなくしたかったのが大きいです。 しかし、Next.jsが提供するnext/core-web-vitalsのために、ESLintもこのテンプレートには入ってしまっています。

Formatterの方は、Prettierで満足していましたが、importのソート、今後入るTailwindのクラス名ソートの機能が欲しかったため、 TypeScriptのフォーマットはBiomeに任せています。ただ、Biomeはymlやmarkdownのサポートはないため、そちらはPrettierに今も任せています。

現在は過渡期で、テンプレートの中に同じ役割を行うライブラリが複数入っているように見えますが、役割は重複していません。 しかし、言語単位でツールを分けているためにVSCodeやlint-stagedの設定が複雑なのです。

React Hook Formまだいる?

Server Actionsの登場により、useFormState, useFormStatus(or useTransision) で十分に見えるかもしれませんが、決してそんな事はありません。 やっぱり、現実的にバリデーションしたり、useArrayFieldsを利用したUIを作りたくなるケースがあり、便利なので手放せません。 ただ可能であれば、form actionでserver actionを渡し、極力server componentにしておきたいです。

Next.jsのapp routerは安定している?

基本的な機能は安定しているように感じますが、intercepting routeがまだ安定しないように見えます。特にgroupingと組み合わせると動かなくなる問題があるのがちょっと厳しいかも。。 FYI: #54636, #53170.

Partial Prerenderingが安定になるまで、様子見してもいいと思います。

あと、結構難しいのが、開発環境と本番環境でキャッシュの挙動が変わるケースがあり、build/startしたら自分の思ってた通りの挙動じゃないみたいなケースがあります。 どうしても頭の中だとデフォルトでレスポンス等もbuild時にキャッシュされるってのを忘れがちだったり、 headers(), cookies()があるためにdynamicへ切り替わる等コードから読み解かないといけなかったり、そういう見逃しが未だにあります。

まとめ

10年前に作ったYoのジェネレーターはメンテできなかったけど、今回はGitHub Templateでrenovateでdepsの管理もしているし、 数年ぐらいは頑張れる気がしてきた。

自分が普段使っている技術スタックや設定が参考になれば幸いです。

GitHub - hiroppy/web-app-template: A minimal template for web app including DataBase, Google Auth, and frontend infrastructure 🎃 A minimal template for web app including DataBase, Google Auth, and frontend infrastructure 🎃 - hir...