はじめに
備忘録です。
ExpressでAPI開発
駆け出しの構成から一歩踏み込んだ構成を目指す
本番環境でも耐えられるディレクトリ構成を考える
構成
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
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
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
**/node_modules
プロジェクトのルートファイル
app.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
import app from "./app";
import { PORT } from "./config";
app.listen(PORT, () => console.log("サーバーを開始します。"));
routes
routes/userRouter.ts
エンドポイントに対するControllerの振り分けを行う
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の呼び出し
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
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
// req.bodyの型を定義
type NewUser = {
name: string;
email: string;
password: string;
};
config
config/index.ts
環境変数を取得するファイル
export const PORT: number = parseInt(process.env.PORT!, 10) || 3000;
終わりに
よくみる構成に近いと思います。
最近のトレンドはDDDな気がしますが、まずははじめの一歩ということで...
実際は上記の構成にtest/とvalidation/が増えると思います。
Expressのようなディレクトリが用意されていないフレームワークで構成を考えるのは大変です。
ですが、とても勉強になります。Rails等のベストプラクティスの構成をはじめから用意してくれているフレームワークのありがたみが実感できます。
また、フレームワークのディレクトリ構成を見て、何を意図してその構成にしているのか考察できるようになるのかなと思います。