├── .gitignore ├── .node-version ├── .vscode └── launch.json ├── README.md ├── bin └── .gitkeep ├── copyStaticAssets.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app.ts ├── app │ ├── service │ │ ├── README.md │ │ └── user_service.ts │ └── usecase │ │ └── README.md ├── config │ └── README.md ├── domain │ ├── basic │ │ └── README.md │ └── model │ │ ├── README.md │ │ └── user │ │ ├── user.ts │ │ ├── user_date_of_birth.ts │ │ ├── user_first_name.ts │ │ ├── user_identifier.ts │ │ ├── user_last_name.ts │ │ ├── user_list.ts │ │ ├── user_organization.ts │ │ └── user_repository.ts ├── infrastructure │ ├── README.md │ ├── datasource │ │ ├── README.md │ │ └── user │ │ │ └── user_datasource.ts │ ├── logging │ │ └── logger.ts │ └── transfer │ │ └── README.md ├── presentation │ ├── controller │ │ ├── README.md │ │ ├── concern │ │ │ ├── README.md │ │ │ └── common.ts │ │ ├── root_controller.ts │ │ └── user_controller.ts │ ├── routes.ts │ └── view │ │ ├── README.md │ │ └── user │ │ └── user_view.ts ├── public │ └── css │ │ ├── navbar-top-fixed.css │ │ └── style.css └── server.ts ├── templates ├── error.pug ├── index.pug ├── layout.pug ├── shared │ └── nav.pug └── user │ ├── edit.pug │ ├── index.pug │ ├── new.pug │ └── show.pug ├── test └── .gitkeep ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # End of https://www.gitignore.io/api/node 84 | 85 | # typescript build output 86 | dist 87 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.13.1 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // IntelliSense を使用して利用可能な属性を学べます。 3 | // 既存の属性の説明をホバーして表示します。 4 | // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "プログラムの起動", 11 | "program": "${workspaceFolder}/serve" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Launch Program", 17 | "program": "${workspaceFolder}/dist/server" 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "ts-node debug", 23 | "args": [ 24 | "${workspaceRoot}/src/server.ts" 25 | ], 26 | "runtimeArgs": [ 27 | "-r", 28 | "ts-node/register" 29 | ], 30 | "cwd": "${workspaceRoot}", 31 | "protocol": "inspector", 32 | // "internalConsoleOptions": "openOnSessionStart", 33 | // "env": { 34 | // "TS_NODE_IGNORE": "false" 35 | // } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDD boilerplate with Node.js + Express + TypeScript 2 | 3 | ## 概要 4 | 5 | Node.js + Express のサーバ構築の際に DDD で実装したいと思い、 6 | そうするとデータ型や interface がある TypeScript の方がよさそう、 7 | ということで作ってみたときのテンプレートです。 8 | 9 | ## 参考 10 | 11 | - https://github.com/Microsoft/TypeScript-Node-Starter 12 | - https://github.com/joshuaalpuerto/node-ddd-boilerplate 13 | 14 | ## アーキテクチャ 15 | 16 | ### 設計方針 17 | 18 | - DDD(ドメイン駆動設計) 19 | 20 | ### サーバー 21 | 22 | - Language: Node.js, TypeScript 23 | - Web Framework: Express 24 | 25 | - DB: MySQL 26 | - ORM: Sequelize 27 | 28 | ### クライアント 29 | 30 | - JavaScript (予定) 31 | 32 | ## 環境構築手順 33 | 34 | ```sh 35 | npm install 36 | 37 | # development 38 | npm run serve-ts 39 | # production 40 | npm run build && npm start 41 | ``` 42 | 43 | Access to http://localhost:3000/users 44 | 45 | ## このリポジトリの作り方(メモ) 46 | 47 | ```sh 48 | express --pug {project_name} 49 | 50 | cd {project_name} 51 | git init 52 | ``` 53 | 54 | あとはコミット履歴を参照。 55 | 56 | ### bootstrap の追加 57 | 58 | `bootstrap`ブランチを参照 59 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take8/express-typescript-ddd/17979f9131074aa9b1d70453b195f35197965fbd/bin/.gitkeep -------------------------------------------------------------------------------- /copyStaticAssets.ts: -------------------------------------------------------------------------------- 1 | import * as shell from "shelljs"; 2 | 3 | shell.mkdir("-p", "dist/public") 4 | 5 | shell.cp("-R", "src/public/js/lib", "dist/public/js/"); 6 | shell.cp("-R", "src/public/css", "dist/public/"); 7 | // shell.cp("-R", "src/public/fonts", "dist/public/"); 8 | shell.cp("-R", "src/public/images", "dist/public/"); 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | tsConfigFile: 'tsconfig.json' 5 | } 6 | }, 7 | moduleFileExtensions: [ 8 | 'ts', 9 | 'js' 10 | ], 11 | transform: { 12 | '^.+\\.(ts|tsx)$': './node_modules/ts-jest/preprocessor.js' 13 | }, 14 | testMatch: [ 15 | '**/test/**/*.test.(ts|js)' 16 | ], 17 | testEnvironment: 'node' 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-ts-ddd", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "build": "npm run build-ts && npm run tslint && npm run copy-static-assets", 8 | "serve": "node dist/server.js", 9 | "serve-ts": "npx ts-node src/server.ts", 10 | "watch-node": "nodemon dist/server.js", 11 | "watch": "concurrently -k -p \"[{name}]\" -n \"Sass,TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-sass\" \"npm run watch-ts\" \"npm run watch-node\"", 12 | "test": "jest --forceExit --coverage --verbose", 13 | "watch-test": "npm run test -- --watchAll", 14 | "build-ts": "tsc", 15 | "watch-ts": "tsc -w", 16 | "tslint": "tslint -c tslint.json -p tsconfig.json", 17 | "copy-static-assets": "ts-node copyStaticAssets.ts", 18 | "debug": "npm run build && npm run watch-debug", 19 | "serve-debug": "nodemon --inspect dist/server.js", 20 | "watch-debug": "concurrently -k -p \"[{name}]\" -n \"Sass,TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-sass\" \"npm run watch-ts\" \"npm run serve-debug\"" 21 | }, 22 | "dependencies": { 23 | "@popperjs/core": "^2.11.0", 24 | "bootstrap": "^5.1.3", 25 | "cookie-parser": "^1.4.4", 26 | "csurf": "^1.11.0", 27 | "debug": "~2.6.9", 28 | "express": "^4.17.1", 29 | "express-session": "^1.17.2", 30 | "http-errors": "~1.6.2", 31 | "log4js": "^4.3.1", 32 | "method-override": "^3.0.0", 33 | "moment": "^2.24.0", 34 | "morgan": "~1.9.0", 35 | "pug": "^3.0.2", 36 | "uuid": "^3.3.2" 37 | }, 38 | "devDependencies": { 39 | "@types/cookie-parser": "^1.4.1", 40 | "@types/csurf": "^1.11.2", 41 | "@types/debug": "^4.1.5", 42 | "@types/express": "^4.17.13", 43 | "@types/express-session": "^1.17.4", 44 | "@types/http-errors": "^1.6.1", 45 | "@types/jest": "^24.0.13", 46 | "@types/log4js": "^2.3.5", 47 | "@types/method-override": "0.0.31", 48 | "@types/moment": "^2.13.0", 49 | "@types/morgan": "^1.7.35", 50 | "@types/node": "^10.14.8", 51 | "@types/shelljs": "^0.8.5", 52 | "@types/uuid": "^3.4.4", 53 | "jest": "^24.8.0", 54 | "node-sass": "^4.14.1", 55 | "nodemon": "^1.19.1", 56 | "shelljs": "^0.8.3", 57 | "ts-jest": "^24.0.2", 58 | "ts-node": "^7.0.1", 59 | "tslint": "^5.17.0", 60 | "typescript": "^3.5.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import createError from "http-errors"; 2 | import express from "express"; 3 | import path from "path"; 4 | 5 | import cookieParser from "cookie-parser"; 6 | import session from "express-session"; 7 | import csrf from "csurf"; 8 | 9 | // import logger from "morgan"; 10 | import logger from "./infrastructure/logging/logger"; 11 | 12 | // routing 13 | import methodOverride from "method-override"; // ルーティングの前に読み込む 14 | import routes from "./presentation/routes"; 15 | 16 | class App { 17 | public express: express.Application = express(); 18 | 19 | constructor() { 20 | this.initMiddleware(); 21 | this.initRouter(); 22 | this.initErrorHandler(); 23 | } 24 | 25 | private initMiddleware() { 26 | // view engine setup 27 | this.express.set("views", path.join(__dirname, "../templates")); 28 | this.express.set("view engine", "pug"); 29 | 30 | // logger 31 | // this.express.use(logger("dev")); 32 | 33 | this.express.use(express.json()); 34 | this.express.use(express.urlencoded({ extended: false })); 35 | 36 | // CSRF prevention 37 | this.express.use(cookieParser()); 38 | this.express.use(session({ 39 | resave: false, 40 | saveUninitialized: false, 41 | secret: "secret" 42 | })); 43 | this.express.use(csrf()); 44 | this.express.use((req, res, next) => { 45 | res.locals.csrfToken = req.csrfToken(); 46 | next(); 47 | }); 48 | 49 | // static files 50 | this.express.use(express.static(path.join(__dirname, "public"))); 51 | // Access to node_modules 52 | // TODO: Danger? 53 | this.express.use("/node_modules", express.static(path.join(__dirname, "../node_modules"))); 54 | } 55 | 56 | private initRouter() { 57 | // routing 58 | this.express.use(methodOverride("_method")); 59 | this.express.use("/", routes); 60 | } 61 | 62 | private initErrorHandler() { 63 | // catch 404 and forward to error handler 64 | this.express.use(function ( 65 | req: express.Request, 66 | res: express.Response, 67 | next: express.NextFunction 68 | ) { 69 | logger.error("Error 404"); 70 | // next(createError(404)); 71 | res.send(createError(404)); 72 | }); 73 | 74 | // error handler 75 | this.express.use(function ( 76 | err: any, 77 | req: express.Request, 78 | res: express.Response, 79 | next: express.NextFunction 80 | ) { 81 | // set locals, only providing error in development 82 | res.locals.message = err.message; 83 | res.locals.error = req.app.get("env") === "development" ? err : {}; 84 | 85 | // render the error page 86 | const status = err.status || 500; 87 | res.status(status); 88 | logger.error("Error " + status); 89 | res.render("error"); 90 | }); 91 | } 92 | } 93 | 94 | export default new App().express; 95 | -------------------------------------------------------------------------------- /src/app/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | 3 | > DDDのサービス層 4 | -------------------------------------------------------------------------------- /src/app/service/user_service.ts: -------------------------------------------------------------------------------- 1 | import userRepository from "../../infrastructure/datasource/user/user_datasource"; 2 | import { UserIdentifier } from "../../domain/model/user/user_identifier"; 3 | import { User } from "../../domain/model/user/user"; 4 | 5 | class UserService { 6 | constructor() { 7 | } 8 | 9 | findAll() { 10 | return userRepository.findAll(); 11 | } 12 | 13 | findBy(identifier: UserIdentifier) { 14 | return userRepository.findBy(identifier); 15 | } 16 | 17 | startInput() { 18 | return User.newInstance(); 19 | } 20 | 21 | create(user: User) { 22 | return userRepository.create(user); 23 | } 24 | 25 | update(user: User) { 26 | return userRepository.update(user); 27 | } 28 | 29 | delete(identifier: UserIdentifier) { 30 | return userRepository.delete(identifier); 31 | } 32 | } 33 | 34 | // TODO: 簡易的な singleton 35 | export default new UserService(); 36 | -------------------------------------------------------------------------------- /src/app/usecase/README.md: -------------------------------------------------------------------------------- 1 | # Usecase 2 | 3 | > 機能(サービス)を組み合わせた、ユースケースシナリオを記述する 4 | -------------------------------------------------------------------------------- /src/config/README.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | > 各種ツール(ORM, loggerなど)の設定ファイル置き場 4 | -------------------------------------------------------------------------------- /src/domain/basic/README.md: -------------------------------------------------------------------------------- 1 | # Domain (Basic) 2 | 3 | > 特定の業務に依存しない基本的なドメイン 4 | -------------------------------------------------------------------------------- /src/domain/model/README.md: -------------------------------------------------------------------------------- 1 | # Domain 2 | 3 | > DDDのドメイン層 4 | > 業務の知識、利用者の関心事を記述する 5 | -------------------------------------------------------------------------------- /src/domain/model/user/user.ts: -------------------------------------------------------------------------------- 1 | import { UserIdentifier } from "./user_identifier"; 2 | import { UserFirstName } from "./user_first_name"; 3 | import { UserLastName } from "./user_last_name"; 4 | import { UserDateOfBirth } from "./user_date_of_birth"; 5 | import { UserOrganization } from "./user_organization"; 6 | 7 | export class User { 8 | private _identifier: UserIdentifier; 9 | private _firstName: UserFirstName; 10 | private _lastName: UserLastName; 11 | private _dateOfBirth: UserDateOfBirth; 12 | private _organization: UserOrganization; 13 | 14 | private constructor() { 15 | this._identifier = UserIdentifier.generate(); 16 | } 17 | 18 | static newInstance() { 19 | return new User(); 20 | } 21 | 22 | static of( 23 | _firstName: UserFirstName, 24 | _lastName: UserLastName, 25 | _dateOfBirth: UserDateOfBirth, 26 | _organization: UserOrganization 27 | ) { 28 | const user = new User(); 29 | user._firstName = _firstName; 30 | user._lastName = _lastName; 31 | user._dateOfBirth = _dateOfBirth; 32 | user._organization = _organization; 33 | return user; 34 | } 35 | 36 | static withIdentifier( 37 | _identifier: UserIdentifier, 38 | _firstName: UserFirstName, 39 | _lastName: UserLastName, 40 | _dateOfBirth: UserDateOfBirth, 41 | _organization: UserOrganization 42 | ) { 43 | const user = User.of(_firstName, _lastName, _dateOfBirth, _organization); 44 | user._identifier = _identifier; 45 | return user; 46 | } 47 | 48 | identifier() { 49 | return this._identifier; 50 | } 51 | 52 | firstName() { 53 | return this._firstName; 54 | } 55 | 56 | lastName() { 57 | return this._lastName; 58 | } 59 | 60 | dateOfBirth() { 61 | return this._dateOfBirth; 62 | } 63 | 64 | organization() { 65 | return this._organization; 66 | } 67 | 68 | /** 69 | * フルネームを返す。 70 | */ 71 | fullName(): string { 72 | return `${this._lastName.value()} ${this._firstName.value()}`; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/domain/model/user/user_date_of_birth.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export class UserDateOfBirth { 4 | private _value: Date; 5 | 6 | constructor(_value: Date) { 7 | this._value = _value; 8 | } 9 | 10 | value() { 11 | return moment(this._value).format("YYYY-MM-DD"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/domain/model/user/user_first_name.ts: -------------------------------------------------------------------------------- 1 | export class UserFirstName { 2 | private _value: string; 3 | 4 | constructor(_value: string) { 5 | this._value = _value; 6 | } 7 | 8 | value() { 9 | return this._value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/domain/model/user/user_identifier.ts: -------------------------------------------------------------------------------- 1 | import UUID from "uuid"; 2 | 3 | export class UserIdentifier { 4 | private _value: string; 5 | 6 | constructor(_value: string) { 7 | this._value = _value; 8 | } 9 | 10 | static generate() { 11 | const identifier = UUID.v4(); 12 | return new UserIdentifier(identifier); 13 | } 14 | 15 | value() { 16 | return this._value; 17 | } 18 | 19 | static of(identifier: string) { 20 | return new UserIdentifier(identifier); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/domain/model/user/user_last_name.ts: -------------------------------------------------------------------------------- 1 | export class UserLastName { 2 | private _value: string; 3 | 4 | constructor(_value: string) { 5 | this._value = _value; 6 | } 7 | 8 | value() { 9 | return this._value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/domain/model/user/user_list.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user"; 2 | 3 | export class UserList { 4 | private _values: User[]; 5 | 6 | constructor(_values: User[]) { 7 | this._values = _values; 8 | } 9 | 10 | values() { 11 | return this._values; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/domain/model/user/user_organization.ts: -------------------------------------------------------------------------------- 1 | export class UserOrganization { 2 | private _value: string; 3 | 4 | constructor(_value: string) { 5 | this._value = _value; 6 | } 7 | 8 | value() { 9 | return this._value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/domain/model/user/user_repository.ts: -------------------------------------------------------------------------------- 1 | import { UserIdentifier } from "./user_identifier"; 2 | import { User } from "./user"; 3 | import { UserList } from "./user_list"; 4 | 5 | export interface UserRepository { 6 | findAll(): UserList; 7 | 8 | findBy(identifier: UserIdentifier): User; 9 | 10 | create(user: User): void; 11 | 12 | update(user: User): void; 13 | 14 | delete(identifier: UserIdentifier): void; 15 | } 16 | -------------------------------------------------------------------------------- /src/infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Infrastructure 2 | 3 | > 技術基盤 4 | > DB, 外部APIなど 5 | > このレイヤのクラスを他のレイヤのクラスは直接呼び出さないこと 6 | > 使う側の意図をインタフェースで宣言(= Repository) 7 | > 実装クラスを、このレイヤで宣言(命名ガイド:実装技術名+インタフェース名) 8 | -------------------------------------------------------------------------------- /src/infrastructure/datasource/README.md: -------------------------------------------------------------------------------- 1 | # Datasource 2 | 3 | > データベースアクセスの実装クラス群 4 | -------------------------------------------------------------------------------- /src/infrastructure/datasource/user/user_datasource.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from "../../../domain/model/user/user_repository"; 2 | 3 | import { UserList } from "../../../domain/model/user/user_list"; 4 | import { User } from "../../../domain/model/user/user"; 5 | import { UserIdentifier } from "../../../domain/model/user/user_identifier"; 6 | import { UserLastName } from "../../../domain/model/user/user_last_name"; 7 | import { UserFirstName } from "../../../domain/model/user/user_first_name"; 8 | import { UserDateOfBirth } from "../../../domain/model/user/user_date_of_birth"; 9 | import { UserOrganization } from "../../../domain/model/user/user_organization"; 10 | 11 | import moment from "moment"; 12 | 13 | class UserDatasource implements UserRepository { 14 | // TODO: dummy 15 | private dataMap: { [index: string]: User } = {}; 16 | 17 | constructor() { 18 | const user1 = User.of( 19 | new UserFirstName("太郎"), 20 | new UserLastName("山田"), 21 | new UserDateOfBirth(moment("1999-11-22").toDate()), 22 | new UserOrganization("A社") 23 | ); 24 | const user2 = User.of( 25 | new UserFirstName("次郎"), 26 | new UserLastName("鈴木"), 27 | new UserDateOfBirth(moment("2001-04-30").toDate()), 28 | new UserOrganization("B社") 29 | ); 30 | this.dataMap[user1.identifier().value()] = user1; 31 | this.dataMap[user2.identifier().value()] = user2; 32 | } 33 | 34 | findAll(): UserList { 35 | return new UserList(Object.values(this.dataMap)); 36 | } 37 | 38 | findBy(identifier: UserIdentifier): User { 39 | return this.dataMap[identifier.value()]; 40 | } 41 | 42 | create(user: User): void { 43 | this.dataMap[user.identifier().value()] = user; 44 | } 45 | 46 | update(user: User): void { 47 | this.dataMap[user.identifier().value()] = user; 48 | } 49 | 50 | delete(identifier: UserIdentifier): void { 51 | delete this.dataMap[identifier.value()]; 52 | } 53 | } 54 | 55 | export default new UserDatasource(); 56 | -------------------------------------------------------------------------------- /src/infrastructure/logging/logger.ts: -------------------------------------------------------------------------------- 1 | import * as Log4js from "log4js"; 2 | 3 | // Log4js.configure('./filename'); 4 | Log4js.configure({ 5 | appenders: { 6 | // 標準出力 7 | stdout: { 8 | type: "stdout" 9 | }, 10 | // ファイル出力 11 | system: { 12 | type: "dateFile", 13 | // プロジェクトルートディレクトリを起点とした相対パスで解釈される 14 | // filename: path.join(__dirname, "../logs/system.log"), 15 | filename: "./logs/system.log", 16 | // `filename` の後ろにこのパターンでファイル名が付けられる 17 | pattern: ".yyyy-MM-dd", 18 | // `true` を指定すると、ローテートしたファイル名の末尾に拡張子が付く 19 | keepFileExt: true, 20 | // `true` を指定すると、ローテートしたファイルを .gz 形式で圧縮してくれる 21 | compress: true, 22 | // この数以上にログファイルが溜まると、古いファイルを削除してくれる 23 | daysToKeep: 5 24 | } 25 | }, 26 | categories: { 27 | // 標準出力とファイルの両方に出力する 28 | default: { 29 | appenders: ["stdout", "system"], 30 | level: "info" 31 | } 32 | } 33 | }); 34 | const logger = Log4js.getLogger(); 35 | logger.info("logger initialized."); 36 | 37 | export default logger; 38 | -------------------------------------------------------------------------------- /src/infrastructure/transfer/README.md: -------------------------------------------------------------------------------- 1 | # Transfer 2 | 3 | > 外部との通信の実装クラス 4 | -------------------------------------------------------------------------------- /src/presentation/controller/README.md: -------------------------------------------------------------------------------- 1 | # Controller 2 | 3 | > コントローラー層 4 | -------------------------------------------------------------------------------- /src/presentation/controller/concern/README.md: -------------------------------------------------------------------------------- 1 | # Concern 2 | 3 | > コントローラの共通処理(Ruby on Rails のネーミングを流用) 4 | -------------------------------------------------------------------------------- /src/presentation/controller/concern/common.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import logger from "../../../infrastructure/logging/logger"; 3 | 4 | /** 5 | * call log 6 | */ 7 | export const callLog = (req: Request, res: Response, next: NextFunction) => { 8 | logger.info(`${req.method} ${req.path}: START`); 9 | next(); 10 | logger.info(`${req.method} ${req.path}: END`); 11 | }; 12 | -------------------------------------------------------------------------------- /src/presentation/controller/root_controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | /** 4 | * GET / 5 | * Home page. 6 | */ 7 | export let index = (req: Request, res: Response, next: NextFunction) => { 8 | res.render("index", { title: "Express" }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/presentation/controller/user_controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import userService from "../../app/service/user_service"; 3 | import userView from "../view/user/user_view"; 4 | import { UserIdentifier } from "../../domain/model/user/user_identifier"; 5 | 6 | /** 7 | * List users. 8 | */ 9 | export let index = (req: Request, res: Response, next: NextFunction) => { 10 | const userList = userService.findAll(); 11 | res.render("user/index", { title: "Users", userList: userList }); 12 | }; 13 | 14 | /** 15 | * Show user. 16 | */ 17 | export let show = (req: Request, res: Response, next: NextFunction) => { 18 | // TODO: validation 19 | const id = req.params.id; 20 | const identifier = UserIdentifier.of(id); 21 | const user = userService.findBy(identifier); 22 | res.render("user/show", { title: `Show User: ${user.fullName()}`, user: user }); 23 | }; 24 | 25 | /** 26 | * Render user form to create. 27 | */ 28 | export let _new = (req: Request, res: Response, next: NextFunction) => { 29 | const user = userService.startInput(); 30 | res.render("user/new", { title: "New User", user: user }); 31 | }; 32 | 33 | /** 34 | * Create user. 35 | */ 36 | export let create = (req: Request, res: Response, next: NextFunction) => { 37 | // TODO: validation 38 | const user = userView.toDomain(req.body); 39 | userService.create(user); 40 | res.redirect("/users"); 41 | }; 42 | 43 | /** 44 | * Render user form to update. 45 | */ 46 | export let edit = (req: Request, res: Response, next: NextFunction) => { 47 | // TODO: validation 48 | const id = req.params.id; 49 | const identifier = UserIdentifier.of(id); 50 | const user = userService.findBy(identifier); 51 | res.render("user/edit", { title: `Edit User: ${user.fullName()}`, user: user }); 52 | }; 53 | 54 | /** 55 | * Update user. 56 | */ 57 | export let update = (req: Request, res: Response, next: NextFunction) => { 58 | // TODO: validation 59 | const user = userView.toDomain(req.body); 60 | userService.update(user); 61 | res.redirect("/users"); 62 | }; 63 | 64 | /** 65 | * Delete user. 66 | */ 67 | export let _delete = (req: Request, res: Response, next: NextFunction) => { 68 | // TODO: validation 69 | const id = req.params.id; 70 | const identifier = UserIdentifier.of(id); 71 | userService.delete(identifier); 72 | res.redirect("/users"); 73 | }; 74 | -------------------------------------------------------------------------------- /src/presentation/routes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | // Controllers (route handlers) 3 | import * as rootController from "./controller/root_controller"; 4 | import * as userController from "./controller/user_controller"; 5 | 6 | import { callLog } from "./controller/concern/common"; 7 | 8 | const router = express.Router(); 9 | 10 | /** 11 | * Primary app routes. 12 | */ 13 | router.get("/", callLog, rootController.index); 14 | 15 | router.get("/users", callLog, userController.index); 16 | router.get("/users/new", callLog, userController._new); 17 | router.post("/users", callLog, userController.create); 18 | router.get("/users/:id/edit", callLog, userController.edit); 19 | router.put("/users/:id", callLog, userController.update); 20 | router.delete("/users/:id", callLog, userController._delete); 21 | router.get("/users/:id", callLog, userController.show); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/presentation/view/README.md: -------------------------------------------------------------------------------- 1 | # View 2 | 3 | > ビューを表現するクラス群 4 | > JSON <-> オブジェクト マッピングクラス 5 | > View Helper 6 | > etc. 7 | > 8 | > ビューテンプレートではなく、ビューテンプレートとオブジェクトのマッピングを行う 9 | -------------------------------------------------------------------------------- /src/presentation/view/user/user_view.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../../domain/model/user/user"; 2 | import { UserFirstName } from "../../../domain/model/user/user_first_name"; 3 | import { UserLastName } from "../../../domain/model/user/user_last_name"; 4 | import { UserDateOfBirth } from "../../../domain/model/user/user_date_of_birth"; 5 | import { UserOrganization } from "../../../domain/model/user/user_organization"; 6 | import { UserIdentifier } from "../../../domain/model/user/user_identifier"; 7 | 8 | interface UserObject { 9 | identifier: string; 10 | firstName: string; 11 | lastName: string; 12 | dateOfBirth: Date; 13 | organization: string; 14 | } 15 | 16 | /** 17 | * リクエストボディとドメインモデルの変換を行う。 18 | */ 19 | class UserView { 20 | constructor() { 21 | } 22 | 23 | toDomain(body: UserObject): User { 24 | return User.withIdentifier( 25 | new UserIdentifier(body.identifier), 26 | new UserFirstName(body.firstName), 27 | new UserLastName(body.lastName), 28 | new UserDateOfBirth(body.dateOfBirth), 29 | new UserOrganization(body.organization) 30 | ); 31 | } 32 | } 33 | 34 | export default new UserView(); 35 | -------------------------------------------------------------------------------- /src/public/css/navbar-top-fixed.css: -------------------------------------------------------------------------------- 1 | /* Show it is fixed to the top */ 2 | body { 3 | min-height: 75rem; 4 | padding-top: 4.5rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00b7ff; 8 | } 9 | 10 | /* -------- Users -------- */ 11 | /* .c-table { 12 | border: #ccc 1px solid; 13 | } 14 | 15 | .c-table-header { 16 | } 17 | 18 | .c-table-row { 19 | } */ 20 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | import app from "./app"; 7 | // const debug = require("debug")("express-ts-ddd:server"); 8 | import debug from "debug"; 9 | import http from "http"; 10 | 11 | debug("express-ts-ddd:server"); 12 | 13 | /** 14 | * Get port from environment and store in Express. 15 | */ 16 | const port = normalizePort(process.env.PORT || "3000"); 17 | app.set("port", port); 18 | 19 | /** 20 | * Create HTTP server. 21 | */ 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | server.listen(port); 28 | server.on("error", onError); 29 | server.on("listening", onListening); 30 | 31 | /** 32 | * Normalize a port into a number, string, or false. 33 | */ 34 | function normalizePort(val: string) { 35 | const port = parseInt(val, 10); 36 | 37 | if (isNaN(port)) { 38 | // named pipe 39 | return val; 40 | } 41 | 42 | if (port >= 0) { 43 | // port number 44 | return port; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * Event listener for HTTP server "error" event. 52 | */ 53 | function onError(error: any) { 54 | if (error.syscall !== "listen") { 55 | throw error; 56 | } 57 | 58 | const bind = typeof port === "string" 59 | ? "Pipe " + port 60 | : "Port " + port; 61 | 62 | // handle specific listen errors with friendly messages 63 | switch (error.code) { 64 | case "EACCES": 65 | console.error(bind + " requires elevated privileges"); 66 | process.exit(1); 67 | break; 68 | case "EADDRINUSE": 69 | console.error(bind + " is already in use"); 70 | process.exit(1); 71 | break; 72 | default: 73 | throw error; 74 | } 75 | } 76 | 77 | /** 78 | * Event listener for HTTP server "listening" event. 79 | */ 80 | function onListening() { 81 | const addr = server.address(); 82 | const bind = typeof addr === "string" 83 | ? "pipe " + addr 84 | : "port " + addr.port; 85 | debug("Listening on " + bind); 86 | } 87 | -------------------------------------------------------------------------------- /templates/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /templates/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /templates/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/css/style.css') 6 | 7 | // bootstrap 8 | link(rel="stylesheet", href="/node_modules/bootstrap/dist/css/bootstrap.min.css") 9 | script(src="/node_modules/@popperjs/core/dist/umd/popper.min.js") 10 | script(src="/node_modules/bootstrap/dist/js/bootstrap.min.js") 11 | 12 | block head 13 | 14 | body 15 | block content 16 | -------------------------------------------------------------------------------- /templates/shared/nav.pug: -------------------------------------------------------------------------------- 1 | nav.navbar.navbar-expand-md.navbar-dark.fixed-top.bg-dark 2 | h1.d-none ナビゲーションメニュー 3 | div.container-fluid 4 | button.navbar-toggler.navbar-toggler-right(type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation") 5 | span.navbar-toggler-icon 6 | a.navbar-brand(href="#") Fixed navbar 7 | 8 | div.collapse.navbar-collapse#navbarCollapse 9 | ul.navbar-nav.me-auto 10 | //- TODO: "active"を切り替えるロジックが必要 11 | li.nav-item 12 | //- インライン要素は interpolation (#[ ... ]) だとすっきり書ける 13 | a.nav-link(href="/") Home #[span.sr-only (current)] 14 | li.nav-item.active 15 | a.nav-link(href="/users") Users 16 | //- li.nav-item 17 | //- a.nav-link.disabled(href="#") Disabled 18 | 19 | //- form.form-inline.mt-2.mt-md-0 20 | //- input.form-control.me-sm-2(type="text" placeholder="Search") 21 | //- button.btn.btn-outline-success.my-2.my-sm-0(type="submit") Search 22 | -------------------------------------------------------------------------------- /templates/user/edit.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block head 4 | link(rel="stylesheet", href="/css/navbar-top-fixed.css") 5 | 6 | block content 7 | include ../shared/nav 8 | 9 | main 10 | //- "="の後のテキストはエスケープされる 11 | h1= title 12 | hr 13 | 14 | form(action=`/users/${user.identifier().value()}?_method=PUT`, method="POST") 15 | input(type="hidden", name="_csrf" value=csrfToken) 16 | input(type="hidden", name="identifier" value=`${user.identifier().value()}`) 17 | div.row.mb-3 18 | div.col-md-2 19 | label.form-label(for="inputFirstName") FirstName 20 | input#inputFirstName.form-control(type="text", name="firstName" value=`${user.firstName().value()}`) 21 | div.col-md-2 22 | label.form-label(for="inputLastName") LastName 23 | input#inputLastName.form-control(type="text", name="lastName" value=`${user.lastName().value()}`) 24 | div.row.mb-3 25 | div.col-md-4 26 | label.form-label(for="inputDateOfBirth") Date of birth 27 | input#inputDateOfBirth.form-control(type="date" name="dateOfBirth" value=`${user.dateOfBirth().value()}`) 28 | div.row.mb-3 29 | div.col-md-4 30 | label.form-label(for="inputOrganization") Organization 31 | input#inputOrganization.form-control(type="text" name="organization" value=`${user.organization().value()}`) 32 | 33 | div.my-2 34 | button.btn.btn-primary(type="submit") Save 35 | -------------------------------------------------------------------------------- /templates/user/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block head 4 | link(rel="stylesheet", href="/css/navbar-top-fixed.css") 5 | 6 | block content 7 | include ../shared/nav 8 | 9 | main 10 | //- "="の後のテキストはエスケープされる 11 | h1= title 12 | hr 13 | //- p Welcome to #{title} 14 | 15 | div.mb-2 16 | a.btn.btn-success(href="/users/new") New 17 | //- divは省略可能 18 | .table-responsive 19 | table.table.table-striped 20 | thead 21 | tr 22 | th # 23 | th 氏名 24 | th 生年月日 25 | th 組織 26 | th 27 | tbody 28 | each user in userList.values() 29 | tr 30 | td.align-middle #{user.identifier().value()} 31 | td.align-middle #{user.fullName()} 32 | td.align-middle #{user.dateOfBirth().value()} 33 | td.align-middle #{user.organization().value()} 34 | td.align-middle 35 | a.btn.btn-secondary(href=`/users/${user.identifier().value()}`).me-1 Show 36 | a.btn.btn-info(href=`/users/${user.identifier().value()}/edit`).me-1 Edit 37 | form.d-inline(action=`/users/${user.identifier().value()}?_method=DELETE`, method="POST") 38 | input(type="hidden", name="_csrf" value=csrfToken) 39 | button.btn.btn-danger(type="submit") Delete 40 | 41 | //- TODO: tableタグを使わないようにしたい 42 | //- //- divは省略可能 43 | //- .container.c-table 44 | //- .row.c-table-header 45 | //- .col-3 氏名 46 | //- .col-3 生年月日 47 | //- .col-3 組織 48 | //- .col-3 49 | //- .row.c-table-row 50 | //- .col-3 Yamada Taro 51 | //- .col-3 1999-11-22 52 | //- .col-3 A company 53 | //- .col-3 54 | //- a.btn.btn-info(href="#").me-1 Edit 55 | //- a.btn.btn-danger(href="#") Delete 56 | //- .row.c-table-row 57 | //- .col-3 Yamada Taro 58 | //- .col-3 1999-11-22 59 | //- .col-3 A company 60 | //- .col-3 61 | //- a.btn.btn-info(href="#").me-1 Edit 62 | //- a.btn.btn-danger(href="#") Delete 63 | -------------------------------------------------------------------------------- /templates/user/new.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block head 4 | link(rel="stylesheet", href="/css/navbar-top-fixed.css") 5 | 6 | block content 7 | include ../shared/nav 8 | 9 | main 10 | //- "="の後のテキストはエスケープされる 11 | h1= title 12 | hr 13 | 14 | form(action="/users", method="POST") 15 | input(type="hidden", name="_csrf" value=csrfToken) 16 | input(type="hidden", name="identifier" value=`${user.identifier().value()}`) 17 | div.row.mb-3 18 | div.col-md-2 19 | label.form-label(for="inputFirstName") FirstName 20 | input#inputFirstName.form-control(type="text", name="firstName") 21 | div.col-md-2 22 | label.form-label(for="inputLastName") LastName 23 | input#inputLastName.form-control(type="text", name="lastName") 24 | div.row.mb-3 25 | div.col-md-4 26 | label.form-label(for="inputDateOfBirth") Date of birth 27 | input#inputDateOfBirth.form-control(type="date", name="dateOfBirth") 28 | div.row.mb-3 29 | div.col-md-4 30 | label.form-label(for="inputOrganization") Organization 31 | input#inputOrganization.form-control(type="text", name="organization") 32 | 33 | div.my-2 34 | button.btn.btn-primary(type="submit") Save 35 | -------------------------------------------------------------------------------- /templates/user/show.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block head 4 | link(rel="stylesheet", href="/css/navbar-top-fixed.css") 5 | 6 | block content 7 | include ../shared/nav 8 | 9 | main 10 | //- "="の後のテキストはエスケープされる 11 | h1= title 12 | hr 13 | 14 | form(action="", method="get") 15 | input(type="hidden", name="_csrf" value=csrfToken) 16 | input(type="hidden", name="identifier" value=`${user.identifier().value()}`) 17 | div.row.mb-3 18 | div.col-md-2 19 | label.form-label(for="inputFirstName") FirstName 20 | input#inputFirstName.form-control(type="text", name="firstName" disabled value=`${user.firstName().value()}`) 21 | div.col-md-2 22 | label.form-label(for="inputLastName") LastName 23 | input#inputLastName.form-control(type="text", name="lastName" disabled value=`${user.lastName().value()}`) 24 | div.row.mb-3 25 | div.col-md-4 26 | label.form-label(for="inputDateOfBirth") Date of birth 27 | input#inputDateOfBirth.form-control(type="date" name="dateOfBirth" disabled value=`${user.dateOfBirth().value()}`) 28 | div.row.mb-3 29 | div.col-md-4 30 | label.form-label(for="inputOrganization") Organization 31 | input#inputOrganization.form-control(type="text" name="organization" disabled value=`${user.organization().value()}`) 32 | 33 | //- div.my-2 34 | //- button.btn.btn-primary(type="submit") Save 35 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take8/express-typescript-ddd/17979f9131074aa9b1d70453b195f35197965fbd/test/.gitkeep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es6", 5 | "lib": [ "es2015" ], 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | // "paths": { 14 | // "*": [ 15 | // "node_modules/*", 16 | // "src/types/*" 17 | // ] 18 | // }, 19 | "allowJs": true, 20 | // TODO: JsDocの型チェックをできる 21 | // "checkJs": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "one-line": [ 13 | true, 14 | "check-open-brace", 15 | "check-whitespace" 16 | ], 17 | "no-var-keyword": true, 18 | "quotemark": [ 19 | true, 20 | "double", 21 | "avoid-escape" 22 | ], 23 | "semicolon": [ 24 | true, 25 | "always", 26 | "ignore-bound-class-methods" 27 | ], 28 | "whitespace": [ 29 | true, 30 | "check-branch", 31 | "check-decl", 32 | "check-operator", 33 | "check-module", 34 | "check-separator", 35 | "check-type" 36 | ], 37 | "typedef-whitespace": [ 38 | true, 39 | { 40 | "call-signature": "nospace", 41 | "index-signature": "nospace", 42 | "parameter": "nospace", 43 | "property-declaration": "nospace", 44 | "variable-declaration": "nospace" 45 | }, 46 | { 47 | "call-signature": "onespace", 48 | "index-signature": "onespace", 49 | "parameter": "onespace", 50 | "property-declaration": "onespace", 51 | "variable-declaration": "onespace" 52 | } 53 | ], 54 | "no-internal-module": true, 55 | "no-trailing-whitespace": true, 56 | "no-null-keyword": true, 57 | "prefer-const": true, 58 | "jsdoc-format": true 59 | } 60 | } 61 | --------------------------------------------------------------------------------