TypeScriptで実現!Next.js × Expressによるスキーマ自動生成&環境構築ガイド【Prisma、tsoa、Chakra
はじめに
TypeScriptで型安全なフルスタック開発を実現したい、でも毎回のスキーマの同期や型定義に手間取る…そんな悩みを解消すべく、このガイドではNext.js×Expressによるスキーマ自動生成環境の構築方法を書いてみました。
tsoaでバックエンドのAPIスキーマを定義し、Prismaでデータベース操作を行います。
そして、Swaggerスキーマを活用してフロントエンドに型情報を自動生成し、Next.jsとChakra UIを使って柔軟なUIを構築します。
設定さえ済めば、手作業での型定義やコードの齟齬に悩むことなく、バックエンドとフロントエンドの連携がしやすくなると思います!
該当リポジトリ
関連記事
バックエンド: Docker環境構築
プロジェクトのルートディレクトリにbackendという名前のディレクトリを作成します。
mkdir backend
cd backend
Dockerfileとdocker-compose.ymlファイルをbackendディレクトリに作成します。
backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN yarn install --production=false
COPY . .
EXPOSE 8080
CMD ["yarn", "dev"]
backend/docker-compose.yml
version: "3"
services:
app:
build: .
container_name: test_app
ports:
- "8080:8080"
volumes:
- .:/app
command: yarn dev
db:
image: mysql:8.0.33
container_name: test_db
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
volumes:
mysql_data:
環境変数を管理するため、以下の.envファイルと.env.exampleファイルを作成します。
backend/.env
MYSQL_DATABASE=test_db
MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=password
COMPOSE_PROJECT_NAME=test_express
MYSQL_DATABASE=
MYSQL_USERE=
MYSQL_PASSWORD=
MYSQL_ROOT_PASSWORD=
COMPOSE_PROJECT_NAME=test_express
Gitに不要なファイルが追加されないよう、.gitignoreファイルを設定します。
backend/.gitignore
/node_modules
.env
簡単なpackage.jsonを作成し、プロジェクト名とバージョンを指定します。
backend/package.json
{
"name": "typescript-nextjs-express-onboarding",
"version": "1.0.0",
"license": "MIT"
}
backendディレクトリでDocker Composeを使用してコンテナを起動します。
docker compose up -d
データベース接続確認として今回はSequel Aceを使用します。
MYSQL_PASSWORD
の値をパスワードとして入力してください。
バックエンド: Expressサーバーを立ち上げる
以下のpackage.jsonファイルは、プロジェクトの基本設定を記述しています。scriptsには開発サーバーの起動やビルドのコマンドが含まれ、dependenciesとdevDependenciesには必要なライブラリがリストされています。
backend/package.json
{
"name": "typescript-nextjs-express-onboarding",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"dev": "nodemon",
"build": "tsc"
},
"dependencies": {
"express": "^4.21.1"
},
"devDependencies": {
"@types/express": "^4.17.1",
"@types/node": "^22.8.1",
"nodemon": "^3.1.7",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}
nodemon.jsonはnodemonの設定ファイルです。nodemonはファイルの変更を検知してサーバーを自動で再起動するツールです。
backend/nodemon.json
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node src/index.ts"
}
TypeScriptプロジェクトのコンパイル設定を行います。
backend/tsconfig.jso
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
次に、Expressサーバーのエントリーポイントであるindex.tsを作成します。このファイルは、ルートエンドポイント/にアクセスしたとき「Hello Express!」と返す簡単なサーバーを作成します。
backend/src/index.tsx
import express, { Request, Response } from "express";
const app = express();
const port = 8000;
app.get("/", (_: Request, res: Response) => {
res.send("Hello Express!");
});
app.listen(port);
次に、backendディレクトリでDocker Composeを使って、appコンテナ内で依存関係をインストールします。
appコンテナを起動し、yarn installコマンドで依存関係をインストールします。--rmオプションにより、実行後にコンテナが自動で削除されます。
docker-compose run --rm app yarn install
インストールが完了したら、次にbackendディレクトリでdocker-compose upでアプリケーションとデータベースを起動します。
docker compose up -d
http://localhost:8080/ で以下のように表示されれば成功です
バックエンド: ESLint
TypeScriptのプロジェクトにESLintとPrettierを組み合わせ、コードの一貫性や可読性を向上させます。
ESLintとPrettier、さらにTypeScriptサポート用のプラグインを開発用の依存パッケージとして追加します。
backendディレクトリで以下のコマンドを実行してください。
docker-compose run --rm app yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier
ESLintの設定を記述します。これにより、TypeScriptとPrettierのルールに沿ってコードをチェックできるようになります。下記ルールから好きにアレンジしてもOKです
backend/eslint.config.js
const eslintPluginPrettier = require("eslint-plugin-prettier");
const eslintPluginTypescript = require("@typescript-eslint/eslint-plugin");
const eslintParserTypescript = require("@typescript-eslint/parser");
const commonConfig = [
{
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
parser: eslintParserTypescript,
},
plugins: {
prettier: eslintPluginPrettier,
"@typescript-eslint": eslintPluginTypescript,
},
rules: {
"prettier/prettier": "error",
"no-console": ["error", { allow: ["error"] }],
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
ignoreRestSiblings: false,
},
],
"@typescript-eslint/no-unused-expressions": "error",
},
},
];
module.exports = commonConfig;
backend/package.json
"scripts": {
"dev": "nodemon",
"build": "tsc",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}
VSCodeなどでESLintでコードチェックしたい場合はbackend/node_modules
の中を更新する必要がありますのでbackendディレクトリでyarn installしてください(今後追加するライブラリも同様です)
docker-compose run --rm app yarn install
この状態でconsole.logを仕込むと以下のようなエラーが出ます
(backendディレクトリで実行)
docker-compose run --rm app yarn lint
/app/src/index.ts
11:1 error Unexpected console statement no-console
✖ 1 problem (1 error, 0 warnings)
バックエンド: Prisma導入
Prismaは、データベース操作を簡単にするためのツールです。
まず、backendディレクトリでPrismaとそのクライアントパッケージをインストールします。
docker-compose run --rm app yarn add prisma @prisma/client
次に、Prismaの初期設定を行い、backendディレクトリでスキーマファイルを生成します。
docker-compose run --rm app yarn prisma init
Prismaのschema.prismaファイルを開き、以下の内容に設定。
公式ドキュメントを参考にUserとPostモデルを定義。
この設定により、ユーザーとその投稿を管理するためのスキーマが作成されます。
backend/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
MySQLデータベースとの接続情報を.envファイルに追加します。
backend/.env
MYSQL_DATABASE=test_db
MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=password
COMPOSE_PROJECT_NAME=test_express
# 追加
DATABASE_URL=mysql://root:password@test_db:3306/test_db
backend/.env.example
MYSQL_DATABASE=
MYSQL_USERE=
MYSQL_PASSWORD=
MYSQL_ROOT_PASSWORD=
COMPOSE_PROJECT_NAME=test_express
# 追加
DATABASE_URL=
次に、Prismaのマイグレーションコマンドをbackendディレクトリで実行
docker-compose run --rm app yarn prisma migrate dev --name init
Sequel Aceでテーブルを確認すると新しくできることが確認できます。
バックエンド: tsoa導入
TypeScriptのデコレーターで簡単にAPIドキュメントを作成し、Swagger UIで閲覧できるようにします。
backendディレクトリでTsoaとSwagger UIをインストールし、開発環境と本番環境で必要なパッケージを分けて管理します。
docker-compose run --rm app yarn add tsoa swagger-ui-express
docker-compose run --rm app yarn add -D concurrently @types/swagger-ui-express
ドキュメントとルートの出力ディレクトリを指定します。
backend/tsoa.json
{
"entryFile": "src/index.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/**/*Controller.ts"],
"spec": {
"outputDirectory": "build",
"specVersion": 3
},
"routes": {
"routesDir": "build"
}
}
Userデータモデルを利用したユーザーの作成と取得のAPIを作成します。
backend/src/controllers/usersController.ts
import { Body, Controller, Get, Path, Post, Route, Response } from "tsoa";
import { PrismaClient, User } from "@prisma/client";
const prisma = new PrismaClient();
type UserCreationParams = Pick<User, "email" | "name" | "password">;
interface ValidateErrorJSON {
message: "Validation failed";
details: { [name: string]: unknown };
}
@Route("users")
export class UsersController extends Controller {
@Get("{userId}")
public async getUser(@Path() userId: number): Promise<User | null> {
return await prisma.user.findUnique({
where: { id: userId },
});
}
@Response<ValidateErrorJSON>(422, "Validation Failed")
@Post()
public async createUser(
@Body() requestBody: UserCreationParams,
): Promise<void> {
await prisma.user.create({
data: { ...requestBody },
});
return;
}
}
Tsoaが生成するSwaggerドキュメントを/docsで提供し、エラーハンドリングも設定します。
backend/src/index.ts
import express, {
json,
urlencoded,
Response as ExResponse,
Request as ExRequest,
NextFunction,
} from "express";
import swaggerUi from "swagger-ui-express";
import { RegisterRoutes } from "../build/routes";
import { ValidateError } from "tsoa";
const app = express();
app.use(urlencoded({ extended: true }));
app.use(json());
app.use("/docs", swaggerUi.serve, async (_req: ExRequest, res: ExResponse) => {
const swaggerDocument = await import("../build/swagger.json");
res.send(swaggerUi.generateHTML(swaggerDocument));
});
RegisterRoutes(app);
app.use((err: unknown, _: ExRequest, res: ExResponse, next: NextFunction) => {
if (err instanceof ValidateError) {
return res.status(422).json({
message: "Validation Failed",
details: err?.fields,
});
}
if (err instanceof Error) {
return res.status(500).json({
message: "Internal Server Error",
});
}
next();
});
const port = process.env.PORT || 8080;
app.listen(port);
export { app };
TsoaとTypeScriptのビルドスクリプトを追加
backend/package.json
"scripts": {
"dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec-and-routes\"",
"build": "tsoa spec-and-routes && tsc",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}
リポジトリに不要なファイルが含まれないようにします。
backend/.gitignore
/node_modules
.env
build
dist
backendディレクトリでDocker Composeを使ってサーバーを再起動します。
docker-compose down
docker-compose up
ブラウザでhttp://localhost:8080/docs にアクセスし、Swagger UIが表示されることを確認
フロントエンド: 環境構築
フロントエンドプロジェクトを作成する前に、ルートディレクトリにいることを確認します。ルートにはbackendディレクトリが既に存在しています。
% ls
backend
ルートディレクトリで、Next.jsのプロジェクトを作成します。
プロジェクト名はfrontendとし、他はデフォルトでOK
npx create-next-app@latest
✔ What is your project named? … frontend
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for next dev? … No / Yes
✔ Would you like to customize the import alias (@/* by default)? … No / Yes
不要なものを削除します。
どこまで削除するかは自由ですが、この記事では下記の状況になってます。
pwd
/Users/ユーザー名/ルートディレクトリ名/frontend/src/app
app % tree
.
├── favicon.ico
├── layout.tsx
└── page.tsx
frontend/src/app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja">
<body>
{children}
</body>
</html>
);
}
frontend/src/app/page.tsx
export default function Home() {
return (
<div>
<h1>Home</h1>
<p>Welcome to the home page!</p>
</div>
);
}
fronedendディレクトリでサーバー起動
npm run dev
http://localhost:3000/ で下記のような画面が表示されれば成功です
フロントエンド: ESLint
以下のコマンドで、ESLintと関連プラグインをインストールします。
npm install -D eslint eslint-plugin-prettier eslint-config-prettier prettier eslint-plugin-import eslint-plugin-react
eslintrc.jsonファイルを作成し、以下の内容を記述します。
ルールはサンプルになりますので好きに設定してOKです。
frontend/.eslintrc.json
{
"extends": [
"next/core-web-vitals",
"next/typescript",
"plugin:react/recommended",
"plugin:prettier/recommended"
],
"plugins": [
"prettier",
"import",
"react"
],
"rules": {
"prettier/prettier": "error",
"import/order": "error",
"no-console": [
"error",
{
"allow": [
"error"
]
}
],
"react/jsx-sort-props": "error",
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off",
"@typescript-eslint/no-empty-object-type": "off"
}
}
scriptsセクションに、Lintチェック用のコマンドを追加します。
frontend/package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix"
}
この状態でconsole.logを仕込むと以下のようなエラーが出ます
(frontendディレクトリで実行)
npm run lint
13:3 error Unexpected console statement no-console
✖ 1 problem (1 error, 0 warnings)
Chakra UI導入
Chakra UIは、プロダクトをスピーディに構築するためのコンポーネントシステムです。
高品質なWebアプリやデザインシステムを構築するためのアクセシブルなReactコンポーネントです。
Chakra UIインストールできない場合はNext14系にしてください
package.jsonでバージョンを変更し、npm installし直します。
package.json
(任意)
"dependencies": {
"next": "14.2.16",
"react": "^18",
"react-dom": "^18"
}
next.config.ts
からnext.config.mjs
に変更
frontend/next.config.mjs
(任意)
const config = {};
export default config;
公式ガイドを参考に、Chakra UIをインストールします。
次のコマンドをfrontendディレクトリで実行します。
npm i @chakra-ui/react @emotion/react
Chakra CLIを使用して、必要なスニペットを追加します。
npx @chakra-ui/cli snippet add
自動生成されたfrontend/src/components/ui
内のコンポーネントをコードフォーマッターをルール通りにします。
npm run lint:fix
アプリ全体でChakra UIを使用できるようにします。
frontend/src/app/layout.tsx
import { Provider } from "@/components/ui/provider";
export default function RootLayout(props: { children: React.ReactNode }) {
const { children } = props;
return (
<html suppressHydrationWarning>
<body>
<Provider>{children}</Provider>
</body>
</html>
);
}
バンドルサイズを最適化します。下記コードはNext14系の場合です。
frontend/next.config.mjs
const config = {
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
};
export default config;
Chakra UIのコンポーネントが正常に動作するか確認します。
frontend/src/app/page.tsx
import { HStack } from "@chakra-ui/react";
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div>
<h1>Home</h1>
<p>Welcome to the home page!</p>
<HStack>
<Button>Click me</Button>
<Button>Click me</Button>
</HStack>
</div>
);
}
http://localhost:3000/ にアクセスして以下のように表示されればOKです
フロントエンド: React Hook Form導入
react-hook-formはパフォーマンス、柔軟性、拡張性に優れたフォームと、使いやすいバリデーションのフォームライブラリです。
必要なパッケージをインストール
npm install react-hook-form zod @hookform/resolvers
各入力フィールドがフォームの制御にアクセスできるよう、useControllerを使ったカスタムフックを定義します。
frontend/src/components/template/fieldInput/FieldInput.hooks.ts
import { Control, FieldValues, Path, useController } from "react-hook-form";
export type UseFieldInputProps<T extends FieldValues> = {
control: Control<T>;
name: Path<T>;
};
export const useFieldInput = <T extends FieldValues>({
name,
control,
}: UseFieldInputProps<T>) => {
const {
field: { value, onChange, onBlur },
fieldState: { invalid },
formState: { errors },
} = useController({ name, control });
return { value, invalid, errors, onChange, onBlur };
};
Chakra UIのInputとReact Hook Formを組み合わせて、フォームフィールドのコンポーネントを作成します。
エラーメッセージが表示されるよう、errorMessageも取得しています。
frontend/src/components/template/fieldInput/FieldInput.tsx
import { Input, InputProps } from "@chakra-ui/react";
import { FieldValues } from "react-hook-form";
import { useFieldInput, UseFieldInputProps } from "./FieldInput.hooks";
import { Field, FieldProps } from "@/components/ui/field";
type FieldInputProps<T extends FieldValues> = UseFieldInputProps<T> & {
fieldProps?: FieldProps;
inputProps?: InputProps;
};
export const FieldInput = <T extends FieldValues>({
name,
control,
fieldProps,
inputProps,
}: FieldInputProps<T>) => {
const { value, invalid, errors, onChange, onBlur } = useFieldInput({
name,
control,
});
const errorMessage = errors[name]?.message as string | undefined;
return (
<Field
errorText={errorMessage}
invalid={invalid}
label={name}
{...fieldProps}
>
<Input
name={name}
onBlur={onBlur}
onChange={onChange}
value={value ?? ""}
{...inputProps}
/>
</Field>
);
};
インポート側でのコードがシンプルになるよう設定
frontend/src/components/template/fieldInput/index.tsx
export * from "./FieldInput";
useFormを使ってフォームの制御を行い、
送信ボタンをクリックすると入力データがバリデーションされるようにします。
frontend/src/components/pages/home/Home.tsx
"use client";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button, Flex } from "@chakra-ui/react";
import { FieldInput } from "@/components/template/fieldInput";
const REQUIRED_MESSAGE = "必須項目です";
const NAME_MAX_LENGTH_MESSAGE = "20文字以内で入力してください";
const EMAIL_MESSAGE = "メールアドレスの形式で入力してください";
const PASSWORD_MIN_LENGTH_MESSAGE = "パスワードは8文字以上で入力してください";
const schema = z.object({
name: z
.string({
invalid_type_error: REQUIRED_MESSAGE,
required_error: REQUIRED_MESSAGE,
})
.max(20, NAME_MAX_LENGTH_MESSAGE),
email: z
.string({
invalid_type_error: REQUIRED_MESSAGE,
required_error: REQUIRED_MESSAGE,
})
.email(EMAIL_MESSAGE),
password: z
.string({
invalid_type_error: REQUIRED_MESSAGE,
required_error: REQUIRED_MESSAGE,
})
.min(8, PASSWORD_MIN_LENGTH_MESSAGE),
});
type UseFormProps = {
name: string;
email: string;
password: string;
};
export const Home = () => {
const { control, handleSubmit } = useForm<UseFormProps>({
mode: "onTouched",
resolver: zodResolver(schema),
});
const onSubmit = async (data: UseFormProps) => {
alert(JSON.stringify(data));
};
return (
<Flex direction="column" gap="32px" padding="16px">
<FieldInput
control={control}
inputProps={{ width: "400px" }}
name="name"
/>
<FieldInput
control={control}
inputProps={{ width: "400px" }}
name="email"
/>
<FieldInput
control={control}
inputProps={{ width: "400px" }}
name="password"
/>
<Button onClick={handleSubmit(onSubmit)} width="200px">
送信
</Button>
</Flex>
);
};
インポート側でのコードがシンプルになるよう設定
frontend/src/components/pages/home/index.tsx
export * from "./Home";
/
にアクセスした時のコンポーネントを定義
frontend/src/app/page.tsx
import { Home } from "@/components/pages/home";
export default function Top() {
return <Home />;
}
http://localhost:3000/ にアクセスすると以下のような画面が表示されます
入力状況が不十分だとエラーが出ます
OKの場合は下記のような感じになります
バックエンド: APIクライアントの自動生成
swagger-typescript-apiを使用してバックエンドのSwaggerスキーマから自動的にフロントエンド用の型定義とAPIクライアントを生成します。
backendディレクトリで以下のコマンドを実行して必要なパッケージをインストールします。
docker-compose run --rm app yarn add -D cors @types/cors swagger-typescript-api
backend/build/swagger.json
からfrontend/schema
に自動生成します。
backend/package.json
"scripts": {
"generate": "swagger-typescript-api -p ./build/swagger.json -o ../frontend/schema -n api.ts --modular --unwrap-response-data"
}
CORSの許可URLとしてフロントエンドのURLを指定します
# 追加
FRONTEND_URL=http://localhost:3000
# 追加
FRONTEND_URL=http://localhost:3000
CORSの設定を追加し、フロントエンドからのリクエストを許可します。
backend/src/index.ts
import cors from "cors";
// 省略
const app = express();
// 追加
app.use(cors({ origin: process.env.FRONTEND_URL }));
backendディレクトリで以下のコマンドを実行し、APIクライアントの自動生成を行います。
yarn generate
swagger-typescript-apiによって以下のファイルが生成されていれば成功です。
frontend/schema
├── Users.ts # エンドポイントごとのモジュール
├── data-contracts.ts # 型定義
└── http-client.ts # HTTPクライアント
フロントエンド: API繋ぎ込み
実際に自動生成されたAPIクライアントを使ってみましょう
(本来はnew Users を生成するだけのエンドポイントファイルを作ると思いますが、簡略化するための直接クラス生成してます)
frontend/src/components/pages/home/Home.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button, Flex } from "@chakra-ui/react";
import { Users } from "../../../../schema/Users";
import { FieldInput } from "@/components/template/fieldInput";
// 省略
const onSubmit = async (data: UseFormProps) => {
const users = new Users({ baseUrl: "http://localhost:8080" });
await users.createUser({ ...data });
};
http://localhost:3000/ で送信ボタンクリックした時、DBにレコード生成れれば成功です!
バックエンド: テストコード
Jestの設定ファイルを生成しています。これにより、Expressバックエンドのテスト環境が整備され、モックデータを使ったテストが可能になります。
docker-compose run --rm app yarn add -D jest supertest ts-jest @faker-js/faker @types/jest @types/supertest
ts-jest の設定ファイルを生成
docker-compose run --rm app yarn ts-jest config:init
Jest の設定でforceExit: true
を利用して、テストが完全に終了した際にプロセスを強制終了するようにしています。
backend/jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
testEnvironment: "node",
transform: {
"^.+.tsx?$": ["ts-jest", {}],
},
roots: ["<rootDir>/src"],
forceExit: true,
};
ユーザーAPIのテストコードを記載。
本来はテストで作成したデータは削除すべきですが、実装を簡略化するためそのままにしてます。
backend/src/test/usersController.spec.ts
import request from "supertest";
import { app } from "..";
import { PrismaClient } from "@prisma/client";
import { faker } from "@faker-js/faker";
const prisma = new PrismaClient();
describe("ユーザー API", () => {
it("IDでユーザーを取得する", async () => {
const testUser = await prisma.user.create({
data: {
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.internet.password(),
},
});
const testUserId = testUser.id;
const response = await request(app).get(`/users/${testUserId}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty("id", testUserId);
});
it("ランダムなデータで新しいユーザーを作成する", async () => {
const newUser = {
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.internet.password(),
};
const response = await request(app).post("/users").send(newUser);
expect(response.status).toBe(204);
});
});
終わりに
今回のプロジェクトでは、TypeScriptを活用しながら、フロントエンドとバックエンドで統一されたスキーマを自動生成する環境を構築しました。手動でAPI仕様や型定義を管理する必要がなくなり、コードの整合性とメンテナンス性が大幅に向上したのではないでしょうか。特に、yamlファイルを記述することなく、TsoaとSwaggerを用いてスキーマ生成ができるのは、大きな生産性向上のポイントです。
今後は、Storybookを追加して、コンポーネントやAPIの動作確認をより効率的に行えるようにしていく予定です。また、認証・認可の仕組みやデプロイメント、CI/CDの構築にも取り組み、より実践的なプロダクション環境に近い形を目指します。
最後までお読みいただきありがとうございます!
Discussion