Express API Docker

## はじめに
備忘録です。

ExpressでAPI開発
駆け出しの構成から一歩踏み込んだ構成を目指す



*本番環境でも耐えられるディレクトリ構成を考える*

## 構成
```shell
backend/
  node_modules/
  prisma/
  src/
    config/
      - index.ts
    controllers/
      - indexController.ts
      - userController.ts
    models/
      - index.ts
      - users.ts
    routes/
      - indexRouter.ts
      - userRouter.ts
    services/
      - indexService.ts
      - userService.ts
    - app.ts
    - server.ts
  - .env
  - package-lock.json
  - package.json
  - tsconfig.json
db/
docker/
  backend/
    - Dockerfile
- .dockerignore
- .gitignore
- docker-compose.yml
```

## Dcoker関連

`docker/backend/Dockerfile`
```Dockerfile
FROM node:18.14.2-buster

WORKDIR /usr/src/app

COPY ./backend/package*.json ./

RUN npm install
# 本番環境用の場合
# RUN npm install --only=production

COPY ./backend .

# コンテナ起動後にexecで起動するか
# shellで入って起動した方が利便性高そう
# DBが立ち上がる前に接続しようとするので気をつける
# CMD ["npm", "start"]
```

`docker-compose.yml`
```yml
version: "3.8"
services:
  backend:
    build:
      context: .
      dockerfile: ./docker/backend/Dockerfile
    env_file:
      - ./backend/.env
    tty: true
    depends_on:
      - db
    volumes:
      - ./backend:/usr/src/app
    ports:
      - "3000:3000"
    environment:
      - TZ=Asia/Tokyo
  db:
    image: mysql:8.0.31
    platform: linux/amd64
    env_file: ./db/.env
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    volumes:
      - ./db/data:/var/lib/mysql
      - ./db/conf.d:/etc/mysql/conf.d
    ports:
      - 3306:3306
    environment:
      TZ: Asia/Tokyo
```

```.dockerignore
**/node_modules
```

## プロジェクトのルートファイル
`app.ts`
```ts
import express, { Request, Response } from "express";
import helmet from "helmet";
import cors from "cors";
import compression from "compression";
import morgan from "morgan";
import * as dotenv from "dotenv";
dotenv.config();

import indexRouter from "./routes/indexRouter";
import userRouter from "./routes/userRouter";

const app = express();

app.use(express.json());
app.use(cors());
app.use(helmet());
app.use(compression());
app.use(morgan("dev"));

// Routes
app.use("/", indexRouter);
app.use("/users", userRouter);

export default app;
```

`server.ts`
```ts
import app from "./app";
import { PORT } from "./config";

app.listen(PORT, () => console.log("サーバーを開始します。"));
```

## routes
`routes/userRouter.ts`
*エンドポイントに対するControllerの振り分けを行う*
```ts
import express from "express";
import { UsersController } from "../controllers/userController";

// Controllerをインスタンス化
const usersController = new UsersController();
const router = express.Router();

router.get("/", usersController.findAll);
router.post("/", usersController.create);

export default router;
```

## Controller
`controllers/userController.ts`
try, catchの処理
req.bodyの型定義
tryの中でServiceの呼び出し
```ts
import { Request, Response } from "express";
import { UserService } from "../services/userService";
import { NewUser } from "../models";

// Serviceをインスタンス化
const userService = new UserService();

export class UsersController {
  async findAll(req: Request, res: Response) {
    try {
      const users = await userService.findAll();
      res.status(200).json(users);
    } catch (err) {
      res.status(500).json(err);
    }
  }

// req.bodyの型を指定している
  async create(req: Request<{}, {}, NewUser>, res: Response) {
    try {
      const user = await userService.create(req.body);
      res.status(200).json(user);
    } catch (err) {
      res.status(500).json(err);
    }
  }
}
```

## Service
`services/userService.ts`
```ts
import { PrismaClient, User } from "@prisma/client";
import { NewUser } from "../models";

const prisma = new PrismaClient();

// 返り値はprismaからmodelを取得
export class UserService {
  async findAll(): Promise<User[]> {
    return await prisma.user.findMany();
  }

  async create(newUser: NewUser): Promise<User> {
    return await prisma.user.create({
      data: newUser,
    });
  }
}
```

## models
`models/users.ts`
```ts
// req.bodyの型を定義
type NewUser = {
  name: string;
  email: string;
  password: string;
};
```

## config
`config/index.ts`
*環境変数を取得するファイル*
```ts
export const PORT: number = parseInt(process.env.PORT!, 10) || 3000;
```

## 終わりに
よくみる構成に近いと思います。
最近のトレンドはDDDな気がしますが、まずははじめの一歩ということで...
実際は上記の構成にtest/とvalidation/が増えると思います。
Expressのようなディレクトリが用意されていないフレームワークで構成を考えるのは大変です。
ですが、とても勉強になります。Rails等のベストプラクティスの構成をはじめから用意してくれているフレームワークのありがたみが実感できます。
また、フレームワークのディレクトリ構成を見て、何を意図してその構成にしているのか考察できるようになるのかなと思います。