🟢

Node.js Expressのディレクトリ構成

に公開

今更Node.js Expressのディレクトリ構成についてメモ
※AIと壁打ちしながらまとめたので、誤っている点があるかもしれません

project-root/
├── src/                      # ソースコード
│   ├── app.ts                # Expressのエントリーポイント
│   ├── server.ts             # サーバー起動ロジック(appを読み込む)
│   ├── config/               # 設定ファイル(DB接続, 環境変数, ロガー設定など)
│   │   └── database.ts
│   │   └── env.ts
│   ├── routes/               # ルーティング定義
│   │   └── index.ts
│   │   └── users.ts
│   ├── controllers/          # ルートに対する処理(ビジネスロジック呼び出し)
│   │   └── userController.ts
│   ├── services/             # ビジネスロジック(アプリの中核処理)
│   │   └── userService.ts
│   ├── models/               # DBモデルやスキーマ定義
│   │   └── userModel.ts
│   ├── middlewares/          # ミドルウェア(認証・バリデーション・エラーハンドラなど)
│   │   └── authMiddleware.ts
│   ├── utils/                # 共通関数やヘルパー
│   │   └── logger.ts
│   ├── validators/           # 入力バリデーション (Joi, express-validatorなど)
│   │   └── userValidator.ts
│   └── tests/                # ユニット/統合テスト
│       └── user.test.ts
│
├── .env                      # 環境変数(dotenvで読み込む)
├── .gitignore
├── package.json
└── README.md

処理の流れ

  1. クライアントからapp.tsにアクセス
  2. app.ts → routesのURL一覧にアクセス
  3. routes → controllersにてresponseを返す
    ※controllersでvalidationなども行う
  4. controllers → servicesにてDBにアクセス
    ※modelsの型を利用

サンプルコード

prisma & MySQLを例に

server.ts

サーバーの起動

import dotenv from "dotenv";
import http from "http";
import app from "./app";
import { prisma } from "./models/userModel"; // Prisma Client

// 環境変数ロード
dotenv.config();

const PORT = process.env.PORT || 3000;

// HTTPサーバー作成
const server = http.createServer(app);

// DB接続確認 → サーバー起動
async function startServer() {
  try {
    await prisma.$connect();
    console.log("✅ Connected to MySQL");

    server.listen(PORT, () => {
      console.log(`🚀 Server running on http://localhost:${PORT}`);
    });
  } catch (error) {
    console.error("❌ Failed to connect to DB:", error);
    process.exit(1);
  }
}

startServer();

app.ts

大元のファイル

import express, { Application, Request, Response } from "express";
import cors from "cors";
import morgan from "morgan";

import userRoutes from "./routes/userRoutes";

const app: Application = express();

// ミドルウェア
app.use(cors());
app.use(express.json());
app.use(morgan("dev"));

// ルーティング
app.use("/api/users", userRoutes);

// ヘルスチェック
app.get("/health", (req: Request, res: Response) => {
  res.json({ status: "ok" });
});

export default app;

routes

URLを書く

import { Router } from "express";
import { getUserById } from "../controllers/userController";

const router = Router();
router.get("/:id", getUserById);

export default router;

controllers

response部分を書く

import { Request, Response } from "express";
import { findUserById } from "../services/userService";

export const getUserById = async (req: Request, res: Response) => {
  const user = await findUserById(Number(req.params.id));
  if (!user) {
    return res.status(404).json({ message: "User not found" });
  }
  res.json(user);
};

services

データ取得 & 整形メソッド

import { prisma } from "../models/userModel";

export const findUserById = async (id: number) => {
  return await prisma.user.findUnique({
    where: { id },
  });
};

validators

バリデーション格納

import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';

export const createUserSchema = z.object({
  name: z.string().min(1, { message: '名前は必須です' }),
  email: z.string().email({ message: '正しいメールアドレスを入力してください' }),
});

models

DBに合わせた型の設定など

import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();

export interface User {
  id: string;
  name: string;
  email: string;
}

Discussion