目次
環境
API
- Node v18.17.1
- Express v4.18.2
- TypeScript v5.2.2
DB
- Prisma v5.2.0
- mysql v8.0
ローカル環境
Docker
Httpリクエストの準備
post manやThunder Client等のクライアントライブラリを用意すること。フロントエンドの実装はPart3で行うため、それまではこちらからリクエストのテストを実施する
https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client
APIのCRUD実装
前準備
ファイル、ディレクトリ準備
細かくディレクトリやファイルを分けたいので、その準備を行う(不要な場合はスキップしてもらってもOK)
modulesディレクトリを作成しその中に責務ごとにファイルを配置
// ユーザー関連の機能を集約するディレクトリ
mkdir -p modules/user
mkdir modules/user/{repository,routes,useCase,utils}
// DB層へのアクセス
touch modules/user/{repository/UserRepository.ts,repository/UserRepositoryImpl.ts}
 
// エンドポイント定義
touch modules/user/routes/user.ts
// ユーザーのリソースに対する具体的なユースケース(時間の関係上、createとfindのみ実装。他はTODO)
touch modules/user/{useCase/CreateUserUseCase.ts,useCase/FindUserUseCase.ts}
// バリデーションやエラーハンドラーなどのutil
touch modules/user/{utils/errorHandler.ts,utils/validation.ts}expressの処理を分割
api/main.tsに全て突っ込むと必ずゴチャつくので、スッキリさせたい。インスタンス生成は、api/config/express.tsに、ルーティング定義は、api/modules/user/routes/user.tsに定義する
api/main.ts
import app from "./config/express";
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Running at Port ${port}...`);
});api/config/express.ts
import express, { Application } from "express";
import cors from "cors";
import user from "../modules/user/routes/user";
const app: Application = express();
app.use(express.json());
const allowedOrigins = ["http://localhost:3000"];
const options: cors.CorsOptions = {
  origin: allowedOrigins,
};
app.use(cors(options));
app.use("/users", user);
export default app;api/modules/user/routes/user.ts
import express, { Request, Response } from "express";
const router = express.Router();
/**
 * ユーザー新規作成
 */
router.post("", async (req: Request, res: Response) => {
//  後ほど実装
});
/**
 * ユーザー一覧取得
 */
router.get("", async (req: Request, res: Response) => {
//  後ほど実装
});
/**
 * ユーザー取得
 */
router.get("/:id", async (req: Request, res: Response) => {
//  後ほど実装
});
/**
 * ユーザー更新
 */
router.patch("", async (req: Request, res: Response) => {
//  後ほど実装 
});
/**
 * ユーザー削除
 */
router.delete("", async (req: Request, res: Response) => {
//  後ほど実装
});
export default router;
Create
実装
api/modules/user/routes/user.ts
import express, { Request, Response } from "express";
const router = express.Router();
/**
 * ユーザー新規作成
 */
router.post("", async (req: Request, res: Response) => {
  // 追加
  const { email, name } = req.body.data;
  const useCase = new CreateUserUseCase();
  const user = await useCase.create(name, email);
  return res.status(200).json({
    user,
  });
});
/**
 * ユーザー一覧取得
 */
router.get("", async (req: Request, res: Response) => {
  //  後ほど実装
});
/**
 * ユーザー取得
 */
router.get("/:id", async (req: Request, res: Response) => {
  //  後ほど実装
});
/**
 * ユーザー更新
 */
router.patch("", async (req: Request, res: Response) => {
  //  後ほど実装
});
/**
 * ユーザー削除
 */
router.delete("", async (req: Request, res: Response) => {
  //  後ほど実装
});
export default router;api/modules/user/useCase/CreateUserUseCase.ts
import { User } from "@prisma/client";
import { UserRepository } from "../repository/UserRepository";
import { UserRepositoryImpl } from "../repository/UserRepositoryImpl";
export class CreateUserUseCase {
  private userRepository: UserRepository;
  public constructor() {
    // TODO: DI
    this.userRepository = new UserRepositoryImpl();
  }
  public async create(name: string, email: string): Promise<User | null> {
    return await this.userRepository.save(name, email);
  }
}api/modules/user/repository/UserRepository.ts
import { User } from "@prisma/client";
export interface UserRepository {
  save(name: string, email: string): Promise<User>;
}api/modules/user/repository/UserRepositoryImpl.ts
import { PrismaClient, User } from "@prisma/client";
import { UserRepository } from "./UserRepository";
export class UserRepositoryImpl implements UserRepository {
  async save(name: string, email: string): Promise<User> {
    const prisma = new PrismaClient();
    const user = await prisma.user.create({
      data: {
        email,
        name,
      },
    });
    return user;
  }
}リクエスト検証
Thunder Clientを開き、POST、urlをhttp://localhost:18000/usersを指定。Sendをクリックし、右側のステータスが200で作成したユーザー情報が返ってきていればOK。送信するjsonは以下のような感じ
{
    "data" : {
        "name": "hoge",
        "email": "hoge@gmail.com"
    }
}
適当に、もう一人ユーザーを追加しておく
Read
実装
api/modules/user/routes/user.ts
import express, { Request, Response } from "express";
import { CreateUserUseCase } from "../useCase/CreateUserUseCase";
import { FindUserUseCase } from "../useCase/FindUserUseCase";
import { PrismaClient } from "@prisma/client";
const router = express.Router();
const prisma = new PrismaClient({
  errorFormat: "minimal",
});
/**
 * ユーザー新規作成
 */
router.post("", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー一覧取得
 */
router.get("", async (req: Request, res: Response) => {
  // T0D0: ユースケースに切り出す
  const users = await prisma.user.findMany();
  return res.status(200).json({
    users,
  });
});
/**
 * ユーザー取得
 */
router.get("/:id", async (req: Request, res: Response) => {
  const useCase = new FindUserUseCase();
  const user = await useCase.find(req.params.id);
  return res.status(200).json({
    user,
  });
});
/**
 * ユーザー更新
 */
router.patch("", async (req: Request, res: Response) => {
  //  後ほど実装
});
/**
 * ユーザー削除
 */
router.delete("", async (req: Request, res: Response) => {
  //  後ほど実装
});
export default router;api/modules/user/useCase/FindUserUseCase.ts
import { User } from "@prisma/client";
import { UserRepositoryImpl } from "../repository/UserRepositoryImpl";
import { UserRepository } from "../repository/UserRepository";
export class FindUserUseCase {
  private userRepository: UserRepository;
  public constructor() {
    // TODO: DI
    this.userRepository = new UserRepositoryImpl();
  }
  public async find(id: string): Promise<User | null> {
    return await this.userRepository.find(id);
  }
}api/modules/user/repository/UserRepository.ts
import { User } from "@prisma/client";
export interface UserRepository {
  save(name: string, email: string): Promise<User>;
  // 追加
  find(id: string): Promise<User | null>;
}api/modules/user/repository/UserRepositoryImpl.ts
import { PrismaClient, User } from "@prisma/client";
import { UserRepository } from "./UserRepository";
export class UserRepositoryImpl implements UserRepository {
  async save(name: string, email: string): Promise<User> {
    const prisma = new PrismaClient();
    const user = await prisma.user.create({
      data: {
        email,
        name,
      },
    });
    return user;
  }
  async find(id: string): Promise<User | null> {
    const prisma = new PrismaClient();
    return await prisma.user.findUnique({
      where: { id },
    });
  }
}リクエスト検証
Thunder Clientを開き、GET、urlをhttp://localhost:18000/usersを指定。Sendをクリックし、右側のステータスが200で、作成済みユーザーが全件返ってきていればOK。

また、ID指定での1件取得も確認。GET、urlをhttp://localhost:18000/users/<userId>を指定。

Update
api/modules/user/routes/user.ts
import express, { Request, Response } from "express";
import { CreateUserUseCase } from "../useCase/CreateUserUseCase";
import { PrismaClient } from "@prisma/client";
import { FindUserUseCase } from "../useCase/FindUserUseCase";
const router = express.Router();
const prisma = new PrismaClient({
  errorFormat: "minimal",
});
/**
 * ユーザー新規作成
 */
router.post("", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー一覧取得
 */
router.get("", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー取得
 */
router.get("/:id", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー更新
 */
router.patch("", async (req: Request, res: Response) => {
  const { id, email, name } = req.body.data;
  // T0D0: ユースケースに切り出す
  const user = await prisma.user.update({
    where: {
      id,
    },
    data: {
      email,
      name,
    },
  });
  return res.status(200).json({
    user,
  });
});
/**
 * ユーザー削除
 */
router.delete("", async (req: Request, res: Response) => {
  //  後ほど実装
});
export default router;
// {
//     "data" : {
//         "name": "hoge",
//         "email": "hoge@gmail.com"
//     }
// }
リクエスト検証
Thunder Clientを開き、PATCH、urlをhttp://localhost:18000/usersを指定。Sendをクリックし、右側のステータスが200で、指定ユーザーの名前とメールアドレスが変わった状態で返ってきていればOK。

Delete
api/modules/user/routes/user.ts
import express, { Request, Response } from "express";
import { CreateUserUseCase } from "../useCase/CreateUserUseCase";
import { PrismaClient } from "@prisma/client";
import { FindUserUseCase } from "../useCase/FindUserUseCase";
const router = express.Router();
const prisma = new PrismaClient({
  errorFormat: "minimal",
});
/**
 * ユーザー新規作成
 */
router.post("", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー一覧取得
 */
router.get("", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー取得
 */
router.get("/:id", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー更新
 */
router.patch("", async (req: Request, res: Response) => {
  ・・・
});
/**
 * ユーザー削除
 */
router.delete("", async (req: Request, res: Response) => {
  const { id } = req.body;
  // T0D0: ユースケースに切り出す
  const user = await prisma.user.delete({
    where: {
      id,
    },
  });
  return res.status(200).json({
    user,
  });
});
export default router;リクエスト検証
Thunder Clientを開き、DELETE、urlをhttp://localhost:18000/usersを指定。Sendをクリックし、右側のステータスが200で、指定ユーザーが削除され、削除したユーザ情報が返ってきていればOK。

Part2はここまで。Part3ではフロントエンドのアプリケーションを作成し、APIと通信する。
まとめ
いかがでしたでしょうか。本記事は、ExpressとPrismaでAPIをサクッと作るシリーズのPart2です。Part2では主にPart1で作成したPrisma Clientを駆使してAPIの実装とリクエストの検証を行なっています。
