Masayan tech blog.

  1. ブログ記事一覧>
  2. ExpressとPrismaでサクッと作るAPI:Part2(APIのCRUD実装)

ExpressとPrismaでサクッと作るAPI:Part2(APIのCRUD実装)

公開日

目次

  • 環境
  • Httpリクエストの準備
  • APIのCRUD実装
  • まとめ

環境

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の実装とリクエストの検証を行なっています。