diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..cce063c
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,16 @@
+# 빌드 산출물
+dist/
+coverage/
+logs/
+node_modules/
+
+# 설정 파일
+*.config.js
+*.config.ts
+
+# 환경파일 등
+.env
+.env.*
+
+# 기타
+!.eslintrc.js
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..5847758
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,18 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "extends": ["plugin:@typescript-eslint/recommended", "prettier"],
+ "parserOptions": {
+ "ecmaVersion": 2022,
+ "sourceType": "module"
+ },
+ "env": {
+ "node": true,
+ "es2022": true
+ },
+ "rules": {
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/no-var-requires": "off"
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug-report---.md b/.github/ISSUE_TEMPLATE/bug-report---.md
deleted file mode 100644
index 2d741f8..0000000
--- a/.github/ISSUE_TEMPLATE/bug-report---.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-name: "Bug Report(버그 보고서) \U0001F41B"
-about: "Create a report to help us improve"
-title: ""
-labels: "\U0001F41E Bug"
-assignees: "ljlm0402"
----
-
-### Describe the Bug (버그 설명)
-
-
-
-### Version to Reproduce (현재 사용한 버전)
-
-
-
-### Steps to Reproduce (재현 순서)
-
-
-
-### Expected Behavior (예상 동작)
-
-
-
-### Actual Behavior (실제 동작)
-
-
-
-### Additional Context (추가 사항)
-
-
-
-### Capture screen (캡쳐 화면)
-
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 0000000..b9a430c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,52 @@
+name: "\U0001F41B Bug Report"
+description: Report a bug or unexpected behavior.
+title: "[Bug] "
+labels: [bug]
+assignees: "ljlm0402"
+body:
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: Description
+ description: Please describe the bug in detail.
+ placeholder: ex) The app crashes when clicking the button.
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to Reproduce
+ description: List the steps to reproduce the bug.
+ placeholder: |
+ 1. Open the app
+ 2. Click the X button
+ 3. See the error
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ description: What did you expect to happen?
+ placeholder: ex) The screen should change normally.
+ validations:
+ required: true
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots / Logs
+ description: Attach screenshots or logs if available.
+ - type: input
+ id: env
+ attributes:
+ label: Environment
+ description: "OS, browser, version, etc. (ex: Windows 11, Chrome 124)"
+ validations:
+ required: false
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched for existing issues.
+ - label: I have checked the relevant documentation.
diff --git a/.github/ISSUE_TEMPLATE/feature-request---.md b/.github/ISSUE_TEMPLATE/feature-request---.md
deleted file mode 100644
index f73f982..0000000
--- a/.github/ISSUE_TEMPLATE/feature-request---.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-name: "Feature Request(기능 요청서) \U0001F4A1"
-about: "Suggest an idea for this project"
-title: ""
-labels: "\U000026A1 Feature"
-assignees: "ljlm0402"
----
-
-### Motivation (새로운 기능 설명)
-
-
-
-### Proposed Solution (기능을 통해 얻고자 하는 솔루션)
-
-
-
-### Alternatives (제안 된 솔루션이 더 나은 이유)
-
-
-
-### Additional Context (추가 사항)
-
-
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 0000000..e8b43f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,35 @@
+name: "\U0001F4A1 Feature Request"
+description: Suggest a new feature or improvement.
+title: "[Feature] "
+labels: [enhancement]
+assignees: "ljlm0402"
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: Summary
+ description: Briefly describe the feature or improvement.
+ placeholder: ex) Add notification feature for users
+ validations:
+ required: true
+ - type: textarea
+ id: detail
+ attributes:
+ label: Details
+ description: Describe the feature or improvement in detail.
+ placeholder: ex) Send email notifications when users log in successfully.
+ validations:
+ required: true
+ - type: textarea
+ id: reason
+ attributes:
+ label: Reason / Benefit
+ description: Why is this needed? What benefits are expected?
+ placeholder: ex) Reduce churn by notifying users of key events.
+ validations:
+ required: false
+ - type: textarea
+ id: etc
+ attributes:
+ label: Additional Context
+ description: Any references, links, or related screenshots.
diff --git a/.github/ISSUE_TEMPLATE/qna.yaml b/.github/ISSUE_TEMPLATE/qna.yaml
new file mode 100644
index 0000000..a69c784
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/qna.yaml
@@ -0,0 +1,35 @@
+name: "\U00002753 Q&A"
+description: Ask a question or request clarification.
+title: "[Q&A] "
+labels: [question]
+assignees: "ljlm0402"
+body:
+ - type: textarea
+ id: question
+ attributes:
+ label: Question
+ description: Please describe your question or what you need help with.
+ placeholder: ex) How can I customize the login page UI?
+ validations:
+ required: true
+ - type: textarea
+ id: attempts
+ attributes:
+ label: What have you tried?
+ description: Describe any attempts, research, or steps you've already taken to solve the issue.
+ placeholder: ex) I checked the documentation and tried modifying `login.scss`, but it had no effect.
+ validations:
+ required: false
+ - type: textarea
+ id: expected
+ attributes:
+ label: What kind of answer would be helpful?
+ description: Let us know what kind of information or solution you are looking for.
+ placeholder: ex) Sample code or documentation reference would be helpful.
+ validations:
+ required: false
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Any other details, screenshots, or references that may help.
diff --git a/.github/ISSUE_TEMPLATE/question---.md b/.github/ISSUE_TEMPLATE/question---.md
deleted file mode 100644
index 454e916..0000000
--- a/.github/ISSUE_TEMPLATE/question---.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: "Question(문의) \U00002753"
-about: "Ask a question about this project"
-title: ""
-labels: "\U00002754 Question"
-assignees: "ljlm0402"
----
-
-### Summary (요약)
-
-
-
-### Additional Details (추가적인 세부 사항)
-
-
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 5c9c67a..6ee0c62 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,36 +1,22 @@
-## Content (작업 내용) 📝
+## 📌 Summary
-
-
+
-### Screenshot for work content (작업 내용 스크린샷) 📸
+## 💡 Reason for Change
-
-
+
-## Links (링크) 🔗
+## ✅ Checklist
-- Issue Links (이슈 링크)
+- [ ] I have followed the code/documentation style guidelines.
+- [ ] I have tested the relevant functionality.
+- [ ] This change does not negatively affect existing features.
+- [ ] I have linked related issues/commits/documents below if necessary.
-- API Links (API 스팩 문서)
+## 🔗 Related Issues (optional)
-- Development Links (개발 문서)
+- #issue_number
-- Document Links (기획 문서)
+## 📝 Additional Notes
-- Design Links (디자인 문서)
-
-## Etc (기타 사항) 🔖
-
-
-
-
-## Check List (체크 사항) ✅
-
-
-
-
-- [ ] Issue (이슈)
-- [ ] Tests (테스트)
-- [ ] Ready to be merged (병합 준비 완료)
-- [ ] Added myself to contributors table (기여자 추가)
+
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4efe235..837b2a3 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,26 +1,24 @@
name: npm publish
on:
- pull_request:
- branches:
- - master
- paths:
- - package.json
+ push:
+ tags:
+ - "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- - name: ⬇️ Checkout source code
- uses: actions/checkout@v2
+ - name: ⬇️ Checkout Repository
+ uses: actions/checkout@v3
- name: ⎔ Setup nodejs
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v4
with:
- node-version: 16
+ node-version: lts/*
registry-url: https://registry.npmjs.org/
- - name: 🏗 Publish npm
+ - name: 🏗 Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
diff --git a/.npmignore b/.npmignore
index 26f5c15..9143bde 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1 +1,8 @@
-.github
+.vscode/
+.github/
+.eslintrc
+eslint.config.js
+.prettierrc
+.prettierignore
+pnpm-lock.yaml
+*.log
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..1f67368
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "printWidth": 150,
+ "tabWidth": 2,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "semi": true,
+ "arrowParens": "avoid"
+}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
deleted file mode 100644
index 789d7b5..0000000
--- a/CONTRIBUTORS.md
+++ /dev/null
@@ -1,69 +0,0 @@
-- Jeongwon Kim [https://github.com/swtpumpkin](https://github.com/swtpumpkin)
-
-- João Silva [https://github.com/joaopms](https://github.com/joaopms)
-
-- BitYoungjae [https://github.com/BitYoungjae](https://github.com/BitYoungjae)
-
-- Paolo Tagliani [https://github.com/pablosproject](https://github.com/pablosproject)
-
-- Lloyd Park [https://github.com/yeondam88](https://github.com/yeondam88)
-
-- strama4 [https://github.com/strama4](https://github.com/strama4)
-
-- sonbyungjun [https://github.com/sonbyungjun](https://github.com/sonbyungjun)
-
-- Sean Maxwell [https://github.com/seanpmaxwell](https://github.com/seanpmaxwell)
-
-- Ed Guy [https://github.com/edguy3](https://github.com/edguy3)
-
-- Malavan [https://github.com/malavancs](https://github.com/malavancs)
-
-- Jon Gallant [https://github.com/jongio](https://github.com/jongio)
-
-- Kuba Rozkwitalski [https://github.com/kubarozkwitalski](https://github.com/kubarozkwitalski)
-
-- Craig Harman [https://github.com/craigharman](https://github.com/craigharman)
-
-- Edward Teixeira Dias Junior [https://github.com/edward-teixeira](https://github.com/edward-teixeira)
-
-- n2ptune [https://github.com/n2ptune](https://github.com/n2ptune)
-
-- michael r [https://github.com/alanmynah](https://github.com/alanmynah)
-
-- Benjamin [https://github.com/benjaminudoh10](https://github.com/benjaminudoh10)
-
-- Amrik Singh [https://github.com/AmrikSD](https://github.com/AmrikSD)
-
-- oricc [https://github.com/oricc](https://github.com/oricc)
-
-- Dustin Newbold [https://github.com/dustinnewbold](https://github.com/dustinnewbold)
-
-- WhatIfWeDigDeeper [https://github.com/WhatIfWeDigDeeper](https://github.com/WhatIfWeDigDeeper)
-
-- David Stewart [https://github.com/davidjmstewart](https://github.com/davidjmstewart)
-
-- JagTheFriend [JagTheFriend](https://github.com/JagTheFriend)
-
-- Tamzid Karim [Tamzid Karim](https://github.com/tamzidkarim)
-
-- Andrija Milojević [https://github.com/andrija29](https://github.com/andrija29)
-
-- Engjell Avdiu [https://github.com/engjellavdiu](https://github.com/engjellavdiu)
-
-- Florian Mifsud [https://github.com/florianmifsud](https://github.com/florianmifsud)
-
-- Rashid [https://github.com/rashidmya](https://github.com/rashidmya)
-
-- JoshuaOLoduca [https://github.com/JoshuaOLoduca](https://github.com/JoshuaOLoduca)
-
-- Markus Laubscher [https://github.com/markuslaubscher](https://github.com/markuslaubscher)
-
-- coder-palak [https://github.com/coder-palak](https://github.com/coder-palak)
-
-- H2RO [https://github.com/primary-1](https://github.com/primary-1)
-
-- Emmanuel Yeboah [https://github.com/noelzappy](https://github.com/noelzappy)
-
-- Jonathan Felicity [https://github.com/jonathanfelicity](https://github.com/jonathanfelicity)
-
-- Hichem Fantar [https://github.com/hichemfantar](https://github.com/hichemfantar)
diff --git a/README.kr.md b/README.kr.md
index ee31b6c..33c95cf 100644
--- a/README.kr.md
+++ b/README.kr.md
@@ -1,13 +1,13 @@
-
+
- 타입스크립트 익스프레스 스타터
+ TypeScript Express Starter
-🚀 타입스크립트 기반의 익스프레스 보일러 플레이트 스타터 패키지
+🚀 TypeScript 기반 Express RESTful API 보일러플레이트
@@ -20,31 +20,16 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [🇰🇷 한국어](https://github.com/ljlm0402/typescript-express-starter/blob/master/README.kr.md)
@@ -52,272 +37,142 @@
-## 😎 프로젝트를 소개합니다.
-
-Express는 유형 정의에 취약한 JavaScript로 구성 되어있습니다.
-
-이것이 바로 TypeScript를 도입하는 스타터 패키지로 수퍼 세트를 피하는 이유입니다.
-
-패키지는 JavaScript 대신 TypeScript를 사용하도록 구성되어 있습니다.
-
-> 참고 : [express-generator-typescript](https://github.com/seanpmaxwell/express-generator-typescript)
-
-### 🤔 Express는 무엇인가요 ?
-
-Node.js를 위한 빠르고 개방적인 간결한 웹 프레임워크입니다.
-
-## 🚀 시작하기
-
-### npm 전역 설치
-
-```bash
-$ npm install -g typescript-express-starter
-```
-
-### npx를 통해 프로젝트를 설치
-
-프로젝트 이름을 입력하지 않으면, 기본값으로 _typescript-express-starter_ 폴더로 설치됩니다.
-
-```bash
-$ npx typescript-express-starter "project name"
-```
-
-### 원하시는 템플릿을 선택
-
-
-
-설치가 완료되면 Script 명령어를 통해 프로젝트를 실행합니다.
-
-#### 템플릿 종류
+## 📝 소개
-| 이름 | 설명 |
-| :---------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| Default | Express 기본 |
-| [routing controllers](https://github.com/typestack/routing-controllers) | 데코레이터 사용량이 많은 구조화되고 선언적이며 아름답게 구성된 클래스 기반 컨트롤러 생성 |
-| [Sequelize](https://github.com/sequelize/sequelize) | PostgreSQL, MySQL, MariaDB, SQLite, Microsoft SQL Server를 지원하는 Promise 패턴 기반의 Node.js ORM |
-| [Mongoose](https://github.com/Automattic/mongoose) | Node.js와 MongoDB를 위한 ODM(Object Data Mapping) 라이브러리 |
-| [TypeORM](https://github.com/typeorm/typeorm) | 자바스크립트, 타입스크립트과 함께 사용되어 Node.js, React Native, Expo에서 실행될 수 있는 ORM |
-| [Prisma](https://github.com/prisma/prisma) | 데이터베이스에 데이터를 프로그래밍 언어의 객체와 매핑하여 기존에 SQL로 작성하던 데이터를 수정, 테이블 구조 변경등의 작업을 객체를 통해 프로그래밍적으로 할 수 있도록 해주는 ORM |
-| [Knex](https://github.com/knex/knex) | 쿼리 빌더를 위한 라이브러리 |
-| [GraphQL](https://github.com/graphql/graphql-js) | API 용 쿼리 언어이며 기존 데이터로 이러한 쿼리를 수행하기위한 런타임 |
-| [Typegoose](https://github.com/typegoose/typegoose) | 타입스크립트 클래스를 사용하여 몽구스 모델 정의 |
-| [Mikro ORM](https://github.com/mikro-orm/mikro-orm) | 데이터 매퍼, 작업 단위 및 아이덴티티 맵 패턴을 기반으로 하는 Node.js용 TypeScript ORM. MongoDB, MySQL, MariaDB, PostgreSQL 및 SQLite 데이터베이스를 지원 |
-| [Node Postgres](https://node-postgres.com/) | PostgreSQL 데이터베이스와 인터페이스하기 위한 node.js 모듈 |
-
-#### 추후 개발 할 템플릿
-
-| 이름 | 설명 |
-| :------------------------------------------------------------------------------ | :------------------------------------------------------------------ |
-| [Sequelize Typescript](https://github.com/RobinBuschmann/sequelize-typescript) | 데코레이터 및 Sequelize를 위한 몇 가지 기능 |
-| [TS SQL](https://github.com/codemix/ts-sql) | SQL 데이터베이스는 TypeScript 유형 주석으로 순전히 구현 |
-| [inversify-express-utils](https://github.com/inversify/inversify-express-utils) | InversifyJS를 사용한 Express 애플리케이션 개발을 위한 일부 유틸리티 |
-| [postgress Typescript]() | |
-| [graphql-prisma]() | |
-
-## 🛎 Script 명령어
-
-- 프로덕션 모드 실행 : `npm run start` 아니면 `Start typescript-express-starter` VS Code 로
-- 개발 모드 실행 : `npm run dev` 아니면 `Dev typescript-express-starter` VS Code 로
-- 단위 테스트 : `npm test` 아니면 `Test typescript-express-starter` VS Code 로
-- 코드 포맷터 검사 : `npm run lint` 아니면 `Lint typescript-express-starter` VS Code 로
-- 코드 포맷터 적용 : `npm run lint:fix` 아니면 `Lint:Fix typescript-express-starter` VS Code 로
-
-## 💎 프로젝트 기능
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+**TypeScript Express Starter**는 안정적이고 확장 가능한 RESTful API 서버를 빠르게 구축할 수 있는 보일러플레이트입니다.
+Express의 유연함과 간결함에 TypeScript의 타입 안정성을 결합하여, 프로토타입 단계부터 프로덕션까지 품질과 유지보수성을 보장합니다.
-### 🐳 Docker :: 컨테이너 플랫폼
+- 깔끔한 아키텍처와 모듈 구조
-[Docker](https://docs.docker.com/)란, 컨테이너 기반의 오픈소스 가상화 플랫폼이다.
+- 기본 내장 보안, 로깅, 유효성 검사, 개발 도구
-[설치 홈페이지](https://docs.docker.com/get-docker/)에 접속해서 설치를 해줍니다.
+- 빠른 개발과 안정적인 배포 환경 지원
-- 백그라운드에서 컨테이너를 시작하고 실행 : `docker-compose up -d`
-- 컨테이너를 중지하고 컨테이너, 네트워크, 볼륨 및 이미지를 제거 : `docker-compose down`
+## 💎 주요 기능
-수정을 원하시면 `docker-compose.yml`과 `Dockerfile`를 수정해주시면 됩니다.
+- ⚡ **TypeScript + Express** — 최신 JavaScript와 완전한 타입 안정성 제공
-### ♻️ Nginx :: 웹 서버
+- 📜 **API 문서** — Swagger/OpenAPI를 기본 제공
-[Nginx](https://www.nginx.com/) 역방향 프록시,로드 밸런서, 메일 프록시 및 HTTP 캐시로도 사용할 수있는 웹 서버입니다.
+- 🛡 **보안** — Helmet, CORS, HPP, 요청 속도 제한(rate limiting) 기본 포함
-프록시는 일반적으로 여러 서버에로드를 분산하거나, 다른 웹 사이트의 콘텐츠를 원활하게 표시하거나, HTTP 이외의 프로토콜을 통해 처리 요청을 애플리케이션 서버에 전달하는 데 사용됩니다.
+- 🧩 **유효성 검사** — Zod 기반의 스키마 런타임 유효성 검사
-Nginx 요청을 프록시하면 지정된 프록시 서버로 요청을 보내고 응답을 가져 와서 클라이언트로 다시 보냅니다.
+- 🔗 **의존성 주입** — tsyringe를 활용한 경량 DI 지원
-수정을 원하시면 `nginx.conf` 파일을 수정해주시면 됩니다.
+- 🗄 **데이터베이스 연동** — Sequelize, Prisma, Mongoose, TypeORM, Knex, Drizzle 등 지원
-### ✨ ESLint, Prettier :: 정적 코드 분석 및 코드 스타일 변환
+- 🛠 **개발 도구** — ESLint, Prettier, Jest, Docker, PM2, NGINX, Makefile 포함
-[ESLint](https://eslint.org/)는 JavaScript 코드에서 발견 된 문제 패턴을 식별하기위한 정적 코드 분석 도구입니다.
+- 🧱 **모듈형 아키텍처** — 손쉽게 확장 및 유지보수 가능
-[Prettier](https://prettier.io/)는 개발자가 작성한 코드를 정해진 코딩 스타일을 따르도록 변환해주는 도구입니다.
+- 🚀 **프로덕션 준비 완료** — Docker, PM2, NGINX 지원
-코드를 구문 분석하고 최대 줄 길이를 고려하여 필요한 경우 코드를 래핑하는 자체 규칙으로 다시 인쇄하여 일관된 스타일을 적용합니다.
+## ⚡️ 빠른 시작
-1. [VSCode](https://code.visualstudio.com/) Extension에서 [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode), [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 설치합니다.
-
-2. 설치가 완료되면, 단축키 `CMD` + `Shift` + `P` (Mac Os) 또는 `Ctrl` + `Shift` + `P` (Windows) 입력합니다.
-
-3. Format Selection With 선택합니다.
-
-4. Configure Default Formatter... 선택합니다.
-
-5. Prettier - Code formatter 적용합니다.
-
-
-
-> 2019년, TSLint 지원이 종료 되어 ESLint를 적용하였습니다.
-
-### 📗 Swagger :: API 문서화
-
-[Swagger](https://swagger.io/)는 개발자가 REST 웹 서비스를 설계, 빌드, 문서화, 소비하는 일을 도와주는 대형 도구 생태계의 지원을 받는 오픈 소스 소프트웨어 프레임워크이다.
-
-API를 대규모로 설계하고 문서화하는 데 용이하게 사용합니다.
-
-Swagger URL은 `http://localhost:3000/api-docs` 으로 작성했습니다.
-
-수정을 원하시면 `swagger.yaml` 파일을 수정해주시면 됩니다.
-
-### 🌐 REST Client :: HTTP Client 도구
-
-REST 클라이언트를 사용하면 HTTP 요청을 보내고 Visual Studio Code에서 직접 응답을 볼 수 있습니다.
-
-VSCode Extension에서 [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) 설치합니다.
-
-수정을 원하시면 src/http 폴더 안에 `*.http` 파일을 수정해주시면 됩니다.
-
-### 🔮 PM2 :: 웹 애플리케이션을 운영 및 프로세스 관리자
-
-[PM2](https://pm2.keymetrics.io/)란, 서버에서 웹 애플리케이션을 운영할 때 보통 데몬으로 서버를 띄워야 하고 Node.js의 경우 서버가 크래시나면 재시작을 하기 위해서 워치독(watchdog) 류의 프로세스 관리자이다.
-
-- 프로덕션 모드 :: `npm run deploy:prod` 또는 `pm2 start ecosystem.config.js --only prod`
-- 개발 모드 :: `npm run deploy:dev` 또는 `pm2 start ecosystem.config.js --only dev`
-
-수정을 원하시면 `ecosystem.config.js` 파일을 수정해주시면 됩니다.
-
-### 🏎 SWC :: 강하고 빠른 자바스크립트 / 타입스크립트 컴파일러
-
-[SWC](https://swc.rs/)는 차세대 고속 개발자 도구를 위한 확장 가능한 Rust 기반 플랫폼입니다.
-
-`SWC는 단일 스레드에서 Babel보다 20배, 4개 코어에서 70배 빠릅니다.`
-
-- tsc 빌드 :: `npm run build`
-- swc 빌드 :: `npm run build:swc`
+```bash
+# 전역 설치
+npm install -g typescript-express-starter
-수정을 원하시면 `.swcrc` 파일을 수정해주시면 됩니다.
+# 새 프로젝트 생성
+typescript-express-starter
+cd my-app
-### 💄 Makefile :: Linux에서 반복 적으로 발생하는 컴파일을 쉽게하기위해서 사용하는 make 프로그램의 설정 파일
+# 개발 모드 실행
+npm run dev
+```
+- 앱 접속: http://localhost:3000/
-- 도움말 :: `make help`
+- 자동 생성된 API 문서: http://localhost:3000/api-docs
-수정을 원하시면 `Makefile` 파일을 수정해주시면 됩니다.
+### 샘플 영상
-## 🗂 코드 구조 (default)
+## 📂 프로젝트 구조
```bash
-│
-├──📂 .vscode
-│ ├── launch.json
-│ └── settings.json
-│
-├──📂 src
-│ ├──📂 config
-│ │ └── index.ts
-│ │
-│ ├──📂 controllers
-│ │ ├── auth.controller.ts
-│ │ └── users.controller.ts
-│ │
-│ ├──📂 dtos
-│ │ └── users.dto.ts
-│ │
-│ ├──📂 exceptions
-│ │ └── httpException.ts
-│ │
-│ ├──📂 http
-│ │ ├── auth.http
-│ │ └── users.http
-│ │
-│ ├──📂 interfaces
-│ │ ├── auth.interface.ts
-│ │ ├── routes.interface.ts
-│ │ └── users.interface.ts
-│ │
-│ ├──📂 middlewares
-│ │ ├── auth.middleware.ts
-│ │ ├── error.middleware.ts
-│ │ └── validation.middleware.ts
-│ │
-│ ├──📂 models
-│ │ └── users.model.ts
-│ │
-│ ├──📂 routes
-│ │ ├── auth.route.ts
-│ │ └── users.route.ts
-│ │
-│ ├──📂 services
-│ │ ├── auth.service.ts
-│ │ └── users.service.ts
-│ │
-│ ├──📂 test
-│ │ ├── auth.test.ts
-│ │ └── users.test.ts
-│ │
-│ ├──📂 utils
-│ │ ├── logger.ts
-│ │ └── vaildateEnv.ts
-│ │
-│ ├── app.ts
-│ └── server.ts
-│
-├── .dockerignore
-├── .editorconfig
-├── .env.development.local
-├── .env.production.local
-├── .env.test.local
-├── .eslintignore
-├── .eslintrc
-├── .gitignore
-├── .huskyrc
-├── .lintstagedrc.json
-├── .prettierrc
-├── .swcrc
-├── docker-compose.yml
-├── Dockerfile.dev
-├── Dockerfile.prod
-├── ecosystem.config.js
-├── jest.config.js
-├── Makefile
-├── nginx.conf
-├── nodemon.json
-├── package-lock.json
-├── package.json
-├── swagger.yaml
-└── tsconfig.json
+src/
+ ├── config/ # 환경 변수, 설정 파일
+ ├── controllers/ # 요청 처리 및 응답 반환
+ ├── dtos/ # 요청/응답 데이터 구조 정의
+ ├── exceptions/ # 커스텀 예외 클래스
+ ├── interfaces/ # 타입/인터페이스 정의
+ ├── middlewares/ # 미들웨어 (로그, 인증, 에러 처리 등)
+ ├── repositories/ # 데이터베이스 접근 로직
+ ├── routes/ # 라우팅 정의
+ ├── services/ # 비즈니스 로직
+ ├── utils/ # 유틸리티 함수
+ ├── app.ts # Express 앱 초기화
+ └── server.ts # 서버 실행 엔트리 포인트
+
+.env # 기본 환경 변수
+.env.development.local # 개발 환경 변수
+.env.production.local # 운영 환경 변수
+.env.test.local # 테스트 환경 변수
+nodemon.json # Nodemon 환경 변수
+swagger.yaml # Swagger API 문서 정의
+tsconfig.jsnon # TypeScript 환경 변수
```
+## 🛠 개발 도구(Devtools) 유형
+
+| 구분 | 도구 / 설정 파일 | 설명 |
+| --------------- | ------------------- | -------------------------------- |
+| **코드 포맷터 / 린터** | `biome`, `prettier`, `eslint` | 코드 포맷팅 및 린팅 규칙 설정 |
+| **빌드 / 번들러** | `swc`, `tsup` | 빌드 및 번들링 설정 |
+| **테스트** | `jest`, `vitest` | 단위/통합 테스트 프레임워크 |
+| **프로세스 매니저** | `pm2` | Node.js 프로세스 관리 및 모니터링 |
+| **CI/CD** | `github` | GitHub Actions 워크플로우 설정 |
+| **Git 훅** | `husky` | 커밋/푸시 전 린트 및 테스트 실행 |
+| **컨테이너화** | `docker` | Docker 및 docker-compose 배포 환경 설정 |
+
+> 이 표를 통해 각 도구의 용도와 역할을 한눈에 파악할 수 있습니다.
+
+## 🧩 템플릿 선택
+
+CLI를 통해 원하는 스택을 선택하여 프로젝트를 생성할 수 있습니다.
+
+| 템플릿 | 스택 / 통합 기능 |
+| ------------- | ---------------------------- |
+| Default | Express + TypeScript |
+| Sequelize | Sequelize ORM |
+| Mongoose | MongoDB ODM (Mongoose) |
+| TypeORM | TypeORM |
+| Prisma | Prisma ORM |
+| Knex | SQL Query Builder |
+| GraphQL | GraphQL 지원 |
+| Typegoose | TS 친화적인 Mongoose |
+| Mikro ORM | 멀티 DB 지원 Data Mapper ORM |
+| Node Postgres | PostgreSQL 드라이버 (pg) |
+| Drizzle | Drizzle |
+
+## 🤔 포지셔닝: 각 프레임워크 사용에 적합한 상황
+
+| 기준 | TypeScript Express Starter | NestJS |
+| -------- | ----------------------------------- | ------------------------- |
+| 학습 곡선 | ✅ 낮음 — Express에 익숙하다면 바로 사용 가능 | 높음 — OOP/DI/데코레이터 학습 필요 |
+| 유연성 | ✅ 매우 높음 — 스택의 모든 부분을 자유롭게 커스터마이징 가능 | 컨벤션 기반, 구조가 정해져 있음 |
+| 모듈성 | 미들웨어 & 모듈 패턴 | 🌟 강력한 내장 모듈 시스템 |
+| 타입 안정성 | 완전한 TypeScript 지원 | 완전한 TypeScript 지원 |
+| 테스트 | ✅ Jest & Vitest 지원 — 원하는 방식 선택 가능 | Jest E2E 테스트 환경 내장 |
+| 확장성 | ✅ 빠른 프로토타이핑 → 중규모 애플리케이션에 적합 | 🌟 대규모 엔터프라이즈 애플리케이션에 최적화 |
+| DI 프레임워크 | 경량 tsyringe — 최소한의 오버헤드 | 🌟 기능이 풍부한 내장 DI 컨테이너 |
+| 최적 사용 사례 | ✅ 마이크로서비스, MVP, 빠른 개발 속도 | 🌟 복잡하고 대규모의 엔터프라이즈 환경 |
+
+## 📑 권장 커밋 메시지
+
+| 상황 | 커밋 메시지 |
+| --------- | ------------ |
+| 기능 추가 | ✨ 기능 추가 |
+| 버그 수정 | 🐞 버그 수정 |
+| 코드 리팩토링 | 🛠 코드 리팩토링 |
+| 패키지 설치 | 📦 패키지 설치 |
+| 문서 수정 | 📚 문서 수정 |
+| 버전 업데이트 | 🌼 버전 업데이트 |
+| 신규 템플릿 추가 | 🎉 신규 템플릿 추가 |
+
+## 📄 라이선스
+MIT(LICENSE) © AGUMON (ljlm0402)
+
## ⭐️ 응원해주신 분들
[](https://github.com/ljlm0402/typescript-express-starter/stargazers)
@@ -329,29 +184,3 @@ VSCode Extension에서 [REST Client](https://marketplace.visualstudio.com/items?
## 🤝 도움주신 분들
[](https://github.com/ljlm0402/typescript-express-starter/graphs/contributors)
-
-## 💳 라이선스
-
-[MIT](LICENSE)
-
-## 📑 커밋 메시지 정의
-
-| 언제 | 메시지 |
-| :----------------- | :-------------------- |
-| 기능 추가 | ✨ 기능 추가 |
-| 버그 수정 | 🐞 버그 수정 |
-| 코드 개선 | 🛠 코드 개선 |
-| 패키지 설치 | 📦 패키지 설치 |
-| 문서 수정 | 📚 문서 수정 |
-| 버전 업데이트 | 🌼 버전 업데이트 |
-| 새로운 템플릿 추가 | 🎉 새로운 템플릿 추가 |
-
-## 📬 이슈를 남겨주세요
-
-건의 사항이나 질문 등을 이슈로 남겨주세요.
-
-최선을 다해 답변하고 반영하겠습니다.
-
-관심을 가져주셔서 감사합니다.
-
-# ദ്ദി*ˊᗜˋ*)
diff --git a/README.md b/README.md
index 40cfd4b..779ba15 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
TypeScript Express Starter
@@ -52,278 +52,148 @@
-## 😎 Introducing The Project
+---
-Express consists of JavaScript, which makes it vulnerable to type definitions.
+## 📝 Introduction
-That's why we avoid supersets with starter packages that introduce TypeScript.
+**TypeScript Express Starter** provides a robust starting point for building secure, scalable, and maintainable RESTful APIs.
-The package is configured to use TypeScript instead of JavaScript.
+It blends the flexibility and simplicity of Express with TypeScript’s type safety, supporting rapid development without compromising code quality or maintainability.
-> The project referred to [express-generator-typescript](https://github.com/seanpmaxwell/express-generator-typescript)
+- Clean architecture and modular structure
-### 🤔 What is Express ?
+- Built-in security, logging, validation, and developer tooling
-Express is a fast, open and concise web framework and is a Node.js based project.
+- Instantly ready for both prototyping and production
-## 🚀 Quick Start
+## 💎 Features
-### Install with the npm Global Package
+- ⚡ **TypeScript + Express** — Modern JS with full type safety
-```bash
-$ npm install -g typescript-express-starter
-```
-
-### Run npx to Install The Package
-
-npx is a tool in the JavaScript package management module, npm.
-
-This is a tool that allows you to run the npm package on a single run without installing the package.
-
-If you do not enter a project name, it defaults to _typescript-express-starter_.
-
-```bash
-$ npx typescript-express-starter "project name"
-```
-
-### Select a Templates
-
-
-
-Start your typescript-express-starter app in development mode at `http://localhost:3000/`
-
-#### Template Type
-
-| Name | Description |
-| :---------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Default | Express Default |
-| [Routing Controllers](https://github.com/typestack/routing-controllers) | Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage |
-| [Sequelize](https://github.com/sequelize/sequelize) | Easy to use multi SQL dialect ORM for Node.js |
-| [Mongoose](https://github.com/Automattic/mongoose) | MongoDB Object Modeling(ODM) designed to work in an asynchronous environment |
-| [TypeORM](https://github.com/typeorm/typeorm) | An ORM that can run in Node.js and Others |
-| [Prisma](https://github.com/prisma/prisma) | Modern Database Access for TypeScript & Node.js |
-| [Knex](https://github.com/knex/knex) | SQL query builder for Postgres, MySQL, MariaDB, SQLite3 and Oracle |
-| [GraphQL](https://github.com/graphql/graphql-js) | query language for APIs and a runtime for fulfilling those queries with your existing data |
-| [Typegoose](https://github.com/typegoose/typegoose) | Define Mongoose models using TypeScript classes |
-| [Mikro ORM](https://github.com/mikro-orm/mikro-orm) | TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite databases |
-| [Node Postgres](https://node-postgres.com/) | node-postgres is a collection of node.js modules for interfacing with your PostgreSQL database |
-
-#### Template to be developed
-
-| Name | Description |
-| :------------------------------------------------------------------------------ | :------------------------------------------------------------------------- |
-| [Sequelize Typescript](https://github.com/RobinBuschmann/sequelize-typescript) | Decorators and some other features for sequelize |
-| [TS SQL](https://github.com/codemix/ts-sql) | A SQL database implemented purely in TypeScript type annotations |
-| [inversify-express-utils](https://github.com/inversify/inversify-express-utils) | Some utilities for the development of Express application with InversifyJS |
-| [postgress typescript]() | |
-| [graphql prisma]() | |
-
-## 🛎 Available Commands for the Server
-
-- Run the Server in production mode : `npm run start` or `Start typescript-express-starter` in VS Code
-- Run the Server in development mode : `npm run dev` or `Dev typescript-express-starter` in VS Code
-- Run all unit-tests : `npm test` or `Test typescript-express-starter` in VS Code
-- Check for linting errors : `npm run lint` or `Lint typescript-express-starter` in VS Code
-- Fix for linting : `npm run lint:fix` or `Lint:Fix typescript-express-starter` in VS Code
-
-## 💎 The Package Features
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-### 🐳 Docker :: Container Platform
-
-[Docker](https://docs.docker.com/) is a platform for developers and sysadmins to build, run, and share applications with containers.
-
-[Docker](https://docs.docker.com/get-docker/) Install.
-
-- starts the containers in the background and leaves them running : `docker-compose up -d`
-- Stops containers and removes containers, networks, volumes, and images : `docker-compose down`
-
-Modify `docker-compose.yml` and `Dockerfile` file to your source code.
-
-### ♻️ NGINX :: Web Server
-
-[NGINX](https://www.nginx.com/) is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache.
-
-Proxying is typically used to distribute the load among several servers, seamlessly show content from different websites, or pass requests for processing to application servers over protocols other than HTTP.
-
-When NGINX proxies a request, it sends the request to a specified proxied server, fetches the response, and sends it back to the client.
+- 📜 **API Docs** — Swagger/OpenAPI ready out-of-the-box
-Modify `nginx.conf` file to your source code.
+- 🛡 **Security** — Helmet, CORS, HPP, rate limiting
-### ✨ ESLint, Prettier :: Code Formatter
+- 🧩 **Validation** — Zod schema-based runtime validation
-[Prettier](https://prettier.io/) is an opinionated code formatter.
+- 🔗 **Dependency Injection** — Lightweight DI with tsyringe
-[ESLint](https://eslint.org/), Find and fix problems in your JavaScript code
+- 🗄 **Database Integrations** — Sequelize, Prisma, Mongoose, TypeORM, Knex, Drizzle, etc.
-It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary.
+- 🛠 **Developer Tools** — ESLint, Prettier, Jest, Docker, PM2, NGINX, Makefile
-1. Install [VSCode](https://code.visualstudio.com/) Extension [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode), [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
+- 🧱 **Modular Architecture** — Easily extendable and maintainable
-2. `CMD` + `Shift` + `P` (Mac Os) or `Ctrl` + `Shift` + `P` (Windows)
+- 🚀 **Production Ready** — Docker, PM2, NGINX support
-3. Format Selection With
+## ⚡️ Quick Start
-4. Configure Default Formatter...
-
-5. Prettier - Code formatter
-
-
-
-> Palantir, the backers behind TSLint announced in 2019 that they would be deprecating TSLint in favor of supporting typescript-eslint in order to benefit the community.
-> So, migration from TSLint to ESLint.
-
-### 📗 Swagger :: API Document
-
-[Swagger](https://swagger.io/) is Simplify API development for users, teams, and enterprises with the Swagger open source and professional toolset.
-
-Easily used by Swagger to design and document APIs at scale.
-
-Start your app in development mode at `http://localhost:3000/api-docs`
-
-Modify `swagger.yaml` file to your source code.
-
-### 🌐 REST Client :: HTTP Client Tools
-
-REST Client allows you to send HTTP request and view the response in Visual Studio Code directly.
-
-VSCode Extension [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) Install.
-
-Modify `*.http` file in src/http folder to your source code.
-
-### 🔮 PM2 :: Advanced, Production process manager for Node.js
-
-[PM2](https://pm2.keymetrics.io/) is a daemon process manager that will help you manage and keep your application online 24/7.
-
-- production mode :: `npm run deploy:prod` or `pm2 start ecosystem.config.js --only prod`
-- development mode :: `npm run deploy:dev` or `pm2 start ecosystem.config.js --only dev`
-
-Modify `ecosystem.config.js` file to your source code.
+```bash
+# Install globally
+npm install -g typescript-express-starter
-### 🏎 SWC :: a super-fast JavaScript / TypeScript compiler
+# Scaffold a new project
+typescript-express-starter
+cd my-app
-[SWC](https://swc.rs/) is an extensible Rust-based platform for the next generation of fast developer tools.
+# Run in development mode
+npm run dev
+```
+- Access the app: http://localhost:3000/
-`SWC is 20x faster than Babel on a single thread and 70x faster on four cores.`
+- Auto-generated API docs: http://localhost:3000/api-docs
-- tsc build :: `npm run build`
-- swc build :: `npm run build:swc`
+### Example
-Modify `.swcrc` file to your source code.
+## 📂 Project Structure
-### 💄 Makefile :: This is a setting file of the make program used to make the compilation that occurs repeatedly on Linux
+```bash
+src/
+ ├── config/ # Configuration files, environment settings
+ ├── controllers/ # Request handling & response logic
+ ├── dtos/ # Data Transfer Objects for request/response
+ ├── exceptions/ # Custom exception classes
+ ├── interfaces/ # TypeScript interfaces and type definitions
+ ├── middlewares/ # Middlewares (logging, auth, error handling, etc.)
+ ├── repositories/ # Database access logic
+ ├── routes/ # API route definitions
+ ├── services/ # Business logic
+ ├── utils/ # Utility/helper functions
+ ├── app.ts # Express app initialization
+ └── server.ts # Server entry point
+
+.env # Default environment variables
+.env.development.local # Development-specific variables
+.env.production.local # Production-specific variables
+.env.test.local # Test-specific variables
+nodemon.json # Nodemon variables
+swagger.yaml # Swagger API documentation
+tsconfig.jsnon # TypeScript variables
+```
-[Makefile](https://makefiletutorial.com/)s are used to help decide which parts of a large program need to be recompiled.
+## 🛠 Devtools Types
+
+| Category | Tools / Configs | Description |
+| --------------------------- | --------------------------- | -------------------------------------------- |
+| **Code Formatter / Linter** | `biome`, `prettier`, `eslint` | Code formatting & linting rules |
+| **Build / Bundler** | `swc`, `tsup` | Build & bundling configuration |
+| **Testing** | `jest`, `vitest` | Unit & integration testing frameworks |
+| **Process Manager** | `pm2` | Manage and monitor Node.js processes |
+| **CI/CD** | `github` | GitHub Actions workflow settings |
+| **Git Hooks** | `husky` | Pre-commit / pre-push hooks for lint/test |
+| **Containerization** | `docker` | Docker & docker-compose setup for deployment |
+
+> This categorization helps developers quickly understand what each tool is used for without checking every folder.
+
+## 🧩 Template Choices
+
+Choose your preferred stack during setup!
+Support for major databases and patterns via CLI:
+
+| Template | Stack / Integration |
+| ------------- | ------------------------------ |
+| Default | Express + TypeScript |
+| Sequelize | Sequelize ORM |
+| Mongoose | MongoDB ODM (Mongoose) |
+| TypeORM | TypeORM |
+| Prisma | Prisma ORM |
+| Knex | SQL Query Builder |
+| GraphQL | GraphQL support |
+| Typegoose | TS-friendly Mongoose |
+| Mikro ORM | Data Mapper ORM (multi-DB) |
+| Node Postgres | PostgreSQL driver (pg) |
+| Drizzle | Drizzle |
+
+> More templates are regularly added and updated.
+
+## 🤔 Positioning: When to Use Each
+
+| Criteria | TypeScript Express Starter | NestJS |
+| ---------------- | --------------------------------------------------- | ------------------------------------------ |
+| Learning Curve | ✅ Low — easy for anyone familiar with Express | Higher — requires OOP/DI/Decorators |
+| Flexibility | ✅ Maximum — customize any part of the stack | Convention-based, opinionated structure |
+| Modularity | Middleware & modular pattern | 🌟 Strong built-in module system |
+| Type Safety | Full TypeScript support | Full TypeScript support |
+| Testing | ✅ Supports Jest & Vitest — flexible choice | Built-in Jest E2E setup |
+| Scale | ✅ Fast prototyping → mid-size apps | 🌟 Large-scale enterprise apps |
+| DI Framework | Lightweight tsyringe — minimal overhead | 🌟 Full-featured DI container |
+| Best Fit | ✅ Microservices, quick MVPs, developer agility | 🌟 Complex, enterprise-grade applications |
-- help :: `make help`
-Modify `Makefile` file to your source code.
+## 📑 Recommended Commit Message
-## 🗂 Code Structure (default)
+| When | Commit Message |
+| --------------- | ------------------ |
+| Add Feature | ✨ Add Feature |
+| Fix Bug | 🐞 Fix Bug |
+| Refactor Code | 🛠 Refactor Code |
+| Install Package | 📦 Install Package |
+| Fix Readme | 📚 Fix Readme |
+| Update Version | 🌼 Update Version |
+| New Template | 🎉 New Template |
-```sh
-│
-├──📂 .vscode
-│ ├── launch.json
-│ └── settings.json
-│
-├──📂 src
-│ ├──📂 config
-│ │ └── index.ts
-│ │
-│ ├──📂 controllers
-│ │ ├── auth.controller.ts
-│ │ └── users.controller.ts
-│ │
-│ ├──📂 dtos
-│ │ └── users.dto.ts
-│ │
-│ ├──📂 exceptions
-│ │ └── httpException.ts
-│ │
-│ ├──📂 http
-│ │ ├── auth.http
-│ │ └── users.http
-│ │
-│ ├──📂 interfaces
-│ │ ├── auth.interface.ts
-│ │ ├── routes.interface.ts
-│ │ └── users.interface.ts
-│ │
-│ ├──📂 middlewares
-│ │ ├── auth.middleware.ts
-│ │ ├── error.middleware.ts
-│ │ └── validation.middleware.ts
-│ │
-│ ├──📂 models
-│ │ └── users.model.ts
-│ │
-│ ├──📂 routes
-│ │ ├── auth.route.ts
-│ │ └── users.route.ts
-│ │
-│ ├──📂 services
-│ │ ├── auth.service.ts
-│ │ └── users.service.ts
-│ │
-│ ├──📂 test
-│ │ ├── auth.test.ts
-│ │ └── users.test.ts
-│ │
-│ ├──📂 utils
-│ │ ├── logger.ts
-│ │ └── vaildateEnv.ts
-│ │
-│ ├── app.ts
-│ └── server.ts
-│
-├── .dockerignore
-├── .editorconfig
-├── .env.development.local
-├── .env.production.local
-├── .env.test.local
-├── .eslintignore
-├── .eslintrc
-├── .gitignore
-├── .huskyrc
-├── .lintstagedrc.json
-├── .prettierrc
-├── .swcrc
-├── docker-compose.yml
-├── Dockerfile.dev
-├── Dockerfile.prod
-├── ecosystem.config.js
-├── jest.config.js
-├── Makefile
-├── nginx.conf
-├── nodemon.json
-├── package-lock.json
-├── package.json
-├── swagger.yaml
-└── tsconfig.json
-```
+## 📄 License
+MIT(LICENSE) © AGUMON (ljlm0402)
## ⭐️ Stargazers
@@ -336,29 +206,3 @@ Modify `Makefile` file to your source code.
## 🤝 Contributors
[](https://github.com/ljlm0402/typescript-express-starter/graphs/contributors)
-
-## 💳 License
-
-[MIT](LICENSE)
-
-## 📑 Recommended Commit Message
-
-| When | Commit Message |
-| :--------------- | :----------------- |
-| Add Feature | ✨ Add Feature |
-| Fix Bug | 🐞 Fix Bug |
-| Refactoring Code | 🛠 Refactoring Code |
-| Install Package | 📦 Install Package |
-| Fix Readme | 📚 Fix Readme |
-| Update Version | 🌼 Update Version |
-| New Template | 🎉 New Template |
-
-## 📬 Please request an issue
-
-Please leave a question or question as an issue.
-
-I will do my best to answer and reflect.
-
-Thank you for your interest.
-
-# ദ്ദി*ˊᗜˋ*)
diff --git a/bin/cli.js b/bin/cli.js
deleted file mode 100644
index 453ea9e..0000000
--- a/bin/cli.js
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env node
-
-/*****************************************************************
- * TypeScript Express Starter
- * 2019.12.18 ~ 🎮
- * Made By AGUMON 🦖
- * https://github.com/ljlm0402/typescript-express-starter
- *****************************************************************/
-
-const path = require("path");
-const starter = require("../lib/starter");
-const destination = getDest(process.argv[2]);
-
-function getDest(destFolder = "typescript-express-starter") {
- return path.join(process.cwd(), destFolder);
-}
-
-starter(destination);
diff --git a/bin/common.js b/bin/common.js
new file mode 100644
index 0000000..70c6a65
--- /dev/null
+++ b/bin/common.js
@@ -0,0 +1,273 @@
+import { fileURLToPath } from 'url';
+import path from 'path';
+
+export const PACKAGE_MANAGER = [
+ { label: 'npm', value: 'npm' },
+ { label: 'yarn', value: 'yarn' },
+ { label: 'pnpm', value: 'pnpm' },
+];
+
+export const TEMPLATES_VALUES = [
+ {
+ name: 'Default',
+ value: 'default',
+ desc: 'Basic Express + TypeScript project',
+ active: true,
+ tags: ['express', 'typescript', 'starter'],
+ version: 'v1.0.0',
+ maintainer: 'core',
+ lastUpdated: '2025-08-27',
+ },
+ {
+ name: 'GraphQL',
+ value: 'graphql',
+ desc: 'GraphQL API template based on Apollo Server',
+ active: false,
+ tags: ['graphql', 'apollo', 'api'],
+ version: 'v1.2.0',
+ maintainer: 'core',
+ lastUpdated: '2025-08-10',
+ },
+ {
+ name: 'Knex',
+ value: 'knex',
+ desc: 'SQL query builder template using Knex.js',
+ active: false,
+ tags: ['sql', 'knex', 'database'],
+ version: 'v0.95.x',
+ maintainer: 'db-team',
+ lastUpdated: '2025-07-30',
+ },
+ {
+ name: 'Mikro-ORM',
+ value: 'mikro-orm',
+ desc: 'TypeScript ORM template based on Mikro-ORM',
+ active: false,
+ tags: ['orm', 'typescript', 'database'],
+ version: 'v6.x',
+ maintainer: 'orm-team',
+ lastUpdated: '2025-07-25',
+ },
+ {
+ name: 'Mongoose',
+ value: 'mongoose',
+ desc: 'MongoDB ODM template using Mongoose',
+ active: false,
+ tags: ['mongodb', 'odm', 'database'],
+ version: 'v7.x',
+ maintainer: 'nosql-team',
+ lastUpdated: '2025-08-01',
+ },
+ {
+ name: 'Node-Postgres',
+ value: 'node-postgres',
+ desc: 'PostgreSQL template using pg library',
+ active: false,
+ tags: ['postgres', 'sql', 'database'],
+ version: 'v8.x',
+ maintainer: 'db-team',
+ lastUpdated: '2025-07-20',
+ },
+ {
+ name: 'Prisma',
+ value: 'prisma',
+ desc: 'SQL/NoSQL ORM template based on Prisma',
+ active: false,
+ tags: ['orm', 'prisma', 'database'],
+ version: 'v5.x',
+ maintainer: 'orm-team',
+ lastUpdated: '2025-08-05',
+ },
+ {
+ name: 'Routing-Controllers',
+ value: 'routing-controllers',
+ desc: 'Express controller structure with TypeScript decorators',
+ active: false,
+ tags: ['decorator', 'controller', 'express'],
+ version: 'v0.10.x',
+ maintainer: 'core',
+ lastUpdated: '2025-07-15',
+ },
+ {
+ name: 'Sequelize',
+ value: 'sequelize',
+ desc: 'SQL ORM template based on Sequelize',
+ active: false,
+ tags: ['orm', 'sequelize', 'database'],
+ version: 'v6.x',
+ maintainer: 'orm-team',
+ lastUpdated: '2025-07-28',
+ },
+ {
+ name: 'Typegoose',
+ value: 'typegoose',
+ desc: 'TypeScript-friendly MongoDB ODM using Typegoose',
+ active: false,
+ tags: ['mongodb', 'typegoose', 'odm'],
+ version: 'v11.x',
+ maintainer: 'nosql-team',
+ lastUpdated: '2025-08-08',
+ },
+ {
+ name: 'TypeORM',
+ value: 'typeorm',
+ desc: 'SQL/NoSQL ORM template based on TypeORM',
+ active: false,
+ tags: ['orm', 'typeorm', 'database'],
+ version: 'v0.3.x',
+ maintainer: 'orm-team',
+ lastUpdated: '2025-07-18',
+ },
+];
+
+export const DEVTOOLS_VALUES = [
+ // == [Formatter] == //
+ {
+ name: 'Biome',
+ value: 'biome',
+ category: 'Formatter',
+ files: ['.biome.json', '.biomeignore'],
+ pkgs: [],
+ devPkgs: ['@biomejs/biome@2.1.4'],
+ scripts: {
+ lint: 'biome lint .',
+ check: 'biome check .',
+ format: 'biome format . --write',
+ },
+ desc: 'All-in-one formatter and linter',
+ },
+ {
+ name: 'Prettier & ESLint',
+ value: 'prettier',
+ category: 'Formatter',
+ files: ['.prettierrc', 'eslint.config.js'],
+ pkgs: [],
+ devPkgs: ['eslint@^9.33.0', 'eslint-config-prettier@^10.1.1', 'globals@^15.10.0', 'prettier@3.6.2', 'typescript-eslint@^8.39.0'],
+ scripts: {
+ lint: 'eslint --ext .ts src/',
+ 'lint:fix': 'npm run lint -- --fix',
+ format: 'prettier --check .',
+ 'format:fix': 'prettier --write .',
+ },
+ desc: 'Separate formatter and linter setup',
+ },
+
+ // == [Compiler] == //
+ {
+ name: 'tsup',
+ value: 'tsup',
+ category: 'Compiler',
+ files: ['tsup.config.ts'],
+ pkgs: [],
+ devPkgs: ['tsup@8.5.0'],
+ scripts: {
+ 'start:tsup': 'node -r tsconfig-paths/register dist/server.js',
+ 'build:tsup': 'tsup --config tsup.config.ts',
+ },
+ desc: 'Fast bundler for TypeScript',
+ },
+ {
+ name: 'SWC',
+ value: 'swc',
+ category: 'Compiler',
+ files: ['.swcrc'],
+ pkgs: [],
+ devPkgs: ['@swc/cli@0.7.8', '@swc/core@1.13.3'],
+ scripts: {
+ 'start:swc': 'node dist/server.js',
+ 'build:swc': 'swc src -d dist --strip-leading-paths --copy-files --delete-dir-on-start',
+ },
+ desc: 'Rust-based TypeScript compiler',
+ },
+
+ // == [Testing] == //
+ {
+ name: 'Jest',
+ value: 'jest',
+ category: 'Testing',
+ files: ['jest.config.cjs', 'jest.config.ts'],
+ pkgs: [],
+ devPkgs: ['@types/supertest@6.0.3', 'supertest@7.1.4', '@types/jest@30.0.0', 'jest@30.0.5', 'ts-jest@29.4.1'],
+ scripts: {
+ 'test': 'jest --config jest.config.cjs --watch',
+ 'test:e2e': 'jest --config jest.config.cjs --testPathPatterns=e2e --watch',
+ 'test:unit': 'jest --config jest.config.cjs --testPathPatterns=unit --watch'
+ },
+ desc: 'Industry-standard test runner for Node.js',
+ },
+ {
+ name: 'Vitest',
+ value: 'vitest',
+ category: 'Testing',
+ files: ['vitest.config.ts'],
+ pkgs: [],
+ devPkgs: ['@types/supertest@6.0.3', 'supertest@7.1.4', 'vite-tsconfig-paths@5.1.4', 'vitest@3.2.4'],
+ scripts: {
+ "test": "vitest",
+ "test:unit": "vitest src/test/unit",
+ "test:e2e": "vitest src/test/e2e",
+ "test:ci": "vitest run --coverage",
+ "test:ci:unit": "vitest run src/test/unit --coverage",
+ "test:ci:e2e": "vitest run src/test/e2e --coverage"
+ },
+ desc: 'Fast Vite-powered unit/e2e test framework',
+ },
+
+ // == [Infrastructure] == //
+ {
+ name: 'Docker',
+ value: 'docker',
+ category: 'Infrastructure',
+ files: ['.dockerignore', 'Dockerfile.dev', 'Dockerfile.prod', 'nginx.conf', 'Makefile'],
+ pkgs: [],
+ devPkgs: [],
+ scripts: {},
+ desc: 'Containerized dev & prod environment',
+ },
+
+ // == [Git Tools] == //
+ {
+ name: 'Husky',
+ value: 'husky',
+ category: 'Git Tools',
+ files: ['.husky'],
+ pkgs: [],
+ devPkgs: ['husky'],
+ scripts: { prepare: 'husky install' },
+ requires: [],
+ desc: 'Git hooks for automation',
+ },
+
+ // == [Deployment] == //
+ {
+ name: 'PM2',
+ value: 'pm2',
+ category: 'Deployment',
+ files: ['ecosystem.config.js'],
+ pkgs: ['pm2'],
+ devPkgs: [],
+ scripts: {
+ 'deploy:prod': 'pm2 start ecosystem.config.js --only prod',
+ 'deploy:dev': 'pm2 start ecosystem.config.js --only dev',
+ },
+ desc: 'Process manager for Node.js',
+ },
+
+ // == [CI/CD] == //
+ {
+ name: 'GitHub Actions',
+ value: 'github',
+ category: 'CI/CD',
+ files: ['.github/workflows/ci.yml'],
+ pkgs: [],
+ devPkgs: [],
+ scripts: {},
+ desc: 'CI/CD workflow automation',
+ },
+];
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export const TEMPLATES = path.join(__dirname, '../templates');
+
+export const DEVTOOLS = path.join(__dirname, '../devtools');
diff --git a/bin/db-map.js b/bin/db-map.js
new file mode 100644
index 0000000..d5bb051
--- /dev/null
+++ b/bin/db-map.js
@@ -0,0 +1,95 @@
+export const TEMPLATE_DB = {
+ default: null,
+ graphql: 'postgres',
+ knex: 'mysql',
+ mikroorm: 'mongo',
+ mongoose: 'mongo',
+ 'node-postgres': 'postgres',
+ prisma: 'mysql',
+ sequelize: 'mysql',
+ typegoose: 'mongo',
+ typeorm: 'postgres',
+};
+
+export const DB_SERVICES = {
+ postgres: `
+ pg:
+ container_name: pg
+ image: postgres:14.5-alpine
+ ports:
+ - "5432:5432"
+ env_file:
+ - .env.development.local
+ restart: always
+ networks:
+ - backend
+ `,
+ mysql: `
+ mysql:
+ container_name: mysql
+ image: mysql:5.7
+ ports:
+ - "3306:3306"
+ env_file:
+ - .env.development.local
+ networks:
+ - backend
+ `,
+ mongo: `
+ mongo:
+ container_name: mongo
+ image: mongo
+ ports:
+ - "27017:27017"
+ env_file:
+ - .env.development.local
+ networks:
+ - backend
+ `,
+};
+
+export const BASE_COMPOSE = (dbSnippet = '') => `
+version: '3.9'
+
+services:
+ proxy:
+ container_name: proxy
+ image: nginx:alpine
+ ports:
+ - '80:80'
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ depends_on:
+ - server
+ restart: unless-stopped
+ networks:
+ - backend
+
+ server:
+ container_name: server
+ build:
+ context: ./
+ dockerfile: Dockerfile.dev
+ ports:
+ - '3000:3000'
+ volumes:
+ - ./:/app:cached
+ - /app/node_modules
+ env_file:
+ - .env.development.local
+ depends_on:
+ - ${dbSnippet ? dbSnippet.match(/^\s*(\w+):/)?.[1] : ''}
+ restart: unless-stopped
+ networks:
+ - backend
+
+${dbSnippet.trim() ? dbSnippet : ''}
+
+networks:
+ backend:
+ driver: bridge
+
+volumes:
+ pgdata:
+ driver: local
+`;
diff --git a/bin/starter.js b/bin/starter.js
new file mode 100644
index 0000000..d7d2ba4
--- /dev/null
+++ b/bin/starter.js
@@ -0,0 +1,365 @@
+#!/usr/bin/env node
+
+/*****************************************************************
+ * TYPESCRIPT-EXPRESS-STARTER - Quick and Easy TypeScript Express Starter
+ * (c) 2020-present AGUMON (https://github.com/ljlm0402/typescript-express-starter)
+ *
+ * MIT License
+ *
+ * Made with ❤️ by AGUMON 🦖
+ *****************************************************************/
+
+import { select, text, isCancel, intro, outro, cancel, note, confirm } from '@clack/prompts';
+import chalk from 'chalk';
+import editJsonFile from 'edit-json-file';
+import { execa } from 'execa';
+import fs from 'fs-extra';
+import ora from 'ora';
+import path from 'path';
+import { PACKAGE_MANAGER, TEMPLATES_VALUES, DEVTOOLS_VALUES, TEMPLATES, DEVTOOLS } from './common.js';
+import { TEMPLATE_DB, DB_SERVICES, BASE_COMPOSE } from './db-map.js';
+
+// ========== [공통 함수들] ==========
+
+// Node 버전 체크 (16+)
+function checkNodeVersion(min = 16) {
+ const major = parseInt(process.versions.node.split('.')[0], 10);
+ if (major < min) {
+ console.error(chalk.red(`Node.js ${min}+ required. You have ${process.versions.node}.`));
+ process.exit(1);
+ }
+}
+
+// 최신 CLI 버전 체크 & 선택적 설치
+async function checkForUpdate(pkgName, localVersion) {
+ try {
+ const { stdout } = await execa('npm', ['view', pkgName, 'version']);
+ const latest = stdout.trim();
+ if (latest !== localVersion) {
+ console.log(chalk.yellow(`🔔 New version available: ${latest} (You are on ${localVersion})`));
+ const shouldUpdate = await confirm({
+ message: `Do you want to update ${pkgName} to version ${latest}?`,
+ initial: true,
+ });
+ if (shouldUpdate) {
+ console.log(chalk.gray(` Updating to latest version...`));
+ try {
+ await execa('npm', ['install', '-g', `${pkgName}@${latest}`], { stdio: 'inherit' });
+ console.log(chalk.green(` ✓ Updated ${pkgName} to ${latest}`));
+ } catch (err) {
+ printError(`Failed to update ${pkgName}`, err.message);
+ }
+ } else {
+ console.log(chalk.gray('Skipped updating.'));
+ }
+ }
+ } catch (err) {
+ printError('Failed to check latest version', err.message);
+ }
+}
+
+// 패키지매니저 글로벌 설치여부
+async function checkPkgManagerInstalled(pm) {
+ try {
+ await execa(pm, ['--version']);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// 최신 버전 조회
+async function getLatestVersion(pkg) {
+ try {
+ const { stdout } = await execa('npm', ['view', pkg, 'version']);
+ return stdout.trim();
+ } catch {
+ return null;
+ }
+}
+
+// 도구 간 의존성 자동 해결
+function resolveDependencies(selected) {
+ const all = new Set(selected);
+ let changed = true;
+ while (changed) {
+ changed = false;
+ for (const tool of DEVTOOLS_VALUES) {
+ if (all.has(tool.value) && tool.requires) {
+ for (const req of tool.requires) {
+ if (!all.has(req)) {
+ all.add(req);
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+ return Array.from(all);
+}
+
+// 파일 복사
+async function copyDevtoolFiles(devtool, destDir) {
+ for (const file of devtool.files) {
+ const src = path.join(DEVTOOLS, devtool.value, file);
+ const dst = path.join(destDir, file);
+ if (await fs.pathExists(src)) {
+ await fs.copy(src, dst, { overwrite: true });
+ console.log(chalk.gray(` ⎯ ${file} copied.`));
+ }
+ }
+}
+
+function isExplicitSpecifier(spec) {
+ return (
+ spec.startsWith('http://') ||
+ spec.startsWith('https://') ||
+ spec.startsWith('git+') ||
+ spec.startsWith('file:') ||
+ spec.startsWith('link:') ||
+ spec.startsWith('workspace:') ||
+ spec.startsWith('npm:')
+ );
+}
+
+// 'pkg' / '@scope/pkg' vs 'pkg@^1.2.3' / '@scope/pkg@1.2.3' 구분
+function splitNameAndVersion(spec) {
+ if (spec.startsWith('@')) {
+ const idx = spec.indexOf('@', 1); // 스코프 다음 '@'가 버전 구분자
+ if (idx === -1) return { name: spec, version: null };
+ return { name: spec.slice(0, idx), version: spec.slice(idx + 1) };
+ } else {
+ const idx = spec.indexOf('@');
+ if (idx === -1) return { name: spec, version: null };
+ return { name: spec.slice(0, idx), version: spec.slice(idx + 1) };
+ }
+}
+
+// 패키지 설치 (버전/범위 지정 시 그대로, 없으면 latest 조회해 고정)
+async function installPackages(pkgs, pkgManager, dev = true, destDir = process.cwd()) {
+ if (!pkgs || pkgs.length === 0) return;
+
+ const resolved = [];
+ for (const spec of pkgs) {
+ // URL/파일/워크스페이스/별칭은 그대로 통과
+ if (isExplicitSpecifier(spec)) {
+ resolved.push(spec);
+ continue;
+ }
+
+ const { name, version } = splitNameAndVersion(spec);
+ // 이미 버전/범위가 명시된 경우 그대로 사용 (예: ^9.33.0, ~10.1.8, 9.33.0)
+ if (version && version.length > 0) {
+ resolved.push(`${name}@${version}`);
+ continue;
+ }
+
+ // 버전 미지정 → npm view로 latest 조회 후 고정
+ const latest = await getLatestVersion(name);
+ resolved.push(latest ? `${name}@${latest}` : name);
+ }
+
+ const installCmd =
+ pkgManager === 'npm'
+ ? ['install', dev ? '--save-dev' : '', ...resolved].filter(Boolean)
+ : pkgManager === 'yarn'
+ ? ['add', dev ? '--dev' : '', ...resolved].filter(Boolean)
+ : ['add', dev ? '-D' : '', ...resolved].filter(Boolean);
+
+ await execa(pkgManager, installCmd, { cwd: destDir, stdio: 'inherit' });
+}
+
+// package.json 수정 (스크립트 추가 등)
+async function updatePackageJson(scripts, destDir) {
+ const pkgPath = path.join(destDir, 'package.json');
+ const file = editJsonFile(pkgPath, { autosave: true });
+ Object.entries(scripts).forEach(([k, v]) => file.set(`scripts.${k}`, v));
+ if (!file.get('scripts.prepare') && fs.existsSync(path.join(destDir, '.huskyrc'))) {
+ file.set('scripts.prepare', 'husky install');
+ }
+ file.save();
+}
+
+function printError(message, suggestion = null) {
+ console.log(chalk.bgRed.white(' ERROR '), chalk.red(message));
+ if (suggestion) {
+ console.log(chalk.gray('Hint:'), chalk.cyan(suggestion));
+ }
+}
+
+// docker-compose 생성
+async function generateCompose(template, destDir) {
+ const dbType = TEMPLATE_DB[template];
+ const dbSnippet = dbType ? DB_SERVICES[dbType] : '';
+ const composeYml = BASE_COMPOSE(dbSnippet);
+ const filePath = path.join(destDir, 'docker-compose.yml');
+ await fs.writeFile(filePath, composeYml, 'utf8');
+ return dbType;
+}
+
+// Git init & 첫 커밋
+async function gitInitAndFirstCommit(destDir) {
+ const doGit = await confirm({ message: 'Initialize git and make first commit?', initial: true });
+ if (!doGit) return;
+ try {
+ await execa('git', ['init'], { cwd: destDir });
+ await execa('git', ['add', '.'], { cwd: destDir });
+ await execa('git', ['commit', '-m', 'init'], { cwd: destDir });
+ console.log(chalk.green(' ✓ git initialized and first commit made!'));
+ } catch (e) {
+ printError('git init/commit failed', 'Check git is installed and accessible.');
+ }
+}
+
+// ========== [메인 CLI 실행 흐름] ==========
+async function main() {
+ // 1. Node 버전 체크
+ checkNodeVersion(16);
+
+ // 2. CLI 최신버전 안내
+ await checkForUpdate('typescript-express-starter', '10.2.2');
+
+ const gradientBanner =
+ '\x1B[38;2;91;192;222m📘\x1B[39m\x1B[38;2;91;192;222m \x1B[39m\x1B[38;2;91;192;222mT\x1B[39m\x1B[38;2;82;175;222my\x1B[39m\x1B[38;2;74;159;222mp\x1B[39m\x1B[38;2;66;143;210me\x1B[39m\x1B[38;2;58;128;198mS\x1B[39m\x1B[38;2;54;124;190mc\x1B[39m\x1B[38;2;52;118;180mr\x1B[39m\x1B[38;2;50;115;172mi\x1B[39m\x1B[38;2;49;120;198mp\x1B[39m\x1B[38;2;47;110;168mt\x1B[39m\x1B[38;2;45;105;160m \x1B[39m\x1B[38;2;43;100;152mE\x1B[39m\x1B[38;2;41;95;144mx\x1B[39m\x1B[38;2;39;90;136mp\x1B[39m\x1B[38;2;37;85;128mr\x1B[39m\x1B[38;2;35;80;120me\x1B[39m\x1B[38;2;33;75;112ms\x1B[39m\x1B[38;2;30;72;106ms\x1B[39m\x1B[38;2;28;70;100m \x1B[39m\x1B[38;2;26;68;96mS\x1B[39m\x1B[38;2;25;68;94mt\x1B[39m\x1B[38;2;25;69;92ma\x1B[39m\x1B[38;2;25;70;91mr\x1B[39m\x1B[38;2;25;70;150mt\x1B[39m\x1B[38;2;25;70;150me\x1B[39m\x1B[38;2;25;70;150mr\x1B[39m';
+ intro(gradientBanner);
+
+ // 3. 패키지 매니저 선택 + 글로벌 설치 확인
+ let pkgManager;
+ while (true) {
+ pkgManager = await select({
+ message: 'Which package manager do you want to use?',
+ options: PACKAGE_MANAGER,
+ initialValue: 'npm',
+ });
+ if (isCancel(pkgManager)) return cancel('❌ Aborted.');
+ if (await checkPkgManagerInstalled(pkgManager)) break;
+ printError(`${pkgManager} is not installed globally! Please install it first.`);
+ }
+ note(`Using: ${pkgManager}`);
+
+ // 4. 템플릿 선택
+ const templateDirs = (await fs.readdir(TEMPLATES)).filter(f => fs.statSync(path.join(TEMPLATES, f)).isDirectory());
+ if (templateDirs.length === 0) return printError('No templates found!');
+
+ const options = TEMPLATES_VALUES
+ .filter(t => t.active && templateDirs.includes(t.value))
+ .map(t => ({
+ label: t.name, // UI에 표시될 이름
+ value: t.value, // 선택 값
+ hint: t.desc, // 오른쪽에 표시될 설명
+ }));
+
+ const template = await select({
+ message: 'Choose a template:',
+ options: options,
+ initialValue: 'default',
+ });
+ if (isCancel(template)) return cancel('❌ Aborted.');
+
+ // 5. 프로젝트명 입력 (중복체크/덮어쓰기)
+ let projectName, destDir;
+ while (true) {
+ projectName = await text({
+ message: 'Enter your project name:',
+ initial: 'my-app',
+ validate: val => (!val ? 'Project name is required' : undefined),
+ });
+ if (isCancel(projectName)) return cancel('❌ Aborted.');
+ destDir = path.resolve(process.cwd(), projectName);
+ if (await fs.pathExists(destDir)) {
+ const overwrite = await confirm({ message: `Directory "${projectName}" already exists. Overwrite?` });
+ if (overwrite) break;
+ } else break;
+ }
+
+ // 6. 개발 도구 옵션 선택 (category 기준으로 그룹화)
+ const groupedDevtools = DEVTOOLS_VALUES.reduce((acc, tool) => {
+ const cat = tool.category || 'Others';
+ if (!acc[cat]) acc[cat] = [];
+ acc[cat].push(tool);
+ return acc;
+ }, {});
+
+ // 6-1. 개발 도구 옵션 선택 (category별 하나씩만 선택하는 방식)
+ let devtoolValues = [];
+ for (const [category, tools] of Object.entries(groupedDevtools)) {
+ const picked = await select({
+ message: `Select a tool for "${category}":`,
+ options: [
+ { label: 'None', value: null },
+ ...tools.map(({ name, value, desc }) => ({
+ label: `${name} (${desc})`,
+ value,
+ })),
+ ],
+ initialValue: null,
+ });
+ if (isCancel(picked)) return cancel('❌ Aborted.');
+ if (picked) devtoolValues.push(picked);
+ }
+ devtoolValues = resolveDependencies(devtoolValues);
+
+ // === [진행] ===
+
+ // [1] 템플릿 복사
+ const spinner = ora('Copying template...').start();
+ try {
+ await fs.copy(path.join(TEMPLATES, template), destDir, { overwrite: true });
+ spinner.succeed('Template copied!');
+ } catch (e) {
+ spinner.fail('Template copy failed!');
+ printError(e.message, 'Check templates folder and permissions.');
+ return process.exit(1);
+ }
+
+ // [1-1] Testing 도구를 선택한 경우에만 /src/test 예제 복사
+ const testDevtool = devtoolValues.map(val => DEVTOOLS_VALUES.find(d => d.value === val)).find(tool => tool && tool.category === 'Testing');
+
+ if (testDevtool) {
+ const devtoolTestDir = path.join(DEVTOOLS, testDevtool.value, 'src', 'test');
+ const projectTestDir = path.join(destDir, 'src', 'test');
+ if (await fs.pathExists(devtoolTestDir)) {
+ await fs.copy(devtoolTestDir, projectTestDir, { overwrite: true });
+ console.log(chalk.gray(` ⎯ test files for ${testDevtool.name} copied.`));
+ }
+ }
+
+ // [2] 개발 도구 파일/패키지/스크립트/코드패치
+ for (const val of devtoolValues) {
+ const tool = DEVTOOLS_VALUES.find(d => d.value === val);
+ if (!tool) continue;
+
+ spinner.start(`Setting up ${tool.name}...`);
+ await copyDevtoolFiles(tool, destDir);
+
+ // [2-1] 개발 도구 - 패키지 설치
+ if (tool.pkgs?.length > 0) await installPackages(tool.pkgs, pkgManager, false, destDir);
+ if (tool.devPkgs?.length > 0) await installPackages(tool.devPkgs, pkgManager, true, destDir);
+
+ // [2-2] 개발 도구 - 스크립트 추가 등
+ if (Object.keys(tool.scripts).length) await updatePackageJson(tool.scripts, destDir);
+
+ // [2-3] 개발 도구 - Docker 선택 한 경우, docker-compose.yml 생성
+ if (tool.value === 'docker') await generateCompose(template, destDir);
+
+ spinner.succeed(`${tool.name} setup done.`);
+ }
+
+ // [3] 템플릿 기본 패키지 설치
+ spinner.start(`Installing base dependencies with ${pkgManager}...`);
+ await execa(pkgManager, ['install'], { cwd: destDir, stdio: 'inherit' });
+ spinner.succeed('📦 Base dependencies installed!');
+
+ // [4] git 첫 커밋 옵션
+ await gitInitAndFirstCommit(destDir);
+
+ outro(chalk.greenBright('\n🎉 Project setup complete!\n'));
+ console.log(chalk.cyan(` $ cd ${projectName}`));
+ console.log(chalk.cyan(` $ ${pkgManager} run dev\n`));
+ console.log(chalk.gray('✨ Happy hacking!\n'));
+}
+
+main().catch(err => {
+ printError('Unexpected error', err.message);
+ process.exit(1);
+});
diff --git a/devtools/biome/.biome.json b/devtools/biome/.biome.json
new file mode 100644
index 0000000..7bd5cba
--- /dev/null
+++ b/devtools/biome/.biome.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json",
+ "files": {
+ "ignore": [
+ "node_modules/**",
+ "dist/**",
+ "build/**"
+ ]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 150,
+ "quoteStyle": "single",
+ "trailingComma": "all",
+ "semiColon": true,
+ "arrowParentheses": "avoid"
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "style/noExplicitAny": "off",
+ "style/noNonNullAssertion": "off",
+ "suspicious/noExplicitAny": "off",
+ "style/noVar": "off",
+ "complexity/useArrowFunctions": "off"
+ }
+ },
+ "organizeImports": {
+ "enabled": true
+ }
+}
diff --git a/devtools/biome/.biomeignore b/devtools/biome/.biomeignore
new file mode 100644
index 0000000..b010109
--- /dev/null
+++ b/devtools/biome/.biomeignore
@@ -0,0 +1,29 @@
+# node_modules and build outputs
+node_modules/
+dist/
+build/
+coverage/
+.tmp/
+.temp/
+
+# 환경설정 및 로그
+.env
+.env.*
+*.log
+
+# TypeScript compiled output
+*.js
+*.d.ts
+*.map
+
+# OS, IDE/Editor 관련
+.DS_Store
+.vscode/
+.idea/
+*.swp
+
+# 기타 무시할 파일/폴더
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
diff --git a/devtools/docker/.dockerignore b/devtools/docker/.dockerignore
new file mode 100644
index 0000000..a83c01f
--- /dev/null
+++ b/devtools/docker/.dockerignore
@@ -0,0 +1,27 @@
+# 빌드 불필요/민감 파일 차단
+node_modules/
+dist/
+.vscode/
+logs/
+coverage/
+
+# 환경설정/시크릿/포매터
+.env
+.env.*
+.eslintrc*
+.eslintignore
+.editorconfig
+.prettierrc*
+.huskyrc
+.lintstagedrc*
+jest.config.js
+
+# 도커 파일 자체
+Dockerfile
+docker-compose.yml
+
+# 시스템
+.DS_Store
+Thumbs.db
+npm-debug.log*
+yarn-error.log*
diff --git a/devtools/docker/Dockerfile.dev b/devtools/docker/Dockerfile.dev
new file mode 100644
index 0000000..00820f3
--- /dev/null
+++ b/devtools/docker/Dockerfile.dev
@@ -0,0 +1,20 @@
+# 베이스 이미지
+FROM node:20-alpine
+
+# 작업 디렉토리 생성
+WORKDIR /app
+
+# 패키지 설치(캐시 활용)
+COPY package.json pnpm-lock.yaml* yarn.lock* package-lock.json* ./
+RUN if [ -f yarn.lock ]; then yarn install; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install; \
+ else npm install; fi
+
+# 전체 코드 복사 (단, node_modules는 제외됨)
+COPY . .
+
+# 개발용 포트 (예: 3000)
+EXPOSE 3000
+
+# 개발용 명령어: nodemon, ts-node-dev 등
+CMD [ "npm", "run", "dev"]
diff --git a/devtools/docker/Dockerfile.prod b/devtools/docker/Dockerfile.prod
new file mode 100644
index 0000000..ee82926
--- /dev/null
+++ b/devtools/docker/Dockerfile.prod
@@ -0,0 +1,35 @@
+# 1. Build Stage
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+COPY package.json pnpm-lock.yaml* yarn.lock* package-lock.json* ./
+RUN if [ -f yarn.lock ]; then yarn install --production=false; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --prod=false; \
+ else npm install --production=false; fi
+
+COPY . .
+
+# TypeScript 빌드
+RUN npx tsc
+
+# 2. Run Stage (더 가벼운 이미지 사용 가능)
+FROM node:20-alpine
+
+WORKDIR /app
+
+# 의존성(production only)
+COPY package.json pnpm-lock.yaml* yarn.lock* package-lock.json* ./
+RUN if [ -f yarn.lock ]; then yarn install --production=true --frozen-lockfile; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --prod --frozen-lockfile; \
+ else npm install --production --frozen-lockfile; fi
+
+# 빌드된 코드만 복사 (node_modules, dist)
+COPY --from=builder /app/dist ./dist
+
+# 환경설정 등 추가 복사 (필요시)
+# COPY .env ./
+
+EXPOSE 3000
+
+CMD [ "npm", "start" ]
diff --git a/lib/default/Makefile b/devtools/docker/Makefile
similarity index 94%
rename from lib/default/Makefile
rename to devtools/docker/Makefile
index 624834b..12fea01 100644
--- a/lib/default/Makefile
+++ b/devtools/docker/Makefile
@@ -40,3 +40,6 @@ clean: ## Clean the images
remove: ## Remove the volumes
docker volume rm -f ${APP_NAME}
+
+logs: ## Show container logs
+ docker logs -f ${APP_NAME}
diff --git a/devtools/docker/nginx.conf b/devtools/docker/nginx.conf
new file mode 100644
index 0000000..4cf72dc
--- /dev/null
+++ b/devtools/docker/nginx.conf
@@ -0,0 +1,67 @@
+user nginx;
+worker_processes 1;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ # gzip compression (optional but recommended)
+ gzip on;
+ gzip_disable "msie6";
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+ gzip_min_length 256;
+
+ upstream api-server {
+ server server:3000; # 여러 서버면 server api1:3000; server api2:3000; ...
+ keepalive 100;
+ }
+
+ server {
+ listen 80;
+ server_name localhost;
+
+ # Health check endpoint
+ location /health {
+ return 200 'ok';
+ add_header Content-Type text/plain;
+ }
+
+ # Main reverse proxy for API
+ location / {
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_pass http://api-server;
+ proxy_read_timeout 300;
+ proxy_connect_timeout 60;
+ }
+
+ # (Optional) Static file serving example
+ # location /static/ {
+ # alias /app/public/;
+ # expires 30d;
+ # access_log off;
+ # }
+ }
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ keepalive_timeout 65;
+ include /etc/nginx/conf.d/*.conf;
+}
diff --git a/devtools/github/.github/workflows/ci.yml b/devtools/github/.github/workflows/ci.yml
new file mode 100644
index 0000000..aa05caf
--- /dev/null
+++ b/devtools/github/.github/workflows/ci.yml
@@ -0,0 +1,24 @@
+name: Node.js CI
+
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main, develop]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [20.x]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm ci
+ - run: npm run lint
+ - run: npm test
+ - run: npm run build
diff --git a/devtools/husky/.husky/commit-msg b/devtools/husky/.husky/commit-msg
new file mode 100644
index 0000000..5426a93
--- /dev/null
+++ b/devtools/husky/.husky/commit-msg
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx commitlint --edit $1
diff --git a/devtools/husky/.husky/pre-commit b/devtools/husky/.husky/pre-commit
new file mode 100644
index 0000000..449fcde
--- /dev/null
+++ b/devtools/husky/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npm test
diff --git a/devtools/husky/.husky/pre-push b/devtools/husky/.husky/pre-push
new file mode 100644
index 0000000..f22d70c
--- /dev/null
+++ b/devtools/husky/.husky/pre-push
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npm run build
diff --git a/devtools/jest/jest.config.cjs b/devtools/jest/jest.config.cjs
new file mode 100644
index 0000000..f1ad6ba
--- /dev/null
+++ b/devtools/jest/jest.config.cjs
@@ -0,0 +1,2 @@
+require('ts-node/register'); // TS config를 실행 가능하게
+module.exports = require('./jest.config.ts').default;
diff --git a/devtools/jest/jest.config.ts b/devtools/jest/jest.config.ts
new file mode 100644
index 0000000..2b6cfd7
--- /dev/null
+++ b/devtools/jest/jest.config.ts
@@ -0,0 +1,25 @@
+import type { Config } from 'jest';
+import { pathsToModuleNameMapper } from 'ts-jest';
+import { compilerOptions } from './tsconfig.json';
+
+const config: Config = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ setupFiles: ['/src/test/setup.ts'],
+ roots: ['/src'],
+ transform: {
+ '^.+\\.tsx?$': 'ts-jest',
+ },
+ moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
+ prefix: '/src/',
+ }),
+ coverageDirectory: 'coverage',
+ collectCoverageFrom: [
+ 'src/**/*.{ts,tsx}',
+ '!src/**/*.d.ts',
+ '!src/**/index.ts',
+ ],
+ testPathIgnorePatterns: ['/node_modules/', '/dist/', '/logs/'],
+};
+
+export default config;
diff --git a/devtools/jest/src/test/e2e/auth.e2e.spec.ts b/devtools/jest/src/test/e2e/auth.e2e.spec.ts
new file mode 100644
index 0000000..6e9ffe6
--- /dev/null
+++ b/devtools/jest/src/test/e2e/auth.e2e.spec.ts
@@ -0,0 +1,40 @@
+import request from 'supertest';
+import { createTestApp, resetUserDB } from '@/test/setup';
+
+describe('Auth API', () => {
+ let server: any;
+ const prefix = '/api/v1';
+
+ beforeAll(() => {
+ server = createTestApp(); // Use shared repository for testing
+ });
+
+ beforeEach(() => {
+ resetUserDB(); // Reset repository before each test
+ });
+
+ const user = { email: 'authuser@example.com', password: 'authpassword123' };
+
+ it('should successfully register a new user', async () => {
+ const res = await request(server).post(`${prefix}/auth/signup`).send(user);
+ expect(res.statusCode).toBe(201);
+ expect(res.body.data.email).toBe(user.email);
+ });
+
+ it('should login a user and set a cookie', async () => {
+ await request(server).post(`${prefix}/auth/signup`).send(user);
+ const res = await request(server).post(`${prefix}/auth/login`).send(user);
+ expect(res.statusCode).toBe(200);
+ expect(res.body.data.email).toBe(user.email);
+ expect(res.header['set-cookie']).toBeDefined();
+ });
+
+ it('should logout a user', async () => {
+ await request(server).post(`${prefix}/auth/signup`).send(user);
+ const loginRes = await request(server).post(`${prefix}/auth/login`).send(user);
+ const cookie = loginRes.headers['set-cookie'];
+ const logoutRes = await request(server).post(`${prefix}/auth/logout`).set('Cookie', cookie[0]);
+ expect(logoutRes.statusCode).toBe(200);
+ expect(logoutRes.body.message).toBe('logout');
+ });
+});
diff --git a/devtools/jest/src/test/e2e/users.e2e.spec.ts b/devtools/jest/src/test/e2e/users.e2e.spec.ts
new file mode 100644
index 0000000..d52b60c
--- /dev/null
+++ b/devtools/jest/src/test/e2e/users.e2e.spec.ts
@@ -0,0 +1,63 @@
+import request from 'supertest';
+import { createTestApp, resetUserDB } from '@/test/setup';
+
+describe('Users API', () => {
+ let server: any;
+ const prefix = '/api/v1';
+
+ beforeAll(() => {
+ server = createTestApp(); // Initialize server with shared repository
+ });
+
+ beforeEach(() => {
+ resetUserDB(); // Reset repository before each test
+ });
+
+ const user = { email: 'user1@example.com', password: 'password123' };
+
+ it('should create a new user', async () => {
+ const res = await request(server).post(`${prefix}/users`).send(user);
+ expect(res.statusCode).toBe(201);
+ expect(res.body.data.email).toBe(user.email);
+ });
+
+ it('should retrieve all users', async () => {
+ await request(server).post(`${prefix}/users`).send(user);
+ const res = await request(server).get(`${prefix}/users`);
+ expect(res.statusCode).toBe(200);
+ expect(Array.isArray(res.body.data)).toBe(true);
+ expect(res.body.data[0].email).toBe(user.email);
+ });
+
+ it('should retrieve a user by id', async () => {
+ const createRes = await request(server).post(`${prefix}/users`).send(user);
+ const id = createRes.body.data.id;
+
+ const res = await request(server).get(`${prefix}/users/${id}`);
+ expect(res.statusCode).toBe(200);
+ expect(res.body.data.email).toBe(user.email);
+ });
+
+ it('should update user information', async () => {
+ const createRes = await request(server).post(`${prefix}/users`).send(user);
+ const id = createRes.body.data.id;
+
+ const newPassword = 'newpassword123';
+ const res = await request(server).put(`${prefix}/users/${id}`).send({ password: newPassword });
+ expect(res.statusCode).toBe(200);
+ expect(res.body.data.id).toBe(id);
+ });
+
+ it('should delete a user', async () => {
+ const createRes = await request(server).post(`${prefix}/users`).send(user);
+ const id = createRes.body.data.id;
+
+ const res = await request(server).delete(`${prefix}/users/${id}`);
+ expect(res.statusCode).toBe(204);
+ });
+
+ it('should return 404 if user does not exist', async () => {
+ const res = await request(server).get(`${prefix}/users/invalid-id`);
+ expect(res.statusCode).toBe(404);
+ });
+});
diff --git a/devtools/jest/src/test/setup.ts b/devtools/jest/src/test/setup.ts
new file mode 100644
index 0000000..44e9885
--- /dev/null
+++ b/devtools/jest/src/test/setup.ts
@@ -0,0 +1,31 @@
+import 'reflect-metadata';
+import { container } from 'tsyringe';
+import App from '@/app';
+import { AuthRoute } from '@routes/auth.route';
+import { UsersRoute } from '@routes/users.route';
+import { UsersRepository, IUsersRepository } from '@repositories/users.repository';
+
+let sharedRepo: UsersRepository;
+
+export function createTestApp({ mockRepo }: { mockRepo?: IUsersRepository } = {}) {
+ // 항상 새로운 인스턴스를 주입하고 싶으면 reset logic 추가 필요
+ if (!sharedRepo) {
+ sharedRepo = new UsersRepository();
+ container.registerInstance(UsersRepository, sharedRepo);
+ }
+ // mockRepo가 있으면 주입
+ if (mockRepo) {
+ container.registerInstance(UsersRepository, mockRepo as UsersRepository);
+ }
+
+ // 클래스 타입을 직접 주입
+ const routes = [container.resolve(UsersRoute), container.resolve(AuthRoute)];
+ const appInstance = new App(routes);
+ return appInstance.getServer();
+}
+
+export function resetUserDB() {
+ if (sharedRepo) {
+ sharedRepo.reset();
+ }
+}
diff --git a/devtools/jest/src/test/unit/services/auth.service.spec.ts b/devtools/jest/src/test/unit/services/auth.service.spec.ts
new file mode 100644
index 0000000..805d2db
--- /dev/null
+++ b/devtools/jest/src/test/unit/services/auth.service.spec.ts
@@ -0,0 +1,69 @@
+import { compare, hash } from 'bcryptjs';
+import { CreateUserDto } from '@dtos/users.dto';
+import { User } from '@interfaces/users.interface';
+import { UsersRepository } from '@repositories/users.repository';
+import { AuthService } from '@services/auth.service';
+
+describe('AuthService (with UserMemoryRepository)', () => {
+ let authService: AuthService;
+ let userRepo: UsersRepository;
+ const testUser: User = {
+ id: '1',
+ email: 'authuser@example.com',
+ password: '', // will be replaced with actual hash
+ };
+
+ beforeEach(async () => {
+ userRepo = new UsersRepository();
+ testUser.password = await hash('plainpw', 10);
+ // Add initial user
+ await userRepo.save({ ...testUser });
+ authService = new AuthService(userRepo); // inject repo
+ });
+
+ it('should sign up a new user', async () => {
+ const dto: CreateUserDto = {
+ email: 'newuser@example.com',
+ password: 'newpassword123',
+ };
+ const created = await authService.signup(dto);
+ expect(created.email).toBe(dto.email);
+
+ const found = await userRepo.findByEmail(dto.email);
+ expect(found).toBeDefined();
+ expect(await compare(dto.password, found!.password)).toBe(true);
+ });
+
+ it('should throw an error if email is already in use', async () => {
+ const dto: CreateUserDto = {
+ email: testUser.email,
+ password: 'anypw',
+ };
+ await expect(authService.signup(dto)).rejects.toThrow(/already in use/);
+ });
+
+ it('should return user and cookie on successful login', async () => {
+ // Create user with hashed password
+ const plainPassword = 'mySecret123';
+ const email = 'loginuser@example.com';
+ const hashed = await hash(plainPassword, 10);
+ await userRepo.save({ id: '2', email, password: hashed });
+
+ const result = await authService.login({ email, password: plainPassword });
+ expect(result.user.email).toBe(email);
+ expect(result.cookie).toContain('Authorization=');
+ });
+
+ it('should throw an error if email or password is incorrect', async () => {
+ // Non-existing email
+ await expect(authService.login({ email: 'nobody@example.com', password: 'xxx' })).rejects.toThrow(/Invalid email or password/i);
+
+ // Incorrect password
+ const email = testUser.email;
+ await expect(authService.login({ email, password: 'wrongpw' })).rejects.toThrow(/password/i);
+ });
+
+ it('should successfully logout without errors', async () => {
+ await expect(authService.logout(testUser)).resolves.toBeUndefined();
+ });
+});
diff --git a/devtools/jest/src/test/unit/services/users.service.spec.ts b/devtools/jest/src/test/unit/services/users.service.spec.ts
new file mode 100644
index 0000000..8d117a8
--- /dev/null
+++ b/devtools/jest/src/test/unit/services/users.service.spec.ts
@@ -0,0 +1,89 @@
+import { User } from '@interfaces/users.interface';
+import { UsersRepository } from '@repositories/users.repository';
+import { UsersService } from '@services/users.service';
+
+describe('UsersService (with UsersRepository)', () => {
+ let usersService: UsersService;
+ let userRepo: UsersRepository;
+
+ // Sample user data
+ const user1: User & { id: string } = { id: '1', email: 'one@example.com', password: 'pw1' };
+ const user2: User & { id: string } = { id: '2', email: 'two@example.com', password: 'pw2' };
+
+ beforeEach(async () => {
+ userRepo = new UsersRepository();
+ userRepo.reset();
+ // Directly save users (save is async but await is optional for init)
+ await userRepo.save({ ...user1 });
+ await userRepo.save({ ...user2 });
+ usersService = new UsersService(userRepo);
+ });
+
+ it('getAllUsers: should return all users', async () => {
+ const users = await usersService.getAllUsers();
+ expect(users.length).toBe(2);
+ expect(users[0].email).toBe(user1.email);
+ });
+
+ it('getUserById: should return user by ID', async () => {
+ const user = await usersService.getUserById('2');
+ expect(user.email).toBe(user2.email);
+ });
+
+ it('getUserById: should throw if ID does not exist', async () => {
+ await expect(usersService.getUserById('999')).rejects.toThrow(/not found/);
+ });
+
+ it('createUser: should add a new user', async () => {
+ const created = await usersService.createUser({
+ id: '', // ignored
+ email: 'new@example.com',
+ password: 'pw3',
+ });
+ expect(created.email).toBe('new@example.com');
+ const all = await usersService.getAllUsers();
+ expect(all.length).toBe(3);
+ });
+
+ it('createUser: should throw if email already exists', async () => {
+ await expect(
+ usersService.createUser({
+ id: '',
+ email: user1.email,
+ password: 'pwX',
+ }),
+ ).rejects.toThrow(/exists/);
+ });
+
+ it('updateUser: should update user password', async () => {
+ const newPassword = 'newpw';
+ const updated = await usersService.updateUser(user2.id as string, {
+ id: user2.id as string,
+ email: user2.email,
+ password: newPassword,
+ });
+ expect(updated).toBeDefined();
+ expect(updated!.password).not.toBe(user2.password); // changed to hashed value
+ });
+
+ it('updateUser: should throw if ID does not exist', async () => {
+ await expect(
+ usersService.updateUser('999', {
+ id: '999',
+ email: 'no@no.com',
+ password: 'no',
+ }),
+ ).rejects.toThrow(/not found/);
+ });
+
+ it('deleteUser: should delete user successfully', async () => {
+ await usersService.deleteUser(user1.id as string);
+ const users = await usersService.getAllUsers();
+ expect(users.length).toBe(1);
+ expect(users[0].id).toBe(user2.id);
+ });
+
+ it('deleteUser: should throw if ID does not exist', async () => {
+ await expect(usersService.deleteUser('999')).rejects.toThrow(/not found/);
+ });
+});
diff --git a/devtools/pm2/ecosystem.config.js b/devtools/pm2/ecosystem.config.js
new file mode 100644
index 0000000..3901d93
--- /dev/null
+++ b/devtools/pm2/ecosystem.config.js
@@ -0,0 +1,60 @@
+module.exports = {
+ apps: [
+ // --- PRODUCTION ---
+ {
+ name: 'prod',
+ script: 'dist/server.js',
+ exec_mode: 'cluster',
+ instances: 'max',
+ autorestart: true,
+ watch: false,
+ max_memory_restart: '1G',
+ output: './logs/access.log',
+ error: './logs/error.log',
+ merge_logs: true,
+ time: true, // 로그에 타임스탬프
+ log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS',
+ node_args: '--enable-source-maps', // 소스맵 스택트레이스
+ env: {
+ PORT: 3000,
+ NODE_ENV: 'production',
+ },
+ // env_production: { ... } // 필요 시 분리도 가능
+ },
+
+ // --- DEVELOPMENT ---
+ {
+ name: 'dev',
+ script: 'src/server.ts',
+ interpreter: 'node',
+ node_args: '-r ts-node/register -r tsconfig-paths/register --enable-source-maps',
+ exec_mode: 'fork',
+ instances: 1,
+ autorestart: true,
+ watch: ['src'],
+ watch_delay: 300, // 저장 폭주 시 완충
+ ignore_watch: ['node_modules', 'logs', 'dist'],
+ max_memory_restart: '1G',
+ output: './logs/access.log',
+ error: './logs/error.log',
+ merge_logs: true,
+ time: true,
+ env: {
+ PORT: 3000,
+ NODE_ENV: 'development',
+ },
+ },
+ ],
+
+ deploy: {
+ production: {
+ user: 'user',
+ host: '0.0.0.0',
+ ref: 'origin/master',
+ repo: 'git@github.com:repo.git',
+ path: '/home/user/app',
+ 'post-deploy':
+ 'npm install && npm run build && pm2 reload ecosystem.config.js --env production',
+ },
+ },
+};
diff --git a/devtools/prettier/.prettierrc b/devtools/prettier/.prettierrc
new file mode 100644
index 0000000..1f67368
--- /dev/null
+++ b/devtools/prettier/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "printWidth": 150,
+ "tabWidth": 2,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "semi": true,
+ "arrowParens": "avoid"
+}
diff --git a/devtools/prettier/eslint.config.js b/devtools/prettier/eslint.config.js
new file mode 100644
index 0000000..38cef6f
--- /dev/null
+++ b/devtools/prettier/eslint.config.js
@@ -0,0 +1,25 @@
+import tseslint from 'typescript-eslint';
+import globals from 'globals';
+import eslintConfigPrettier from 'eslint-config-prettier';
+
+export default [
+ {
+ ignores: ['dist/', 'coverage/', 'logs/', 'node_modules/', '*.config.js', '*.config.ts', '.env', '.env.*', '!.eslintrc.js'],
+ },
+ ...tseslint.configs.recommended,
+ eslintConfigPrettier,
+ {
+ files: ['**/*.ts'],
+ languageOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ globals: { ...globals.node, ...globals.es2022 },
+ },
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/ban-types': 'off',
+ '@typescript-eslint/no-var-requires': 'off',
+ },
+ },
+];
diff --git a/lib/default/.swcrc b/devtools/swc/.swcrc
similarity index 91%
rename from lib/default/.swcrc
rename to devtools/swc/.swcrc
index cf100a9..d427bc3 100644
--- a/lib/default/.swcrc
+++ b/devtools/swc/.swcrc
@@ -21,13 +21,13 @@
"baseUrl": "src",
"paths": {
"@/*": ["*"],
- "@config": ["config"],
+ "@config/*": ["config/*"],
"@controllers/*": ["controllers/*"],
"@dtos/*": ["dtos/*"],
"@exceptions/*": ["exceptions/*"],
"@interfaces/*": ["interfaces/*"],
"@middlewares/*": ["middlewares/*"],
- "@models/*": ["models/*"],
+ "@repositories/*": ["repositories/*"],
"@routes/*": ["routes/*"],
"@services/*": ["services/*"],
"@utils/*": ["utils/*"]
diff --git a/devtools/tsup/tsup.config.ts b/devtools/tsup/tsup.config.ts
new file mode 100644
index 0000000..9184f14
--- /dev/null
+++ b/devtools/tsup/tsup.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: ['src/server.ts'], // CLI 등 추가시 여러 entry도 가능
+ outDir: 'dist', // 출력 디렉토리
+ format: ['cjs'], // 필요시 'esm'도 ['cjs', 'esm']
+ dts: true, // 타입 선언 파일(.d.ts) 생성
+ sourcemap: true, // 소스맵 생성
+ clean: true, // 빌드 전 dist 폴더 정리
+ target: 'es2020', // 트랜스파일 대상
+ minify: false, // JS 파일을 압축(minify) 해서 더 작게 만듭니다.
+ treeshake: true, //사용하지 않는 코드를 제거합니다. 기본은 true지만 명시
+ // alias, swc 옵션은 tsconfig.json 기준으로 자동 적용
+ // external: [], // node_modules 제외하고 싶을 때
+});
diff --git a/devtools/vitest/src/test/e2e/auth.e2e.spec.ts b/devtools/vitest/src/test/e2e/auth.e2e.spec.ts
new file mode 100644
index 0000000..f110cb5
--- /dev/null
+++ b/devtools/vitest/src/test/e2e/auth.e2e.spec.ts
@@ -0,0 +1,41 @@
+import request from 'supertest';
+import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
+import { createTestApp, resetUserDB } from '@/test/setup';
+
+describe('Auth API', () => {
+ let server: any;
+ const prefix = '/api/v1';
+
+ beforeAll(() => {
+ server = createTestApp(); // Use shared repository for testing
+ });
+
+ beforeEach(() => {
+ resetUserDB(); // Reset repository before each test
+ });
+
+ const user = { email: 'authuser@example.com', password: 'authpassword123' };
+
+ it('should successfully register a new user', async () => {
+ const res = await request(server).post(`${prefix}/auth/signup`).send(user);
+ expect(res.statusCode).toBe(201);
+ expect(res.body.data.email).toBe(user.email);
+ });
+
+ it('should login a user and set a cookie', async () => {
+ await request(server).post(`${prefix}/auth/signup`).send(user);
+ const res = await request(server).post(`${prefix}/auth/login`).send(user);
+ expect(res.statusCode).toBe(200);
+ expect(res.body.data.email).toBe(user.email);
+ expect(res.header['set-cookie']).toBeDefined();
+ });
+
+ it('should logout a user', async () => {
+ await request(server).post(`${prefix}/auth/signup`).send(user);
+ const loginRes = await request(server).post(`${prefix}/auth/login`).send(user);
+ const cookie = loginRes.headers['set-cookie'];
+ const logoutRes = await request(server).post(`${prefix}/auth/logout`).set('Cookie', cookie[0]);
+ expect(logoutRes.statusCode).toBe(200);
+ expect(logoutRes.body.message).toBe('logout');
+ });
+});
diff --git a/devtools/vitest/src/test/e2e/users.e2e.spec.ts b/devtools/vitest/src/test/e2e/users.e2e.spec.ts
new file mode 100644
index 0000000..69f8cbf
--- /dev/null
+++ b/devtools/vitest/src/test/e2e/users.e2e.spec.ts
@@ -0,0 +1,64 @@
+import request from 'supertest';
+import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
+import { createTestApp, resetUserDB } from '@/test/setup';
+
+describe('Users API', () => {
+ let server: any;
+ const prefix = '/api/v1';
+
+ beforeAll(() => {
+ server = createTestApp(); // Initialize server with shared repository
+ });
+
+ beforeEach(() => {
+ resetUserDB(); // Reset repository before each test
+ });
+
+ const user = { email: 'user1@example.com', password: 'password123' };
+
+ it('should create a new user', async () => {
+ const res = await request(server).post(`${prefix}/users`).send(user);
+ expect(res.statusCode).toBe(201);
+ expect(res.body.data.email).toBe(user.email);
+ });
+
+ it('should retrieve all users', async () => {
+ await request(server).post(`${prefix}/users`).send(user);
+ const res = await request(server).get(`${prefix}/users`);
+ expect(res.statusCode).toBe(200);
+ expect(Array.isArray(res.body.data)).toBe(true);
+ expect(res.body.data[0].email).toBe(user.email);
+ });
+
+ it('should retrieve a user by id', async () => {
+ const createRes = await request(server).post(`${prefix}/users`).send(user);
+ const id = createRes.body.data.id;
+
+ const res = await request(server).get(`${prefix}/users/${id}`);
+ expect(res.statusCode).toBe(200);
+ expect(res.body.data.email).toBe(user.email);
+ });
+
+ it('should update user information', async () => {
+ const createRes = await request(server).post(`${prefix}/users`).send(user);
+ const id = createRes.body.data.id;
+
+ const newPassword = 'newpassword123';
+ const res = await request(server).put(`${prefix}/users/${id}`).send({ password: newPassword });
+ expect(res.statusCode).toBe(200);
+ expect(res.body.data.id).toBe(id);
+ });
+
+ it('should delete a user', async () => {
+ const createRes = await request(server).post(`${prefix}/users`).send(user);
+ const id = createRes.body.data.id;
+
+ const res = await request(server).delete(`${prefix}/users/${id}`);
+ expect(res.statusCode).toBe(204);
+ });
+
+ it('should return 404 if user does not exist', async () => {
+ const res = await request(server).get(`${prefix}/users/invalid-id`);
+ expect(res.statusCode).toBe(404);
+ });
+});
diff --git a/devtools/vitest/src/test/setup.ts b/devtools/vitest/src/test/setup.ts
new file mode 100644
index 0000000..44e9885
--- /dev/null
+++ b/devtools/vitest/src/test/setup.ts
@@ -0,0 +1,31 @@
+import 'reflect-metadata';
+import { container } from 'tsyringe';
+import App from '@/app';
+import { AuthRoute } from '@routes/auth.route';
+import { UsersRoute } from '@routes/users.route';
+import { UsersRepository, IUsersRepository } from '@repositories/users.repository';
+
+let sharedRepo: UsersRepository;
+
+export function createTestApp({ mockRepo }: { mockRepo?: IUsersRepository } = {}) {
+ // 항상 새로운 인스턴스를 주입하고 싶으면 reset logic 추가 필요
+ if (!sharedRepo) {
+ sharedRepo = new UsersRepository();
+ container.registerInstance(UsersRepository, sharedRepo);
+ }
+ // mockRepo가 있으면 주입
+ if (mockRepo) {
+ container.registerInstance(UsersRepository, mockRepo as UsersRepository);
+ }
+
+ // 클래스 타입을 직접 주입
+ const routes = [container.resolve(UsersRoute), container.resolve(AuthRoute)];
+ const appInstance = new App(routes);
+ return appInstance.getServer();
+}
+
+export function resetUserDB() {
+ if (sharedRepo) {
+ sharedRepo.reset();
+ }
+}
diff --git a/devtools/vitest/src/test/unit/services/auth.service.spec.ts b/devtools/vitest/src/test/unit/services/auth.service.spec.ts
new file mode 100644
index 0000000..273ada8
--- /dev/null
+++ b/devtools/vitest/src/test/unit/services/auth.service.spec.ts
@@ -0,0 +1,70 @@
+import { compare, hash } from 'bcryptjs';
+import { describe, it, expect, beforeEach } from 'vitest';
+import { CreateUserDto } from '@dtos/users.dto';
+import { User } from '@interfaces/users.interface';
+import { UsersRepository } from '@repositories/users.repository';
+import { AuthService } from '@services/auth.service';
+
+describe('AuthService (with UserMemoryRepository)', () => {
+ let authService: AuthService;
+ let userRepo: UsersRepository;
+ const testUser: User = {
+ id: '1',
+ email: 'authuser@example.com',
+ password: '', // will be replaced with actual hash
+ };
+
+ beforeEach(async () => {
+ userRepo = new UsersRepository();
+ testUser.password = await hash('plainpw', 10);
+ // Add initial user
+ await userRepo.save({ ...testUser });
+ authService = new AuthService(userRepo); // inject repo
+ });
+
+ it('should sign up a new user', async () => {
+ const dto: CreateUserDto = {
+ email: 'newuser@example.com',
+ password: 'newpassword123',
+ };
+ const created = await authService.signup(dto);
+ expect(created.email).toBe(dto.email);
+
+ const found = await userRepo.findByEmail(dto.email);
+ expect(found).toBeDefined();
+ expect(await compare(dto.password, found!.password)).toBe(true);
+ });
+
+ it('should throw an error if email is already in use', async () => {
+ const dto: CreateUserDto = {
+ email: testUser.email,
+ password: 'anypw',
+ };
+ await expect(authService.signup(dto)).rejects.toThrow(/already in use/);
+ });
+
+ it('should return user and cookie on successful login', async () => {
+ // Create user with hashed password
+ const plainPassword = 'mySecret123';
+ const email = 'loginuser@example.com';
+ const hashed = await hash(plainPassword, 10);
+ await userRepo.save({ id: '2', email, password: hashed });
+
+ const result = await authService.login({ email, password: plainPassword });
+ expect(result.user.email).toBe(email);
+ expect(result.cookie).toContain('Authorization=');
+ });
+
+ it('should throw an error if email or password is incorrect', async () => {
+ // Non-existing email
+ await expect(authService.login({ email: 'nobody@example.com', password: 'xxx' })).rejects.toThrow(/Invalid email or password/i);
+
+ // Incorrect password
+ const email = testUser.email;
+ await expect(authService.login({ email, password: 'wrongpw' })).rejects.toThrow(/password/i);
+ });
+
+ it('should successfully logout without errors', async () => {
+ await expect(authService.logout(testUser)).resolves.toBeUndefined();
+ });
+});
diff --git a/devtools/vitest/src/test/unit/services/users.service.spec.ts b/devtools/vitest/src/test/unit/services/users.service.spec.ts
new file mode 100644
index 0000000..b540fa8
--- /dev/null
+++ b/devtools/vitest/src/test/unit/services/users.service.spec.ts
@@ -0,0 +1,90 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { User } from '@interfaces/users.interface';
+import { UsersRepository } from '@repositories/users.repository';
+import { UsersService } from '@services/users.service';
+
+describe('UsersService (with UsersRepository)', () => {
+ let usersService: UsersService;
+ let userRepo: UsersRepository;
+
+ // Sample user data
+ const user1: User & { id: string } = { id: '1', email: 'one@example.com', password: 'pw1' };
+ const user2: User & { id: string } = { id: '2', email: 'two@example.com', password: 'pw2' };
+
+ beforeEach(async () => {
+ userRepo = new UsersRepository();
+ userRepo.reset();
+ // Directly save users (save is async but await is optional for init)
+ await userRepo.save({ ...user1 });
+ await userRepo.save({ ...user2 });
+ usersService = new UsersService(userRepo);
+ });
+
+ it('getAllUsers: should return all users', async () => {
+ const users = await usersService.getAllUsers();
+ expect(users.length).toBe(2);
+ expect(users[0].email).toBe(user1.email);
+ });
+
+ it('getUserById: should return user by ID', async () => {
+ const user = await usersService.getUserById('2');
+ expect(user.email).toBe(user2.email);
+ });
+
+ it('getUserById: should throw if ID does not exist', async () => {
+ await expect(usersService.getUserById('999')).rejects.toThrow(/not found/);
+ });
+
+ it('createUser: should add a new user', async () => {
+ const created = await usersService.createUser({
+ id: '', // ignored
+ email: 'new@example.com',
+ password: 'pw3',
+ });
+ expect(created.email).toBe('new@example.com');
+ const all = await usersService.getAllUsers();
+ expect(all.length).toBe(3);
+ });
+
+ it('createUser: should throw if email already exists', async () => {
+ await expect(
+ usersService.createUser({
+ id: '',
+ email: user1.email,
+ password: 'pwX',
+ }),
+ ).rejects.toThrow(/exists/);
+ });
+
+ it('updateUser: should update user password', async () => {
+ const newPassword = 'newpw';
+ const updated = await usersService.updateUser(user2.id as string, {
+ id: user2.id as string,
+ email: user2.email,
+ password: newPassword,
+ });
+ expect(updated).toBeDefined();
+ expect(updated!.password).not.toBe(user2.password); // changed to hashed value
+ });
+
+ it('updateUser: should throw if ID does not exist', async () => {
+ await expect(
+ usersService.updateUser('999', {
+ id: '999',
+ email: 'no@no.com',
+ password: 'no',
+ }),
+ ).rejects.toThrow(/not found/);
+ });
+
+ it('deleteUser: should delete user successfully', async () => {
+ await usersService.deleteUser(user1.id as string);
+ const users = await usersService.getAllUsers();
+ expect(users.length).toBe(1);
+ expect(users[0].id).toBe(user2.id);
+ });
+
+ it('deleteUser: should throw if ID does not exist', async () => {
+ await expect(usersService.deleteUser('999')).rejects.toThrow(/not found/);
+ });
+});
diff --git a/devtools/vitest/vitest.config.ts b/devtools/vitest/vitest.config.ts
new file mode 100644
index 0000000..bbd0dfb
--- /dev/null
+++ b/devtools/vitest/vitest.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vitest/config';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+export default defineConfig({
+ plugins: [tsconfigPaths()],
+ test: {
+ environment: 'node',
+ globals: true, // 전역 expect/describe/it 사용 시 편의. 원치 않으면 제거
+ setupFiles: ['src/test/setup.ts'],
+ include: ['src/test/**/*.{test,spec}.ts'], // src/test/e2e, src/test/unit에 최적화
+ exclude: ['node_modules', 'dist', 'coverage', 'logs'],
+ coverage: {
+ provider: 'v8', // v8 사용 시
+ reporter: ['text', 'html', 'lcov'],
+ reportsDirectory: 'coverage',
+ include: ['src/**/*.{ts,tsx}'],
+ exclude: ['src/**/*.d.ts', 'src/**/index.ts', 'src/test/**'],
+ },
+ // e2e에서 포트/자원 충돌나면 단일 스레드:
+ // poolOptions: { threads: { singleThread: true } },
+ },
+});
diff --git a/lib/default/.dockerignore b/lib/default/.dockerignore
deleted file mode 100644
index 0b2f116..0000000
--- a/lib/default/.dockerignore
+++ /dev/null
@@ -1,18 +0,0 @@
-# compiled output
-.vscode
-/node_modules
-
-# code formatter
-.eslintrc
-.eslintignore
-.editorconfig
-.huskyrc
-.lintstagedrc.json
-.prettierrc
-
-# test
-jest.config.js
-
-# docker
-Dockerfile
-docker-compose.yml
diff --git a/lib/default/.editorconfig b/lib/default/.editorconfig
deleted file mode 100644
index c6c8b36..0000000
--- a/lib/default/.editorconfig
+++ /dev/null
@@ -1,9 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-indent_size = 2
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
diff --git a/lib/default/.env.development.local b/lib/default/.env.development.local
deleted file mode 100644
index dc6fced..0000000
--- a/lib/default/.env.development.local
+++ /dev/null
@@ -1,13 +0,0 @@
-# PORT
-PORT = 3000
-
-# TOKEN
-SECRET_KEY = secretKey
-
-# LOG
-LOG_FORMAT = dev
-LOG_DIR = ../logs
-
-# CORS
-ORIGIN = *
-CREDENTIALS = true
diff --git a/lib/default/.env.production.local b/lib/default/.env.production.local
deleted file mode 100644
index dad9936..0000000
--- a/lib/default/.env.production.local
+++ /dev/null
@@ -1,13 +0,0 @@
-# PORT
-PORT = 3000
-
-# TOKEN
-SECRET_KEY = secretKey
-
-# LOG
-LOG_FORMAT = combined
-LOG_DIR = ../logs
-
-# CORS
-ORIGIN = your.domain.com
-CREDENTIALS = true
diff --git a/lib/default/.env.test.local b/lib/default/.env.test.local
deleted file mode 100644
index dc6fced..0000000
--- a/lib/default/.env.test.local
+++ /dev/null
@@ -1,13 +0,0 @@
-# PORT
-PORT = 3000
-
-# TOKEN
-SECRET_KEY = secretKey
-
-# LOG
-LOG_FORMAT = dev
-LOG_DIR = ../logs
-
-# CORS
-ORIGIN = *
-CREDENTIALS = true
diff --git a/lib/default/.eslintignore b/lib/default/.eslintignore
deleted file mode 100644
index 3e22129..0000000
--- a/lib/default/.eslintignore
+++ /dev/null
@@ -1 +0,0 @@
-/dist
\ No newline at end of file
diff --git a/lib/default/.eslintrc b/lib/default/.eslintrc
deleted file mode 100644
index 206ab05..0000000
--- a/lib/default/.eslintrc
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "parser": "@typescript-eslint/parser",
- "extends": ["prettier", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
- "parserOptions": {
- "ecmaVersion": 2018,
- "sourceType": "module"
- },
- "rules": {
- "@typescript-eslint/explicit-member-accessibility": 0,
- "@typescript-eslint/explicit-function-return-type": 0,
- "@typescript-eslint/no-parameter-properties": 0,
- "@typescript-eslint/interface-name-prefix": 0,
- "@typescript-eslint/explicit-module-boundary-types": 0,
- "@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/ban-types": "off",
- "@typescript-eslint/no-var-requires": "off"
- }
-}
diff --git a/lib/default/.huskyrc b/lib/default/.huskyrc
deleted file mode 100644
index 4d077c8..0000000
--- a/lib/default/.huskyrc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "hooks": {
- "pre-commit": "lint-staged"
- }
-}
diff --git a/lib/default/.lintstagedrc.json b/lib/default/.lintstagedrc.json
deleted file mode 100644
index d2fe776..0000000
--- a/lib/default/.lintstagedrc.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "*.ts": [
- "npm run lint"
- ]
-}
\ No newline at end of file
diff --git a/lib/default/.prettierrc b/lib/default/.prettierrc
deleted file mode 100644
index 93a5aaf..0000000
--- a/lib/default/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "printWidth": 150,
- "tabWidth": 2,
- "singleQuote": true,
- "trailingComma": "all",
- "semi": true,
- "arrowParens": "avoid"
-}
\ No newline at end of file
diff --git a/lib/default/.vscode/launch.json b/lib/default/.vscode/launch.json
deleted file mode 100644
index 00ccfd7..0000000
--- a/lib/default/.vscode/launch.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "version": "0.2.0",
- "configurations": [
- {
- "type": "node-terminal",
- "request": "launch",
- "name": "Dev typescript-express-starter",
- "command": "npm run dev"
- },
- {
- "type": "node-terminal",
- "request": "launch",
- "name": "Start typescript-express-starter",
- "command": "npm run start"
- },
- {
- "type": "node-terminal",
- "request": "launch",
- "name": "Test typescript-express-starter",
- "command": "npm run test"
- },
- {
- "type": "node-terminal",
- "request": "launch",
- "name": "Lint typescript-express-starter",
- "command": "npm run lint"
- },
- {
- "type": "node-terminal",
- "request": "launch",
- "name": "Lint:Fix typescript-express-starter",
- "command": "npm run lint:fix"
- }
- ]
-}
diff --git a/lib/default/.vscode/settings.json b/lib/default/.vscode/settings.json
deleted file mode 100644
index 42ab89c..0000000
--- a/lib/default/.vscode/settings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "editor.codeActionsOnSave": {
- "source.fixAll.eslint": true
- },
- "editor.formatOnSave": false
-}
diff --git a/lib/default/Dockerfile.dev b/lib/default/Dockerfile.dev
deleted file mode 100644
index 0fbb991..0000000
--- a/lib/default/Dockerfile.dev
+++ /dev/null
@@ -1,18 +0,0 @@
-# NodeJS Version 16
-FROM node:16.18-buster-slim
-
-# Copy Dir
-COPY . ./app
-
-# Work to Dir
-WORKDIR /app
-
-# Install Node Package
-RUN npm install --legacy-peer-deps
-
-# Set Env
-ENV NODE_ENV development
-EXPOSE 3000
-
-# Cmd script
-CMD ["npm", "run", "dev"]
diff --git a/lib/default/Dockerfile.prod b/lib/default/Dockerfile.prod
deleted file mode 100644
index 2f81c34..0000000
--- a/lib/default/Dockerfile.prod
+++ /dev/null
@@ -1,18 +0,0 @@
-# NodeJS Version 16
-FROM node:16.18-buster-slim
-
-# Copy Dir
-COPY . ./app
-
-# Work to Dir
-WORKDIR /app
-
-# Install Node Package
-RUN npm install --legacy-peer-deps
-
-# Set Env
-ENV NODE_ENV production
-EXPOSE 3000
-
-# Cmd script
-CMD ["npm", "run", "start"]
diff --git a/lib/default/docker-compose.yml b/lib/default/docker-compose.yml
deleted file mode 100644
index 8138f59..0000000
--- a/lib/default/docker-compose.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-version: "3.9"
-
-services:
- proxy:
- container_name: proxy
- image: nginx:alpine
- ports:
- - "80:80"
- volumes:
- - ./nginx.conf:/etc/nginx/nginx.conf
- restart: "unless-stopped"
- networks:
- - backend
-
- server:
- container_name: server
- build:
- context: ./
- dockerfile: Dockerfile.dev
- ports:
- - "3000:3000"
- volumes:
- - ./:/app
- - /app/node_modules
- restart: 'unless-stopped'
- networks:
- - backend
-
-networks:
- backend:
- driver: bridge
-
-volumes:
- data:
- driver: local
diff --git a/lib/default/ecosystem.config.js b/lib/default/ecosystem.config.js
deleted file mode 100644
index 94cfa72..0000000
--- a/lib/default/ecosystem.config.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * @description pm2 configuration file.
- * @example
- * production mode :: pm2 start ecosystem.config.js --only prod
- * development mode :: pm2 start ecosystem.config.js --only dev
- */
- module.exports = {
- apps: [
- {
- name: 'prod', // pm2 start App name
- script: 'dist/server.js',
- exec_mode: 'cluster', // 'cluster' or 'fork'
- instance_var: 'INSTANCE_ID', // instance variable
- instances: 2, // pm2 instance count
- autorestart: true, // auto restart if process crash
- watch: false, // files change automatic restart
- ignore_watch: ['node_modules', 'logs'], // ignore files change
- max_memory_restart: '1G', // restart if process use more than 1G memory
- merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log
- output: './logs/access.log', // pm2 log file
- error: './logs/error.log', // pm2 error log file
- env: { // environment variable
- PORT: 3000,
- NODE_ENV: 'production',
- },
- },
- {
- name: 'dev', // pm2 start App name
- script: 'ts-node', // ts-node
- args: '-r tsconfig-paths/register --transpile-only src/server.ts', // ts-node args
- exec_mode: 'cluster', // 'cluster' or 'fork'
- instance_var: 'INSTANCE_ID', // instance variable
- instances: 2, // pm2 instance count
- autorestart: true, // auto restart if process crash
- watch: false, // files change automatic restart
- ignore_watch: ['node_modules', 'logs'], // ignore files change
- max_memory_restart: '1G', // restart if process use more than 1G memory
- merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log
- output: './logs/access.log', // pm2 log file
- error: './logs/error.log', // pm2 error log file
- env: { // environment variable
- PORT: 3000,
- NODE_ENV: 'development',
- },
- },
- ],
- deploy: {
- production: {
- user: 'user',
- host: '0.0.0.0',
- ref: 'origin/master',
- repo: 'git@github.com:repo.git',
- path: 'dist/server.js',
- 'post-deploy': 'npm install && npm run build && pm2 reload ecosystem.config.js --only prod',
- },
- },
-};
diff --git a/lib/default/jest.config.js b/lib/default/jest.config.js
deleted file mode 100644
index 8edf5ba..0000000
--- a/lib/default/jest.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const { pathsToModuleNameMapper } = require('ts-jest');
-const { compilerOptions } = require('./tsconfig.json');
-
-module.exports = {
- preset: 'ts-jest',
- testEnvironment: 'node',
- roots: ['/src'],
- transform: {
- '^.+\\.tsx?$': 'ts-jest',
- },
- moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/src' }),
-};
diff --git a/lib/default/nginx.conf b/lib/default/nginx.conf
deleted file mode 100644
index 7cb62f6..0000000
--- a/lib/default/nginx.conf
+++ /dev/null
@@ -1,40 +0,0 @@
-user nginx;
-worker_processes 1;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 1024;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- upstream api-server {
- server server:3000;
- keepalive 100;
- }
-
- server {
- listen 80;
- server_name localhost;
-
- location / {
- proxy_http_version 1.1;
- proxy_pass http://api-server;
- }
-
- }
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
-
- sendfile on;
- keepalive_timeout 65;
- include /etc/nginx/conf.d/*.conf;
-}
diff --git a/lib/default/nodemon.json b/lib/default/nodemon.json
deleted file mode 100644
index 9c4580d..0000000
--- a/lib/default/nodemon.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "watch": [
- "src",
- ".env"
- ],
- "ext": "js,ts,json",
- "ignore": [
- "src/logs/*",
- "src/**/*.{spec,test}.ts"
- ],
- "exec": "ts-node -r tsconfig-paths/register --transpile-only src/server.ts"
-}
\ No newline at end of file
diff --git a/lib/default/package.json b/lib/default/package.json
deleted file mode 100644
index 0b2b639..0000000
--- a/lib/default/package.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
- "name": "typescript-express-starter",
- "version": "0.0.0",
- "description": "TypeScript + Express API Server",
- "author": "",
- "license": "ISC",
- "scripts": {
- "start": "npm run build && cross-env NODE_ENV=production node dist/server.js",
- "dev": "cross-env NODE_ENV=development nodemon",
- "build": "swc src -d dist --source-maps --copy-files",
- "build:tsc": "tsc && tsc-alias",
- "test": "jest --forceExit --detectOpenHandles",
- "lint": "eslint --ignore-path .gitignore --ext .ts src",
- "lint:fix": "npm run lint -- --fix",
- "deploy:prod": "npm run build && pm2 start ecosystem.config.js --only prod",
- "deploy:dev": "pm2 start ecosystem.config.js --only dev"
- },
- "dependencies": {
- "bcrypt": "^5.0.1",
- "class-transformer": "^0.5.1",
- "class-validator": "^0.13.2",
- "compression": "^1.7.4",
- "cookie-parser": "^1.4.6",
- "cors": "^2.8.5",
- "dotenv": "^16.0.1",
- "envalid": "^7.3.1",
- "express": "^4.18.1",
- "helmet": "^5.1.1",
- "hpp": "^0.2.3",
- "jsonwebtoken": "^8.5.1",
- "morgan": "^1.10.0",
- "reflect-metadata": "^0.1.13",
- "swagger-jsdoc": "^6.2.1",
- "swagger-ui-express": "^4.5.0",
- "typedi": "^0.10.0",
- "winston": "^3.8.1",
- "winston-daily-rotate-file": "^4.7.1"
- },
- "devDependencies": {
- "@swc/cli": "^0.1.57",
- "@swc/core": "^1.2.220",
- "@types/bcrypt": "^5.0.0",
- "@types/compression": "^1.7.2",
- "@types/cookie-parser": "^1.4.3",
- "@types/cors": "^2.8.12",
- "@types/express": "^4.17.13",
- "@types/hpp": "^0.2.2",
- "@types/jest": "^28.1.6",
- "@types/jsonwebtoken": "^8.5.8",
- "@types/morgan": "^1.9.3",
- "@types/node": "^17.0.45",
- "@types/supertest": "^2.0.12",
- "@types/swagger-jsdoc": "^6.0.1",
- "@types/swagger-ui-express": "^4.1.3",
- "@typescript-eslint/eslint-plugin": "^5.29.0",
- "@typescript-eslint/parser": "^5.29.0",
- "cross-env": "^7.0.3",
- "eslint": "^8.20.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-prettier": "^4.2.1",
- "husky": "^8.0.1",
- "jest": "^28.1.1",
- "lint-staged": "^13.0.3",
- "node-config": "^0.0.2",
- "node-gyp": "^9.1.0",
- "nodemon": "^2.0.19",
- "pm2": "^5.2.0",
- "prettier": "^2.7.1",
- "supertest": "^6.2.4",
- "ts-jest": "^28.0.7",
- "ts-node": "^10.9.1",
- "tsc-alias": "^1.7.0",
- "tsconfig-paths": "^4.0.0",
- "typescript": "^4.7.4"
- }
-}
diff --git a/lib/default/src/app.ts b/lib/default/src/app.ts
deleted file mode 100644
index d5c6fc3..0000000
--- a/lib/default/src/app.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import 'reflect-metadata';
-import compression from 'compression';
-import cookieParser from 'cookie-parser';
-import cors from 'cors';
-import express from 'express';
-import helmet from 'helmet';
-import hpp from 'hpp';
-import morgan from 'morgan';
-import swaggerJSDoc from 'swagger-jsdoc';
-import swaggerUi from 'swagger-ui-express';
-import { NODE_ENV, PORT, LOG_FORMAT, ORIGIN, CREDENTIALS } from '@config';
-import { Routes } from '@interfaces/routes.interface';
-import { ErrorMiddleware } from '@middlewares/error.middleware';
-import { logger, stream } from '@utils/logger';
-
-export class App {
- public app: express.Application;
- public env: string;
- public port: string | number;
-
- constructor(routes: Routes[]) {
- this.app = express();
- this.env = NODE_ENV || 'development';
- this.port = PORT || 3000;
-
- this.initializeMiddlewares();
- this.initializeRoutes(routes);
- this.initializeSwagger();
- this.initializeErrorHandling();
- }
-
- public listen() {
- this.app.listen(this.port, () => {
- logger.info(`=================================`);
- logger.info(`======= ENV: ${this.env} =======`);
- logger.info(`🚀 App listening on the port ${this.port}`);
- logger.info(`=================================`);
- });
- }
-
- public getServer() {
- return this.app;
- }
-
- private initializeMiddlewares() {
- this.app.use(morgan(LOG_FORMAT, { stream }));
- this.app.use(cors({ origin: ORIGIN, credentials: CREDENTIALS }));
- this.app.use(hpp());
- this.app.use(helmet());
- this.app.use(compression());
- this.app.use(express.json());
- this.app.use(express.urlencoded({ extended: true }));
- this.app.use(cookieParser());
- }
-
- private initializeRoutes(routes: Routes[]) {
- routes.forEach(route => {
- this.app.use('/', route.router);
- });
- }
-
- private initializeSwagger() {
- const options = {
- swaggerDefinition: {
- info: {
- title: 'REST API',
- version: '1.0.0',
- description: 'Example docs',
- },
- },
- apis: ['swagger.yaml'],
- };
-
- const specs = swaggerJSDoc(options);
- this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
- }
-
- private initializeErrorHandling() {
- this.app.use(ErrorMiddleware);
- }
-}
diff --git a/lib/default/src/config/index.ts b/lib/default/src/config/index.ts
deleted file mode 100644
index ef17df5..0000000
--- a/lib/default/src/config/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { config } from 'dotenv';
-config({ path: `.env.${process.env.NODE_ENV || 'development'}.local` });
-
-export const CREDENTIALS = process.env.CREDENTIALS === 'true';
-export const { NODE_ENV, PORT, SECRET_KEY, LOG_FORMAT, LOG_DIR, ORIGIN } = process.env;
diff --git a/lib/default/src/controllers/auth.controller.ts b/lib/default/src/controllers/auth.controller.ts
deleted file mode 100644
index ab035db..0000000
--- a/lib/default/src/controllers/auth.controller.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { NextFunction, Request, Response } from 'express';
-import { Container } from 'typedi';
-import { RequestWithUser } from '@interfaces/auth.interface';
-import { User } from '@interfaces/users.interface';
-import { AuthService } from '@services/auth.service';
-
-export class AuthController {
- public auth = Container.get(AuthService);
-
- public signUp = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const userData: User = req.body;
- const signUpUserData: User = await this.auth.signup(userData);
-
- res.status(201).json({ data: signUpUserData, message: 'signup' });
- } catch (error) {
- next(error);
- }
- };
-
- public logIn = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const userData: User = req.body;
- const { cookie, findUser } = await this.auth.login(userData);
-
- res.setHeader('Set-Cookie', [cookie]);
- res.status(200).json({ data: findUser, message: 'login' });
- } catch (error) {
- next(error);
- }
- };
-
- public logOut = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => {
- try {
- const userData: User = req.user;
- const logOutUserData: User = await this.auth.logout(userData);
-
- res.setHeader('Set-Cookie', ['Authorization=; Max-age=0']);
- res.status(200).json({ data: logOutUserData, message: 'logout' });
- } catch (error) {
- next(error);
- }
- };
-}
diff --git a/lib/default/src/controllers/users.controller.ts b/lib/default/src/controllers/users.controller.ts
deleted file mode 100644
index 60f46bf..0000000
--- a/lib/default/src/controllers/users.controller.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { NextFunction, Request, Response } from 'express';
-import { Container } from 'typedi';
-import { User } from '@interfaces/users.interface';
-import { UserService } from '@services/users.service';
-
-export class UserController {
- public user = Container.get(UserService);
-
- public getUsers = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const findAllUsersData: User[] = await this.user.findAllUser();
-
- res.status(200).json({ data: findAllUsersData, message: 'findAll' });
- } catch (error) {
- next(error);
- }
- };
-
- public getUserById = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const userId = Number(req.params.id);
- const findOneUserData: User = await this.user.findUserById(userId);
-
- res.status(200).json({ data: findOneUserData, message: 'findOne' });
- } catch (error) {
- next(error);
- }
- };
-
- public createUser = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const userData: User = req.body;
- const createUserData: User = await this.user.createUser(userData);
-
- res.status(201).json({ data: createUserData, message: 'created' });
- } catch (error) {
- next(error);
- }
- };
-
- public updateUser = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const userId = Number(req.params.id);
- const userData: User = req.body;
- const updateUserData: User[] = await this.user.updateUser(userId, userData);
-
- res.status(200).json({ data: updateUserData, message: 'updated' });
- } catch (error) {
- next(error);
- }
- };
-
- public deleteUser = async (req: Request, res: Response, next: NextFunction): Promise => {
- try {
- const userId = Number(req.params.id);
- const deleteUserData: User[] = await this.user.deleteUser(userId);
-
- res.status(200).json({ data: deleteUserData, message: 'deleted' });
- } catch (error) {
- next(error);
- }
- };
-}
diff --git a/lib/default/src/dtos/users.dto.ts b/lib/default/src/dtos/users.dto.ts
deleted file mode 100644
index 5bf903b..0000000
--- a/lib/default/src/dtos/users.dto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { IsEmail, IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
-
-export class CreateUserDto {
- @IsEmail()
- public email: string;
-
- @IsString()
- @IsNotEmpty()
- @MinLength(9)
- @MaxLength(32)
- public password: string;
-}
-
-export class UpdateUserDto {
- @IsString()
- @IsNotEmpty()
- @MinLength(9)
- @MaxLength(32)
- public password: string;
-}
diff --git a/lib/default/src/exceptions/HttpException.ts b/lib/default/src/exceptions/HttpException.ts
deleted file mode 100644
index f0ae6aa..0000000
--- a/lib/default/src/exceptions/HttpException.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export class HttpException extends Error {
- public status: number;
- public message: string;
-
- constructor(status: number, message: string) {
- super(message);
- this.status = status;
- this.message = message;
- }
-}
diff --git a/lib/default/src/http/auth.http b/lib/default/src/http/auth.http
deleted file mode 100644
index 2198991..0000000
--- a/lib/default/src/http/auth.http
+++ /dev/null
@@ -1,27 +0,0 @@
-# baseURL
-@baseURL = http://localhost:3000
-
-###
-# User Signup
-POST {{ baseURL }}/signup
-Content-Type: application/json
-
-{
- "email": "example@email.com",
- "password": "password"
-}
-
-###
-# User Login
-POST {{ baseURL }}/login
-Content-Type: application/json
-
-{
- "email": "example@email.com",
- "password": "password"
-}
-
-###
-# User Logout
-POST {{ baseURL }}/logout
-Content-Type: application/json
diff --git a/lib/default/src/http/users.http b/lib/default/src/http/users.http
deleted file mode 100644
index 13209f2..0000000
--- a/lib/default/src/http/users.http
+++ /dev/null
@@ -1,34 +0,0 @@
-# baseURL
-@baseURL = http://localhost:3000
-
-###
-# Find All Users
-GET {{ baseURL }}/users
-
-###
-# Find User By Id
-GET {{ baseURL }}/users/1
-
-###
-# Create User
-POST {{ baseURL }}/users
-Content-Type: application/json
-
-{
- "email": "example@email.com",
- "password": "password"
-}
-
-###
-# Modify User By Id
-PUT {{ baseURL }}/users/1
-Content-Type: application/json
-
-{
- "email": "example@email.com",
- "password": "password"
-}
-
-###
-# Delete User By Id
-DELETE {{ baseURL }}/users/1
diff --git a/lib/default/src/interfaces/users.interface.ts b/lib/default/src/interfaces/users.interface.ts
deleted file mode 100644
index 07bbc28..0000000
--- a/lib/default/src/interfaces/users.interface.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface User {
- id?: number;
- email?: string;
- password: string;
-}
diff --git a/lib/default/src/middlewares/auth.middleware.ts b/lib/default/src/middlewares/auth.middleware.ts
deleted file mode 100644
index c311410..0000000
--- a/lib/default/src/middlewares/auth.middleware.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { NextFunction, Response } from 'express';
-import { verify } from 'jsonwebtoken';
-import { SECRET_KEY } from '@config';
-import { HttpException } from '@exceptions/httpException';
-import { DataStoredInToken, RequestWithUser } from '@interfaces/auth.interface';
-import { UserModel } from '@models/users.model';
-
-const getAuthorization = req => {
- const coockie = req.cookies['Authorization'];
- if (coockie) return coockie;
-
- const header = req.header('Authorization');
- if (header) return header.split('Bearer ')[1];
-
- return null;
-};
-
-export const AuthMiddleware = async (req: RequestWithUser, res: Response, next: NextFunction) => {
- try {
- const Authorization = getAuthorization(req);
-
- if (Authorization) {
- const { id } = (await verify(Authorization, SECRET_KEY)) as DataStoredInToken;
- const findUser = UserModel.find(user => user.id === id);
-
- if (findUser) {
- req.user = findUser;
- next();
- } else {
- next(new HttpException(401, 'Wrong authentication token'));
- }
- } else {
- next(new HttpException(404, 'Authentication token missing'));
- }
- } catch (error) {
- next(new HttpException(401, 'Wrong authentication token'));
- }
-};
diff --git a/lib/default/src/middlewares/error.middleware.ts b/lib/default/src/middlewares/error.middleware.ts
deleted file mode 100644
index 1dbeb17..0000000
--- a/lib/default/src/middlewares/error.middleware.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { NextFunction, Request, Response } from 'express';
-import { HttpException } from '@exceptions/httpException';
-import { logger } from '@utils/logger';
-
-export const ErrorMiddleware = (error: HttpException, req: Request, res: Response, next: NextFunction) => {
- try {
- const status: number = error.status || 500;
- const message: string = error.message || 'Something went wrong';
-
- logger.error(`[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`);
- res.status(status).json({ message });
- } catch (error) {
- next(error);
- }
-};
diff --git a/lib/default/src/middlewares/validation.middleware.ts b/lib/default/src/middlewares/validation.middleware.ts
deleted file mode 100644
index b351e21..0000000
--- a/lib/default/src/middlewares/validation.middleware.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { plainToInstance } from 'class-transformer';
-import { validateOrReject, ValidationError } from 'class-validator';
-import { NextFunction, Request, Response } from 'express';
-import { HttpException } from '@exceptions/httpException';
-
-/**
- * @name ValidationMiddleware
- * @description Allows use of decorator and non-decorator based validation
- * @param type dto
- * @param skipMissingProperties When skipping missing properties
- * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined
- * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present
- */
-export const ValidationMiddleware = (type: any, skipMissingProperties = false, whitelist = true, forbidNonWhitelisted = true) => {
- return (req: Request, res: Response, next: NextFunction) => {
- const dto = plainToInstance(type, req.body);
- validateOrReject(dto, { skipMissingProperties, whitelist, forbidNonWhitelisted })
- .then(() => {
- req.body = dto;
- next();
- })
- .catch((errors: ValidationError[]) => {
- const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', ');
- next(new HttpException(400, message));
- });
- };
-};
diff --git a/lib/default/src/models/users.model.ts b/lib/default/src/models/users.model.ts
deleted file mode 100644
index 7f22a20..0000000
--- a/lib/default/src/models/users.model.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { User } from '@interfaces/users.interface';
-
-// password: password123456789
-export const UserModel: User[] = [
- { id: 1, email: 'example1@email.com', password: '$2b$10$2YC2ht8x06Yto5VAr08kben8.oxjTPrMn0yJhv8xxSVVltH3bOs4u' },
- { id: 2, email: 'example2@email.com', password: '$2b$10$2YC2ht8x06Yto5VAr08kben8.oxjTPrMn0yJhv8xxSVVltH3bOs4u' },
- { id: 3, email: 'example3@email.com', password: '$2b$10$2YC2ht8x06Yto5VAr08kben8.oxjTPrMn0yJhv8xxSVVltH3bOs4u' },
- { id: 4, email: 'example4@email.com', password: '$2b$10$2YC2ht8x06Yto5VAr08kben8.oxjTPrMn0yJhv8xxSVVltH3bOs4u' },
-];
diff --git a/lib/default/src/routes/auth.route.ts b/lib/default/src/routes/auth.route.ts
deleted file mode 100644
index 1a18f73..0000000
--- a/lib/default/src/routes/auth.route.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Router } from 'express';
-import { AuthController } from '@controllers/auth.controller';
-import { CreateUserDto } from '@dtos/users.dto';
-import { Routes } from '@interfaces/routes.interface';
-import { AuthMiddleware } from '@middlewares/auth.middleware';
-import { ValidationMiddleware } from '@middlewares/validation.middleware';
-
-export class AuthRoute implements Routes {
- public router = Router();
- public auth = new AuthController();
-
- constructor() {
- this.initializeRoutes();
- }
-
- private initializeRoutes() {
- this.router.post('/signup', ValidationMiddleware(CreateUserDto), this.auth.signUp);
- this.router.post('/login', ValidationMiddleware(CreateUserDto), this.auth.logIn);
- this.router.post('/logout', AuthMiddleware, this.auth.logOut);
- }
-}
diff --git a/lib/default/src/routes/users.route.ts b/lib/default/src/routes/users.route.ts
deleted file mode 100644
index ed15c24..0000000
--- a/lib/default/src/routes/users.route.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Router } from 'express';
-import { UserController } from '@controllers/users.controller';
-import { CreateUserDto, UpdateUserDto } from '@dtos/users.dto';
-import { Routes } from '@interfaces/routes.interface';
-import { ValidationMiddleware } from '@middlewares/validation.middleware';
-
-export class UserRoute implements Routes {
- public path = '/users';
- public router = Router();
- public user = new UserController();
-
- constructor() {
- this.initializeRoutes();
- }
-
- private initializeRoutes() {
- this.router.get(`${this.path}`, this.user.getUsers);
- this.router.get(`${this.path}/:id(\\d+)`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
- this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(UpdateUserDto), this.user.updateUser);
- this.router.delete(`${this.path}/:id(\\d+)`, this.user.deleteUser);
- }
-}
diff --git a/lib/default/src/server.ts b/lib/default/src/server.ts
deleted file mode 100644
index 2362805..0000000
--- a/lib/default/src/server.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { App } from '@/app';
-import { AuthRoute } from '@routes/auth.route';
-import { UserRoute } from '@routes/users.route';
-import { ValidateEnv } from '@utils/validateEnv';
-
-ValidateEnv();
-
-const app = new App([new UserRoute(), new AuthRoute()]);
-
-app.listen();
diff --git a/lib/default/src/services/auth.service.ts b/lib/default/src/services/auth.service.ts
deleted file mode 100644
index 9710d5e..0000000
--- a/lib/default/src/services/auth.service.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { hash, compare } from 'bcrypt';
-import { sign } from 'jsonwebtoken';
-import { Service } from 'typedi';
-import { SECRET_KEY } from '@config';
-import { HttpException } from '@exceptions/httpException';
-import { DataStoredInToken, TokenData } from '@interfaces/auth.interface';
-import { User } from '@interfaces/users.interface';
-import { UserModel } from '@models/users.model';
-
-const createToken = (user: User): TokenData => {
- const dataStoredInToken: DataStoredInToken = { id: user.id };
- const expiresIn: number = 60 * 60;
-
- return { expiresIn, token: sign(dataStoredInToken, SECRET_KEY, { expiresIn }) };
-};
-
-const createCookie = (tokenData: TokenData): string => {
- return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn};`;
-};
-
-@Service()
-export class AuthService {
- public async signup(userData: User): Promise {
- const findUser: User = UserModel.find(user => user.email === userData.email);
- if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`);
-
- const hashedPassword = await hash(userData.password, 10);
- const createUserData: User = { ...userData, id: UserModel.length + 1, password: hashedPassword };
-
- return createUserData;
- }
-
- public async login(userData: User): Promise<{ cookie: string; findUser: User }> {
- const findUser: User = UserModel.find(user => user.email === userData.email);
- if (!findUser) throw new HttpException(409, `This email ${userData.email} was not found`);
-
- const isPasswordMatching: boolean = await compare(userData.password, findUser.password);
- if (!isPasswordMatching) throw new HttpException(409, "You're password not matching");
-
- const tokenData = createToken(findUser);
- const cookie = createCookie(tokenData);
-
- return { cookie, findUser };
- }
-
- public async logout(userData: User): Promise {
- const findUser: User = UserModel.find(user => user.email === userData.email && user.password === userData.password);
- if (!findUser) throw new HttpException(409, "User doesn't exist");
-
- return findUser;
- }
-}
diff --git a/lib/default/src/services/users.service.ts b/lib/default/src/services/users.service.ts
deleted file mode 100644
index 8d253b8..0000000
--- a/lib/default/src/services/users.service.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { hash } from 'bcrypt';
-import { Service } from 'typedi';
-import { HttpException } from '@exceptions/httpException';
-import { User } from '@interfaces/users.interface';
-import { UserModel } from '@models/users.model';
-
-@Service()
-export class UserService {
- public async findAllUser(): Promise {
- const users: User[] = UserModel;
- return users;
- }
-
- public async findUserById(userId: number): Promise {
- const findUser: User = UserModel.find(user => user.id === userId);
- if (!findUser) throw new HttpException(409, "User doesn't exist");
-
- return findUser;
- }
-
- public async createUser(userData: User): Promise {
- const findUser: User = UserModel.find(user => user.email === userData.email);
- if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`);
-
- const hashedPassword = await hash(userData.password, 10);
- const createUserData: User = { ...userData, id: UserModel.length + 1, password: hashedPassword };
-
- return createUserData;
- }
-
- public async updateUser(userId: number, userData: User): Promise {
- const findUser: User = UserModel.find(user => user.id === userId);
- if (!findUser) throw new HttpException(409, "User doesn't exist");
-
- const hashedPassword = await hash(userData.password, 10);
- const updateUserData: User[] = UserModel.map((user: User) => {
- if (user.id === findUser.id) user = { ...userData, id: userId, password: hashedPassword };
- return user;
- });
-
- return updateUserData;
- }
-
- public async deleteUser(userId: number): Promise {
- const findUser: User = UserModel.find(user => user.id === userId);
- if (!findUser) throw new HttpException(409, "User doesn't exist");
-
- const deleteUserData: User[] = UserModel.filter(user => user.id !== findUser.id);
- return deleteUserData;
- }
-}
diff --git a/lib/default/src/test/auth.test.ts b/lib/default/src/test/auth.test.ts
deleted file mode 100644
index 11ab020..0000000
--- a/lib/default/src/test/auth.test.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import request from 'supertest';
-import { App } from '@/app';
-import { User } from '@interfaces/users.interface';
-import { AuthRoute } from '@routes/auth.route';
-
-afterAll(async () => {
- await new Promise(resolve => setTimeout(() => resolve(), 500));
-});
-
-describe('TEST Authorization API', () => {
- const route = new AuthRoute();
- const app = new App([route]);
-
- describe('[POST] /signup', () => {
- it('response should have the Create userData', () => {
- const userData: User = {
- email: 'example@email.com',
- password: 'password123456789',
- };
-
- return request(app.getServer()).post('/signup').send(userData).expect(201);
- });
- });
-
- describe('[POST] /login', () => {
- it('response should have the Set-Cookie header with the Authorization token', () => {
- const userData: User = {
- email: 'example1@email.com',
- password: 'password123456789',
- };
-
- return request(app.getServer())
- .post('/login')
- .send(userData)
- .expect('Set-Cookie', /^Authorization=.+/)
- .expect(200);
- });
- });
-
- // error: StatusCode : 404, Message : Authentication token missing
- // describe('[POST] /logout', () => {
- // it('logout Set-Cookie Authorization=; Max-age=0', () => {
- // const route = new AuthRoute()
- // const app = new App([route]);
-
- // return request(app.getServer())
- // .post('/logout')
- // .expect('Set-Cookie', /^Authorization=\;/)
- // .expect(200);
- // });
- // });
-});
diff --git a/lib/default/src/test/users.test.ts b/lib/default/src/test/users.test.ts
deleted file mode 100644
index 157dd83..0000000
--- a/lib/default/src/test/users.test.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import request from 'supertest';
-import { App } from '@/app';
-import { User } from '@interfaces/users.interface';
-import { UserModel } from '@models/users.model';
-import { UserRoute } from '@routes/users.route';
-
-afterAll(async () => {
- await new Promise(resolve => setTimeout(() => resolve(), 500));
-});
-
-describe('TEST Users API', () => {
- const route = new UserRoute();
- const app = new App([route]);
-
- describe('[GET] /users', () => {
- it('response statusCode 200 /findAll', () => {
- const findUser: User[] = UserModel;
-
- return request(app.getServer()).get(`${route.path}`).expect(200, { data: findUser, message: 'findAll' });
- });
- });
-
- describe('[GET] /users/:id', () => {
- it('response statusCode 200 /findOne', () => {
- const userId = 1;
- const findUser: User = UserModel.find(user => user.id === userId);
-
- return request(app.getServer()).get(`${route.path}/${userId}`).expect(200, { data: findUser, message: 'findOne' });
- });
- });
-
- describe('[POST] /users', () => {
- it('response statusCode 201 /created', async () => {
- const userData: User = {
- email: 'example@email.com',
- password: 'password123456789',
- };
-
- return request(app.getServer()).post(`${route.path}`).send(userData).expect(201);
- });
- });
-
- describe('[PUT] /users/:id', () => {
- it('response statusCode 200 /updated', async () => {
- const userId = 1;
- const userData: User = {
- password: 'password123456789',
- };
-
- return request(app.getServer()).put(`${route.path}/${userId}`).send(userData).expect(200);
- });
- });
-
- describe('[DELETE] /users/:id', () => {
- it('response statusCode 200 /deleted', () => {
- const userId = 1;
- const deleteUser: User[] = UserModel.filter(user => user.id !== userId);
-
- return request(app.getServer()).delete(`${route.path}/${userId}`).expect(200, { data: deleteUser, message: 'deleted' });
- });
- });
-});
diff --git a/lib/default/src/utils/logger.ts b/lib/default/src/utils/logger.ts
deleted file mode 100644
index b8f43a9..0000000
--- a/lib/default/src/utils/logger.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { existsSync, mkdirSync } from 'fs';
-import { join } from 'path';
-import winston from 'winston';
-import winstonDaily from 'winston-daily-rotate-file';
-import { LOG_DIR } from '@config';
-
-// logs dir
-const logDir: string = join(__dirname, LOG_DIR);
-
-if (!existsSync(logDir)) {
- mkdirSync(logDir);
-}
-
-// Define log format
-const logFormat = winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`);
-
-/*
- * Log Level
- * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
- */
-const logger = winston.createLogger({
- format: winston.format.combine(
- winston.format.timestamp({
- format: 'YYYY-MM-DD HH:mm:ss',
- }),
- logFormat,
- ),
- transports: [
- // debug log setting
- new winstonDaily({
- level: 'debug',
- datePattern: 'YYYY-MM-DD',
- dirname: logDir + '/debug', // log file /logs/debug/*.log in save
- filename: `%DATE%.log`,
- maxFiles: 30, // 30 Days saved
- json: false,
- zippedArchive: true,
- }),
- // error log setting
- new winstonDaily({
- level: 'error',
- datePattern: 'YYYY-MM-DD',
- dirname: logDir + '/error', // log file /logs/error/*.log in save
- filename: `%DATE%.log`,
- maxFiles: 30, // 30 Days saved
- handleExceptions: true,
- json: false,
- zippedArchive: true,
- }),
- ],
-});
-
-logger.add(
- new winston.transports.Console({
- format: winston.format.combine(winston.format.splat(), winston.format.colorize()),
- }),
-);
-
-const stream = {
- write: (message: string) => {
- logger.info(message.substring(0, message.lastIndexOf('\n')));
- },
-};
-
-export { logger, stream };
diff --git a/lib/default/src/utils/validateEnv.ts b/lib/default/src/utils/validateEnv.ts
deleted file mode 100644
index a6f1904..0000000
--- a/lib/default/src/utils/validateEnv.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { cleanEnv, port, str } from 'envalid';
-
-export const ValidateEnv = () => {
- cleanEnv(process.env, {
- NODE_ENV: str(),
- PORT: port(),
- });
-};
diff --git a/lib/default/swagger.yaml b/lib/default/swagger.yaml
deleted file mode 100644
index eebcb31..0000000
--- a/lib/default/swagger.yaml
+++ /dev/null
@@ -1,123 +0,0 @@
-tags:
-- name: users
- description: users API
-
-paths:
-# [GET] users
- /users:
- get:
- tags:
- - users
- summary: Find All Users
- responses:
- 200:
- description: 'OK'
- 500:
- description: 'Server Error'
-
-# [POST] users
- post:
- tags:
- - users
- summary: Add User
- parameters:
- - name: body
- in: body
- description: user Data
- required: true
- schema:
- $ref: '#/definitions/users'
- responses:
- 201:
- description: 'Created'
- 400:
- description: 'Bad Request'
- 409:
- description: 'Conflict'
- 500:
- description: 'Server Error'
-
-# [GET] users/id
- /users/{id}:
- get:
- tags:
- - users
- summary: Find User By Id
- parameters:
- - name: id
- in: path
- description: User Id
- required: true
- type: integer
- responses:
- 200:
- description: 'OK'
- 409:
- description: 'Conflict'
- 500:
- description: 'Server Error'
-
-# [PUT] users/id
- put:
- tags:
- - users
- summary: Update User By Id
- parameters:
- - name: id
- in: path
- description: user Id
- required: true
- type: integer
- - name: body
- in: body
- description: user Data
- required: true
- schema:
- $ref: '#/definitions/users'
- responses:
- 200:
- description: 'OK'
- 400:
- description: 'Bad Request'
- 409:
- description: 'Conflict'
- 500:
- description: 'Server Error'
-
-# [DELETE] users/id
- delete:
- tags:
- - users
- summary: Delete User By Id
- parameters:
- - name: id
- in: path
- description: user Id
- required: true
- type: integer
- responses:
- 200:
- description: 'OK'
- 409:
- description: 'Conflict'
- 500:
- description: 'Server Error'
-
-# definitions
-definitions:
- users:
- type: object
- required:
- - email
- - password
- properties:
- email:
- type: string
- description: user Email
- password:
- type: string
- description: user Password
-
-schemes:
- - https
- - http
diff --git a/lib/mikro-orm/src/routes/auth.route.ts b/lib/mikro-orm/src/routes/auth.route.ts
index 118af5e..a32969f 100644
--- a/lib/mikro-orm/src/routes/auth.route.ts
+++ b/lib/mikro-orm/src/routes/auth.route.ts
@@ -15,8 +15,8 @@ export class AuthRoute implements Routes {
}
private initializeRoutes() {
- this.router.post(`${this.path}signup`, ValidationMiddleware(CreateUserDto, 'body'), this.auth.signUp);
- this.router.post(`${this.path}login`, ValidationMiddleware(CreateUserDto, 'body'), this.auth.logIn);
+ this.router.post(`${this.path}signup`, ValidationMiddleware(CreateUserDto), this.auth.signUp);
+ this.router.post(`${this.path}login`, ValidationMiddleware(CreateUserDto), this.auth.logIn);
this.router.post(`${this.path}logout`, AuthMiddleware, this.auth.logOut);
}
}
diff --git a/lib/mikro-orm/src/routes/users.route.ts b/lib/mikro-orm/src/routes/users.route.ts
index c56a39b..957f192 100644
--- a/lib/mikro-orm/src/routes/users.route.ts
+++ b/lib/mikro-orm/src/routes/users.route.ts
@@ -16,8 +16,8 @@ export class UserRoute implements Routes {
private initializeRoutes() {
this.router.get(`${this.path}`, this.user.getUsers);
this.router.get(`${this.path}/:id`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto, 'body'), this.user.createUser);
- this.router.put(`${this.path}/:id`, ValidationMiddleware(CreateUserDto, 'body', true), this.user.updateUser);
+ this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
+ this.router.put(`${this.path}/:id`, ValidationMiddleware(CreateUserDto, true), this.user.updateUser);
this.router.delete(`${this.path}/:id`, this.user.deleteUser);
}
}
diff --git a/lib/mongoose/src/routes/auth.route.ts b/lib/mongoose/src/routes/auth.route.ts
index 118af5e..a32969f 100644
--- a/lib/mongoose/src/routes/auth.route.ts
+++ b/lib/mongoose/src/routes/auth.route.ts
@@ -15,8 +15,8 @@ export class AuthRoute implements Routes {
}
private initializeRoutes() {
- this.router.post(`${this.path}signup`, ValidationMiddleware(CreateUserDto, 'body'), this.auth.signUp);
- this.router.post(`${this.path}login`, ValidationMiddleware(CreateUserDto, 'body'), this.auth.logIn);
+ this.router.post(`${this.path}signup`, ValidationMiddleware(CreateUserDto), this.auth.signUp);
+ this.router.post(`${this.path}login`, ValidationMiddleware(CreateUserDto), this.auth.logIn);
this.router.post(`${this.path}logout`, AuthMiddleware, this.auth.logOut);
}
}
diff --git a/lib/mongoose/src/routes/users.route.ts b/lib/mongoose/src/routes/users.route.ts
index c56a39b..957f192 100644
--- a/lib/mongoose/src/routes/users.route.ts
+++ b/lib/mongoose/src/routes/users.route.ts
@@ -16,8 +16,8 @@ export class UserRoute implements Routes {
private initializeRoutes() {
this.router.get(`${this.path}`, this.user.getUsers);
this.router.get(`${this.path}/:id`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto, 'body'), this.user.createUser);
- this.router.put(`${this.path}/:id`, ValidationMiddleware(CreateUserDto, 'body', true), this.user.updateUser);
+ this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
+ this.router.put(`${this.path}/:id`, ValidationMiddleware(CreateUserDto, true), this.user.updateUser);
this.router.delete(`${this.path}/:id`, this.user.deleteUser);
}
}
diff --git a/lib/prisma/src/routes/auth.route.ts b/lib/prisma/src/routes/auth.route.ts
index 118af5e..a32969f 100644
--- a/lib/prisma/src/routes/auth.route.ts
+++ b/lib/prisma/src/routes/auth.route.ts
@@ -15,8 +15,8 @@ export class AuthRoute implements Routes {
}
private initializeRoutes() {
- this.router.post(`${this.path}signup`, ValidationMiddleware(CreateUserDto, 'body'), this.auth.signUp);
- this.router.post(`${this.path}login`, ValidationMiddleware(CreateUserDto, 'body'), this.auth.logIn);
+ this.router.post(`${this.path}signup`, ValidationMiddleware(CreateUserDto), this.auth.signUp);
+ this.router.post(`${this.path}login`, ValidationMiddleware(CreateUserDto), this.auth.logIn);
this.router.post(`${this.path}logout`, AuthMiddleware, this.auth.logOut);
}
}
diff --git a/lib/prisma/src/routes/users.route.ts b/lib/prisma/src/routes/users.route.ts
index 8f097f5..b750b9f 100644
--- a/lib/prisma/src/routes/users.route.ts
+++ b/lib/prisma/src/routes/users.route.ts
@@ -16,8 +16,8 @@ export class UserRoute implements Routes {
private initializeRoutes() {
this.router.get(`${this.path}`, this.user.getUsers);
this.router.get(`${this.path}/:id(\\d+)`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto, 'body'), this.user.createUser);
- this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(CreateUserDto, 'body', true), this.user.updateUser);
+ this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
+ this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(CreateUserDto, true), this.user.updateUser);
this.router.delete(`${this.path}/:id(\\d+)`, this.user.deleteUser);
}
}
diff --git a/lib/routing-controllers/package.json b/lib/routing-controllers/package.json
index cbdb85d..b066800 100644
--- a/lib/routing-controllers/package.json
+++ b/lib/routing-controllers/package.json
@@ -10,7 +10,7 @@
"build": "swc src -d dist --source-maps --copy-files",
"build:tsc": "tsc && tsc-alias",
"test": "jest --forceExit --detectOpenHandles",
- "lint": "eslint --ignore-path .gitignore --ext .ts src/",
+ "lint": "eslint --ignore-path .gitignore --ext .ts src",
"lint:fix": "npm run lint -- --fix",
"deploy:prod": "npm run build && pm2 start ecosystem.config.js --only prod",
"deploy:dev": "pm2 start ecosystem.config.js --only dev"
diff --git a/lib/routing-controllers/src/controllers/auth.controller.ts b/lib/routing-controllers/src/controllers/auth.controller.ts
index bd4133e..7bac911 100644
--- a/lib/routing-controllers/src/controllers/auth.controller.ts
+++ b/lib/routing-controllers/src/controllers/auth.controller.ts
@@ -13,7 +13,7 @@ export class AuthController {
public auth = Container.get(AuthService);
@Post('/signup')
- @UseBefore(ValidationMiddleware(CreateUserDto, 'body'))
+ @UseBefore(ValidationMiddleware(CreateUserDto))
@HttpCode(201)
async signUp(@Body() userData: User) {
const signUpUserData: User = await this.auth.signup(userData);
@@ -21,7 +21,7 @@ export class AuthController {
}
@Post('/login')
- @UseBefore(ValidationMiddleware(CreateUserDto, 'body'))
+ @UseBefore(ValidationMiddleware(CreateUserDto))
async logIn(@Res() res: Response, @Body() userData: User) {
const { cookie, findUser } = await this.auth.login(userData);
diff --git a/lib/routing-controllers/src/controllers/users.controller.ts b/lib/routing-controllers/src/controllers/users.controller.ts
index 4a9ce7a..8a7084f 100644
--- a/lib/routing-controllers/src/controllers/users.controller.ts
+++ b/lib/routing-controllers/src/controllers/users.controller.ts
@@ -27,7 +27,7 @@ export class UserController {
@Post('/users')
@HttpCode(201)
- @UseBefore(ValidationMiddleware(CreateUserDto, 'body'))
+ @UseBefore(ValidationMiddleware(CreateUserDto))
@OpenAPI({ summary: 'Create a new user' })
async createUser(@Body() userData: User) {
const createUserData: User = await this.user.createUser(userData);
@@ -35,7 +35,7 @@ export class UserController {
}
@Put('/users/:id')
- @UseBefore(ValidationMiddleware(CreateUserDto, 'body', true))
+ @UseBefore(ValidationMiddleware(CreateUserDto, true))
@OpenAPI({ summary: 'Update a user' })
async updateUser(@Param('id') userId: number, @Body() userData: User) {
const updateUserData: User[] = await this.user.updateUser(userId, userData);
diff --git a/lib/sequelize/src/routes/auth.route.ts b/lib/sequelize/src/routes/auth.route.ts
index 1025094..1a18f73 100644
--- a/lib/sequelize/src/routes/auth.route.ts
+++ b/lib/sequelize/src/routes/auth.route.ts
@@ -14,8 +14,8 @@ export class AuthRoute implements Routes {
}
private initializeRoutes() {
- this.router.post('/signup', ValidationMiddleware(CreateUserDto, 'body'), this.auth.signUp);
- this.router.post('/login', ValidationMiddleware(CreateUserDto, 'body'), this.auth.logIn);
+ this.router.post('/signup', ValidationMiddleware(CreateUserDto), this.auth.signUp);
+ this.router.post('/login', ValidationMiddleware(CreateUserDto), this.auth.logIn);
this.router.post('/logout', AuthMiddleware, this.auth.logOut);
}
}
diff --git a/lib/sequelize/src/routes/users.route.ts b/lib/sequelize/src/routes/users.route.ts
index 8f097f5..b750b9f 100644
--- a/lib/sequelize/src/routes/users.route.ts
+++ b/lib/sequelize/src/routes/users.route.ts
@@ -16,8 +16,8 @@ export class UserRoute implements Routes {
private initializeRoutes() {
this.router.get(`${this.path}`, this.user.getUsers);
this.router.get(`${this.path}/:id(\\d+)`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto, 'body'), this.user.createUser);
- this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(CreateUserDto, 'body', true), this.user.updateUser);
+ this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
+ this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(CreateUserDto, true), this.user.updateUser);
this.router.delete(`${this.path}/:id(\\d+)`, this.user.deleteUser);
}
}
diff --git a/lib/starter.js b/lib/starter.js
deleted file mode 100644
index 16bec17..0000000
--- a/lib/starter.js
+++ /dev/null
@@ -1,262 +0,0 @@
-/*****************************************************************
- * Create TypeScript Express Starter
- * 2019.12.18 ~ 🎮
- * Made By AGUMON 🦖
- * https://github.com/ljlm0402/typescript-express-starter
- *****************************************************************/
-
-const chalk = require("chalk");
-const { exec } = require("child_process");
-const editJsonFile = require("edit-json-file");
-const { createWriteStream, readdir } = require("fs");
-const { writeFile } = require("gitignore");
-const inquirer = require("inquirer");
-const { ncp } = require("ncp");
-const ora = require("ora");
-const path = require("path");
-const { promisify } = require("util");
-
-const readDir = promisify(readdir);
-const asyncExec = promisify(exec);
-const writeGitignore = promisify(writeFile);
-
-/**
- * @method createProject
- * @description Create a project
- */
-const createProject = async (projectName) => {
- let spinner;
-
- try {
- const template = await chooseTemplates();
- // const isUpdated = await dependenciesUpdates();
- // const isDeduped = await dependenciesDeduped();
-
- console.log("[ 1 / 3 ] 🔍 copying project...");
- console.log("[ 2 / 3 ] 🚚 fetching node_modules...");
-
- await copyProjectFiles(projectName, template);
- await updatePackageJson(projectName);
-
- console.log("[ 3 / 3 ] 🔗 linking node_modules...");
- console.log(
- "\u001b[2m──────────────────────────────────────────\u001b[22m"
- );
-
- spinner = ora();
- spinner.start();
-
- await installNodeModules(projectName, spinner);
- // isUpdated && (await updateNodeModules(projectName, spinner));
- // isDeduped && (await dedupeNodeModules(projectName, spinner));
- await postInstallScripts(projectName, template, spinner);
-
- await createGitignore(projectName, spinner);
- await initGit(projectName);
-
- await succeedConsole(template, spinner);
- } catch (error) {
- await failConsole(error, spinner);
- }
-};
-
-/**
- * @method getDirectories
- * @description Get the templates directory.
- */
-const getTemplateDir = async () => {
- const contents = await readDir(__dirname, { withFileTypes: true });
- const directories = contents
- .filter((p) => p.isDirectory())
- .map((p) => p.name);
-
- return directories;
-};
-
-/**
- * @method chooseTemplates
- * @description Choose a template.
- */
-const chooseTemplates = async () => {
- const directories = await getTemplateDir();
- const { chooseTemplates } = await inquirer.prompt([
- {
- type: "list",
- name: "chooseTemplates",
- message: "Select a template",
- choices: [...directories, new inquirer.Separator()],
- },
- ]);
-
- return chooseTemplates;
-};
-
-/**
- * @method dependenciesUpdates
- * @description npm dependencies updated.
- */
-const dependenciesUpdates = async () => {
- const { isUpdated } = await inquirer.prompt([
- {
- type: "confirm",
- name: "isUpdated",
- message: "Update the package dependencies to their latest versions ?",
- },
- ]);
-
- if (isUpdated) {
- const { isUpdatedReconfirm } = await inquirer.prompt([
- {
- type: "confirm",
- name: "isUpdatedReconfirm",
- message:
- "The updated dependencies may contain breaking changes. Continue to update the dependencies anyway ?",
- },
- ]);
-
- return isUpdatedReconfirm;
- }
-
- return false;
-};
-
-/**
- * @method dependenciesDeduped
- * @description npm duplicate dependencies removed.
- */
-const dependenciesDeduped = async () => {
- const { isDeduped } = await inquirer.prompt([
- {
- type: "confirm",
- name: "isDeduped",
- message: "Deduplicate the package dependency tree (recommended) ?",
- },
- ]);
-
- return isDeduped;
-};
-
-/**
- * @method copyProjectFiles
- * @description Duplicate the template.
- */
-const copyProjectFiles = async (destination, directory) => {
- return new Promise((resolve, reject) => {
- const source = path.join(__dirname, `./${directory}`);
- const options = {
- clobber: true,
- stopOnErr: true,
- };
-
- ncp.limit = 16;
- ncp(source, destination, options, function (err) {
- if (err) reject(err);
- resolve();
- });
- });
-};
-
-/**
- * @method updatePackageJson
- * @description Edit package.json.
- */
-const updatePackageJson = async (destination) => {
- const file = editJsonFile(`${destination}/package.json`, { autosave: true });
-
- file.set("name", path.basename(destination));
-};
-
-/**
- * @method installNodeModules
- * @description Install node modules.
- */
-const installNodeModules = async (destination, spinner) => {
- spinner.text = "Install node_modules...\n";
- await asyncExec("npm install --legacy-peer-deps", { cwd: destination });
-};
-
-/**
- * @method updateNodeModules
- * @description Update node modules.
- */
-const updateNodeModules = async (destination, spinner) => {
- spinner.text = "Update node_modules...\n";
- await asyncExec("npm update --legacy-peer-deps", { cwd: destination });
-};
-
-/**
- * @method dedupeNodeModules
- * @description Dedupe node modules.
- */
-const dedupeNodeModules = async (destination, spinner) => {
- spinner.text = "Dedupe node_modules...\n";
- await asyncExec("npm dedupe --legacy-peer-deps", { cwd: destination });
-};
-
-/**
- * @method postInstallScripts
- * @description After installation, configure the settings according to the template.
- */
-const postInstallScripts = async (destination, template, spinner) => {
- switch (template) {
- case "prisma":
- {
- spinner.text = "Run prisma generate...";
- await asyncExec("npm run prisma:generate", { cwd: destination });
- }
- break;
- }
-};
-
-/**
- * @method createGitignore
- * @description Create a .gitignore.
- */
-const createGitignore = async (destination, spinner) => {
- spinner.text = "Create .gitignore...";
-
- const file = createWriteStream(path.join(destination, ".gitignore"), {
- flags: "a",
- });
-
- return writeGitignore({
- type: "Node",
- file: file,
- });
-};
-
-/**
- * @method initGit
- * @description Initialize git settings.
- */
-const initGit = async (destination) => {
- await asyncExec("git init", { cwd: destination });
-};
-
-/**
- * @method succeedConsole
- * @description When the project is successful, the console is displayed.
- */
-const succeedConsole = async (template, spinner) => {
- spinner.succeed(chalk`{green Complete setup project}`);
-
- const msg =
- {
- prisma:
- "⛰ Prisma installed. Check your .env settings and then run `npm run prisma:migrate`",
- knex: "⛰ Knex installed. Check your .env settings and then run `npm run migrate`",
- }[template] || "";
-
- msg && console.log(msg);
-};
-
-/**
- * @method failConsole
- * @description When the project is fail, the console is displayed.
- */
-const failConsole = async (error, spinner) => {
- spinner.fail(chalk`{red Please leave this error as an issue}`);
- console.error(error);
-};
-
-module.exports = createProject;
diff --git a/lib/typegoose/src/routes/auth.route.ts b/lib/typegoose/src/routes/auth.route.ts
index 1025094..1a18f73 100644
--- a/lib/typegoose/src/routes/auth.route.ts
+++ b/lib/typegoose/src/routes/auth.route.ts
@@ -14,8 +14,8 @@ export class AuthRoute implements Routes {
}
private initializeRoutes() {
- this.router.post('/signup', ValidationMiddleware(CreateUserDto, 'body'), this.auth.signUp);
- this.router.post('/login', ValidationMiddleware(CreateUserDto, 'body'), this.auth.logIn);
+ this.router.post('/signup', ValidationMiddleware(CreateUserDto), this.auth.signUp);
+ this.router.post('/login', ValidationMiddleware(CreateUserDto), this.auth.logIn);
this.router.post('/logout', AuthMiddleware, this.auth.logOut);
}
}
diff --git a/lib/typegoose/src/routes/users.route.ts b/lib/typegoose/src/routes/users.route.ts
index c56a39b..957f192 100644
--- a/lib/typegoose/src/routes/users.route.ts
+++ b/lib/typegoose/src/routes/users.route.ts
@@ -16,8 +16,8 @@ export class UserRoute implements Routes {
private initializeRoutes() {
this.router.get(`${this.path}`, this.user.getUsers);
this.router.get(`${this.path}/:id`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto, 'body'), this.user.createUser);
- this.router.put(`${this.path}/:id`, ValidationMiddleware(CreateUserDto, 'body', true), this.user.updateUser);
+ this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
+ this.router.put(`${this.path}/:id`, ValidationMiddleware(CreateUserDto, true), this.user.updateUser);
this.router.delete(`${this.path}/:id`, this.user.deleteUser);
}
}
diff --git a/lib/typeorm/src/routes/auth.route.ts b/lib/typeorm/src/routes/auth.route.ts
index 1025094..1a18f73 100644
--- a/lib/typeorm/src/routes/auth.route.ts
+++ b/lib/typeorm/src/routes/auth.route.ts
@@ -14,8 +14,8 @@ export class AuthRoute implements Routes {
}
private initializeRoutes() {
- this.router.post('/signup', ValidationMiddleware(CreateUserDto, 'body'), this.auth.signUp);
- this.router.post('/login', ValidationMiddleware(CreateUserDto, 'body'), this.auth.logIn);
+ this.router.post('/signup', ValidationMiddleware(CreateUserDto), this.auth.signUp);
+ this.router.post('/login', ValidationMiddleware(CreateUserDto), this.auth.logIn);
this.router.post('/logout', AuthMiddleware, this.auth.logOut);
}
}
diff --git a/lib/typeorm/src/routes/users.route.ts b/lib/typeorm/src/routes/users.route.ts
index 8f097f5..b750b9f 100644
--- a/lib/typeorm/src/routes/users.route.ts
+++ b/lib/typeorm/src/routes/users.route.ts
@@ -16,8 +16,8 @@ export class UserRoute implements Routes {
private initializeRoutes() {
this.router.get(`${this.path}`, this.user.getUsers);
this.router.get(`${this.path}/:id(\\d+)`, this.user.getUserById);
- this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto, 'body'), this.user.createUser);
- this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(CreateUserDto, 'body', true), this.user.updateUser);
+ this.router.post(`${this.path}`, ValidationMiddleware(CreateUserDto), this.user.createUser);
+ this.router.put(`${this.path}/:id(\\d+)`, ValidationMiddleware(CreateUserDto, true), this.user.updateUser);
this.router.delete(`${this.path}/:id(\\d+)`, this.user.deleteUser);
}
}
diff --git a/package.json b/package.json
index b863807..372aa42 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "typescript-express-starter",
- "version": "10.1.1",
+ "version": "10.2.2",
"description": "Quick and Easy TypeScript Express Starter",
"author": "AGUMON ",
"license": "MIT",
@@ -29,27 +29,38 @@
"nginx",
"swc"
],
- "main": "bin/cli.js",
+ "type": "module",
+ "main": "bin/starter.js",
"bin": {
- "typescript-express-starter": "bin/cli.js"
+ "typescript-express-starter": "bin/starter.js"
},
"scripts": {
- "start": "node bin/cli.js"
+ "start": "node bin/starter.js"
},
"dependencies": {
+ "@clack/prompts": "^0.11.0",
"chalk": "^4.1.2",
- "edit-json-file": "^1.5.0",
+ "cli-progress": "^3.12.0",
+ "edit-json-file": "^1.8.1",
+ "execa": "^9.6.0",
+ "fs-extra": "^11.3.0",
"gitignore": "^0.6.0",
"inquirer": "^6.5.2",
"ncp": "^2.0.0",
- "ora": "^4.0.3"
+ "ora": "^4.1.1",
+ "recast": "^0.23.11"
+ },
+ "devDependencies": {
+ "@babel/parser": "^7.28.0",
+ "eslint": "^9.30.1",
+ "prettier": "^3.6.2"
},
"publishConfig": {
"access": "public"
},
"engines": {
- "node": ">= 10.13.0",
- "npm": ">= 6.11.0"
+ "node": ">= 16.13.0",
+ "npm": ">= 9.11.0"
},
"repository": {
"type": "git",
diff --git a/templates/default/.env b/templates/default/.env
new file mode 100644
index 0000000..15c50ce
--- /dev/null
+++ b/templates/default/.env
@@ -0,0 +1,21 @@
+# APP
+PORT=3000
+
+# JWT / Token
+SECRET_KEY=yourSuperSecretKey
+
+# LOGGING
+LOG_DIR=../logs
+LOG_LEVEL=info
+
+# CORS 기본값
+ORIGIN=http://localhost:3000
+CREDENTIALS=true
+CORS_ORIGINS=http://localhost:3000,https://yourdomain.com
+
+# SERVER URL
+API_SERVER_URL=https://api.yourdomain.com
+
+# 선택적 환경변수
+SENTRY_DSN=
+REDIS_URL=redis://localhost:6379
\ No newline at end of file
diff --git a/templates/default/.env.development.local b/templates/default/.env.development.local
new file mode 100644
index 0000000..505fd79
--- /dev/null
+++ b/templates/default/.env.development.local
@@ -0,0 +1,5 @@
+# APP
+NODE_ENV=development
+
+# LOGGING
+LOG_FORMAT=dev
diff --git a/templates/default/.env.production.local b/templates/default/.env.production.local
new file mode 100644
index 0000000..11ad836
--- /dev/null
+++ b/templates/default/.env.production.local
@@ -0,0 +1,5 @@
+# APP
+NODE_ENV=production
+
+# LOGGING
+LOG_FORMAT=combined
diff --git a/templates/default/.env.test.local b/templates/default/.env.test.local
new file mode 100644
index 0000000..2a51176
--- /dev/null
+++ b/templates/default/.env.test.local
@@ -0,0 +1,5 @@
+# APP
+NODE_ENV=test
+
+# LOGGING
+LOG_FORMAT=dev
diff --git a/templates/default/nodemon.json b/templates/default/nodemon.json
new file mode 100644
index 0000000..1fb5df9
--- /dev/null
+++ b/templates/default/nodemon.json
@@ -0,0 +1,7 @@
+{
+ "watch": ["src", ".env"],
+ "ext": "js,ts,json",
+ "ignore": ["logs/*", "**/*.spec.ts", "**/*.test.ts"],
+ "exec": "ts-node -r tsconfig-paths/register --transpile-only src/server.ts",
+ "delay": "500"
+}
diff --git a/templates/default/package.json b/templates/default/package.json
new file mode 100644
index 0000000..912abde
--- /dev/null
+++ b/templates/default/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "default",
+ "version": "1.0.0",
+ "description": "default template",
+ "author": "",
+ "license": "ISC",
+ "scripts": {
+ "start": "cross-env NODE_ENV=production node dist/server.js",
+ "dev": "cross-env NODE_ENV=development nodemon",
+ "build": "tsc && tsc-alias"
+ },
+ "dependencies": {
+ "bcryptjs": "^3.0.2",
+ "compression": "^1.8.1",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.5",
+ "dotenv": "^17.2.1",
+ "express": "^5.1.0",
+ "express-rate-limit": "^8.0.1",
+ "helmet": "^8.1.0",
+ "hpp": "^0.2.3",
+ "jsonwebtoken": "^9.0.2",
+ "morgan": "^1.10.1",
+ "pino": "^9.9.0",
+ "pino-pretty": "^13.1.1",
+ "reflect-metadata": "^0.2.2",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.1",
+ "tslib": "^2.8.1",
+ "tsyringe": "^4.10.0",
+ "zod": "^4.1.3"
+ },
+ "devDependencies": {
+ "@types/compression": "^1.8.1",
+ "@types/cookie-parser": "^1.4.9",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.3",
+ "@types/hpp": "^0.2.6",
+ "@types/jsonwebtoken": "^9.0.10",
+ "@types/morgan": "^1.9.10",
+ "@types/node": "^24.3.0",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.8",
+ "cross-env": "^10.0.0",
+ "node-gyp": "^11.4.2",
+ "nodemon": "^3.1.10",
+ "ts-node": "^10.9.2",
+ "tsc-alias": "^1.8.16",
+ "tsconfig-paths": "^4.2.0",
+ "typescript": "^5.9.2"
+ }
+}
diff --git a/templates/default/src/app.ts b/templates/default/src/app.ts
new file mode 100644
index 0000000..761d72b
--- /dev/null
+++ b/templates/default/src/app.ts
@@ -0,0 +1,155 @@
+import compression from 'compression';
+import cookieParser from 'cookie-parser';
+import cors from 'cors';
+import express from 'express';
+import rateLimit from 'express-rate-limit';
+import helmet from 'helmet';
+import hpp from 'hpp';
+import morgan from 'morgan';
+import swaggerJSDoc from 'swagger-jsdoc';
+import swaggerUi from 'swagger-ui-express';
+import {
+ NODE_ENV, PORT, LOG_FORMAT, CREDENTIALS,
+ CORS_ORIGIN_LIST, API_SERVER_URL,
+} from '@config/env';
+import { Routes } from '@interfaces/routes.interface';
+import { ErrorMiddleware } from '@middlewares/error.middleware';
+import { logger, stream } from '@utils/logger';
+
+class App {
+ public app: express.Application;
+ public env: string;
+ public port: string | number;
+
+ constructor(routes: Routes[], apiPrefix = '/api/v1') {
+ this.app = express();
+ this.env = NODE_ENV || 'development';
+ this.port = PORT || 3000;
+
+ this.initializeTrustProxy();
+ this.initializeMiddlewares();
+ this.initializeRoutes(routes, apiPrefix);
+ this.initializeSwagger(apiPrefix);
+ this.initializeErrorHandling();
+ }
+
+ public listen() {
+ const server = this.app.listen(this.port, () => {
+ logger.info(`=================================`);
+ logger.info(`======= ENV: ${this.env} =======`);
+ logger.info(`🚀 App listening on the port ${this.port}`);
+ logger.info(`=================================`);
+ });
+
+ return server;
+ }
+
+ public getServer() {
+ return this.app;
+ }
+
+ private initializeTrustProxy() {
+ // Nginx, Heroku, Cloudflare 등 프록시 환경에서 실IP 추출을 위해 필요
+ this.app.set('trust proxy', 1);
+ }
+
+ private initializeMiddlewares() {
+ this.app.use(
+ rateLimit({
+ windowMs: 60 * 1000, // 1분
+ max: this.env === 'production' ? 100 : 0, // 개발환경에서는 제한 없음
+ message: { error: 'Too many requests, please try again later.' },
+ keyGenerator: req => req.ip || '', // 신뢰 프록시 하에서 실IP, undefined 방지
+ skip: req => this.env !== 'production' || req.ip === '127.0.0.1',
+ legacyHeaders: false,
+ standardHeaders: true,
+ }),
+ );
+
+ this.app.use(morgan(LOG_FORMAT || 'dev', { stream }));
+
+ // CORS 화이트리스트를 환경변수에서 관리
+ const allowedOrigins = CORS_ORIGIN_LIST.length > 0
+ ? CORS_ORIGIN_LIST
+ : ['http://localhost:3000'];
+
+ this.app.use(
+ cors({
+ origin: (origin, callback) => {
+ if (!origin || allowedOrigins.includes(origin)) {
+ callback(null, true);
+ } else {
+ callback(new Error('Not allowed by CORS'));
+ }
+ },
+ credentials: CREDENTIALS,
+ }),
+ );
+
+ this.app.use(hpp());
+ this.app.use(
+ helmet({
+ contentSecurityPolicy:
+ this.env === 'production'
+ ? {
+ directives: {
+ defaultSrc: ["'self'"],
+ scriptSrc: ["'self'", "'unsafe-inline'"],
+ objectSrc: ["'none'"],
+ upgradeInsecureRequests: [],
+ },
+ }
+ : false, // 개발 환경에서는 CSP 비활성화 (hot reload 등 편의)
+ referrerPolicy: { policy: 'no-referrer' },
+ }),
+ );
+ this.app.use(compression());
+ this.app.use(express.json({ limit: '10mb' }));
+ this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+ this.app.use(cookieParser());
+ }
+
+ private initializeRoutes(routes: Routes[], apiPrefix: string) {
+ routes.forEach(route => {
+ this.app.use(apiPrefix, route.router);
+ });
+ }
+
+ private initializeSwagger(apiPrefix: string) {
+ const options = {
+ swaggerDefinition: {
+ openapi: '3.0.0',
+ info: {
+ title: 'REST API',
+ version: '1.0.0',
+ description: 'Example API Documentation',
+ },
+ servers: [
+ {
+ url: API_SERVER_URL || `http://localhost:${this.port}${apiPrefix}`,
+ description: this.env === 'production' ? 'Production server' : 'Local server',
+ },
+ ],
+ components: {
+ securitySchemes: {
+ bearerAuth: {
+ type: 'http',
+ scheme: 'bearer',
+ bearerFormat: 'JWT',
+ },
+ },
+ },
+ },
+ apis: ['swagger.yaml', 'src/controllers/*.ts'],
+ };
+
+ const specs = swaggerJSDoc(options);
+ this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
+ }
+
+ private initializeErrorHandling() {
+ this.app.use(ErrorMiddleware);
+ }
+}
+
+export default App;
diff --git a/templates/default/src/config/env.ts b/templates/default/src/config/env.ts
new file mode 100644
index 0000000..bb8b718
--- /dev/null
+++ b/templates/default/src/config/env.ts
@@ -0,0 +1,76 @@
+import { config } from 'dotenv';
+import { existsSync } from 'fs';
+import { resolve } from 'path';
+import { z } from 'zod';
+
+/**
+ * 1) dotenv 로드 순서
+ * - .env (공통)
+ * - .env.{NODE_ENV}.local (환경별 override, 있으면 덮어씀)
+ */
+config(); // .env
+const nodeEnv = process.env.NODE_ENV || 'development';
+const layerPath = resolve(process.cwd(), `.env.${nodeEnv}.local`);
+if (existsSync(layerPath)) {
+ config({ path: layerPath });
+}
+
+/**
+ * 2) Zod 스키마 정의
+ * - 필수/선택/기본값 정책은 필요에 맞게 수정 가능
+ */
+const EnvSchema = z
+ .object({
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
+ PORT: z.coerce.number().int().positive().optional(), // 기본값은 app.ts에서 3000 처리
+
+ SECRET_KEY: z.string().min(1),
+
+ LOG_FORMAT: z.string().min(1).optional(), // 기본값은 app.ts에서 'dev'
+ LOG_DIR: z.string().min(1),
+ LOG_LEVEL: z.string().min(1),
+
+ ORIGIN: z.string().min(1), // 필요시 배열화 가능
+ CREDENTIALS: z.coerce.boolean(), // 'true'/'false' 문자열 → boolean
+ CORS_ORIGINS: z.string().optional(), // "http://a.com,http://b.com"
+
+ API_SERVER_URL: z.string().url().optional(),
+
+ SENTRY_DSN: z.string().default(''),
+ REDIS_URL: z.string().url().default('redis://localhost:6379'),
+ })
+ .strip();
+
+/**
+ * 3) 검증(모듈 import 시점에 실행)
+ */
+const parsed = EnvSchema.safeParse(process.env);
+if (!parsed.success) {
+ console.error('\n❌ Invalid environment variables:\n');
+ console.error(parsed.error.format());
+ process.exit(1);
+}
+const env = parsed.data;
+
+/**
+ * 4) 타입 안전한 상수 export
+ * - 다른 파일에서는 process.env 직접 쓰지 말고 여기서만 가져가세요.
+ */
+export const NODE_ENV = env.NODE_ENV;
+export const PORT = env.PORT; // app.ts에서 PORT || 3000
+export const SECRET_KEY = env.SECRET_KEY;
+
+export const LOG_FORMAT = env.LOG_FORMAT; // app.ts에서 LOG_FORMAT || 'dev'
+export const LOG_DIR = env.LOG_DIR;
+export const LOG_LEVEL= env.LOG_LEVEL;
+
+export const ORIGIN = env.ORIGIN;
+export const CREDENTIALS = env.CREDENTIALS;
+
+export const SENTRY_DSN = env.SENTRY_DSN;
+export const REDIS_URL = env.REDIS_URL;
+export const API_SERVER_URL = env.API_SERVER_URL;
+
+// CORS Origins를 배열로도 제공 (없으면 [])
+export const CORS_ORIGIN_LIST =
+ (env.CORS_ORIGINS?.split(',').map(s => s.trim()).filter(Boolean)) ?? [];
diff --git a/templates/default/src/controllers/auth.controller.ts b/templates/default/src/controllers/auth.controller.ts
new file mode 100644
index 0000000..4a533cc
--- /dev/null
+++ b/templates/default/src/controllers/auth.controller.ts
@@ -0,0 +1,50 @@
+import { Request, Response, NextFunction } from 'express';
+import { injectable, inject } from 'tsyringe';
+import { RequestWithUser } from '@interfaces/auth.interface';
+import { User } from '@interfaces/users.interface';
+import { AuthService } from '@services/auth.service';
+
+@injectable()
+export class AuthController {
+ constructor(@inject(AuthService) private readonly authService: AuthService) {}
+
+ public signUp = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const userData: User = req.body;
+ const signUpUserData = await this.authService.signup(userData);
+ res.status(201).json({ data: signUpUserData, message: 'signup' });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ public logIn = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const loginData: User = req.body;
+ const { cookie, user } = await this.authService.login(loginData);
+
+ res.setHeader('Set-Cookie', [cookie]);
+ res.status(200).json({ data: user, message: 'login' });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ public logOut = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const userReq = req as RequestWithUser;
+ const user = userReq.user;
+ await this.authService.logout(user);
+
+ res.clearCookie('Authorization', {
+ httpOnly: true,
+ path: '/',
+ sameSite: 'lax',
+ // secure: true, // 프로덕션에서 HTTPS일 때만
+ });
+ res.status(200).json({ message: 'logout' });
+ } catch (error) {
+ next(error);
+ }
+ };
+}
diff --git a/templates/default/src/controllers/users.controller.ts b/templates/default/src/controllers/users.controller.ts
new file mode 100644
index 0000000..929a4d9
--- /dev/null
+++ b/templates/default/src/controllers/users.controller.ts
@@ -0,0 +1,59 @@
+import { Request, Response, NextFunction } from 'express';
+import { injectable, inject } from 'tsyringe';
+import { User } from '@interfaces/users.interface';
+import { UsersService } from '@services/users.service';
+
+@injectable()
+export class UsersController {
+ constructor(@inject(UsersService) private readonly userService: UsersService) {}
+
+ getUsers = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const users = await this.userService.getAllUsers();
+ res.json({ data: users, message: 'findAll' });
+ } catch (e) {
+ next(e);
+ }
+ };
+
+ getUserById = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const userId: string = req.params.id;
+ const user = await this.userService.getUserById(userId);
+ res.json({ data: user, message: 'findById' });
+ } catch (e) {
+ next(e);
+ }
+ };
+
+ createUser = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const userData: User = req.body;
+ const user = await this.userService.createUser(userData);
+ res.status(201).json({ data: user, message: 'create' });
+ } catch (e) {
+ next(e);
+ }
+ };
+
+ updateUser = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const userId: string = req.params.id;
+ const userData: User = req.body;
+ const user = await this.userService.updateUser(userId, userData);
+ res.json({ data: user, message: 'update' });
+ } catch (e) {
+ next(e);
+ }
+ };
+
+ deleteUser = async (req: Request, res: Response, next: NextFunction): Promise => {
+ try {
+ const userId: string = req.params.id;
+ await this.userService.deleteUser(userId);
+ res.status(204).json({ message: 'delete' });
+ } catch (e) {
+ next(e);
+ }
+ };
+}
diff --git a/templates/default/src/dtos/users.dto.ts b/templates/default/src/dtos/users.dto.ts
new file mode 100644
index 0000000..3ad96b5
--- /dev/null
+++ b/templates/default/src/dtos/users.dto.ts
@@ -0,0 +1,20 @@
+import { z } from 'zod';
+
+// 비밀번호 공통 스키마
+export const passwordSchema = z
+ .string()
+ .min(9, { message: 'Password must be at least 9 characters long.' })
+ .max(32, { message: 'Password must be at most 32 characters long.' });
+
+// 회원가입 DTO (signup, login 공용)
+export const createUserSchema = z.object({
+ email: z.string().email({ message: 'Invalid email format.' }),
+ password: passwordSchema,
+});
+
+export type CreateUserDto = z.infer;
+
+// 수정 DTO (패스워드만 optional)
+export const updateUserSchema = createUserSchema.partial();
+
+export type UpdateUserDto = z.infer;
diff --git a/templates/default/src/exceptions/httpException.ts b/templates/default/src/exceptions/httpException.ts
new file mode 100644
index 0000000..ac7b567
--- /dev/null
+++ b/templates/default/src/exceptions/httpException.ts
@@ -0,0 +1,14 @@
+export class HttpException extends Error {
+ public status: number;
+ public message: string;
+ public data?: unknown;
+
+ constructor(status: number, message: string, data?: unknown) {
+ super(message);
+ this.status = status;
+ this.message = message;
+ this.data = data;
+ this.name = this.constructor.name;
+ Error.captureStackTrace?.(this, this.constructor);
+ }
+}
diff --git a/lib/default/src/interfaces/auth.interface.ts b/templates/default/src/interfaces/auth.interface.ts
similarity index 92%
rename from lib/default/src/interfaces/auth.interface.ts
rename to templates/default/src/interfaces/auth.interface.ts
index 83947b8..76f16bd 100644
--- a/lib/default/src/interfaces/auth.interface.ts
+++ b/templates/default/src/interfaces/auth.interface.ts
@@ -2,7 +2,7 @@ import { Request } from 'express';
import { User } from '@interfaces/users.interface';
export interface DataStoredInToken {
- id: number;
+ id: number | string;
}
export interface TokenData {
diff --git a/lib/default/src/interfaces/routes.interface.ts b/templates/default/src/interfaces/routes.interface.ts
similarity index 100%
rename from lib/default/src/interfaces/routes.interface.ts
rename to templates/default/src/interfaces/routes.interface.ts
diff --git a/templates/default/src/interfaces/users.interface.ts b/templates/default/src/interfaces/users.interface.ts
new file mode 100644
index 0000000..82761d9
--- /dev/null
+++ b/templates/default/src/interfaces/users.interface.ts
@@ -0,0 +1,5 @@
+export interface User {
+ id?: string | number; // uuid or number-string
+ email: string;
+ password: string;
+}
diff --git a/templates/default/src/middlewares/auth.middleware.ts b/templates/default/src/middlewares/auth.middleware.ts
new file mode 100644
index 0000000..46789d2
--- /dev/null
+++ b/templates/default/src/middlewares/auth.middleware.ts
@@ -0,0 +1,49 @@
+import { Request, Response, NextFunction } from 'express';
+import { verify, TokenExpiredError, JsonWebTokenError } from 'jsonwebtoken';
+import { container } from 'tsyringe';
+import { SECRET_KEY } from '@config/env';
+import { HttpException } from '@exceptions/httpException';
+import { DataStoredInToken, RequestWithUser } from '@interfaces/auth.interface';
+import { UsersRepository } from '@repositories/users.repository';
+
+const getAuthorization = (req: RequestWithUser) => {
+ const cookie = req.cookies['Authorization'];
+ if (cookie) return cookie;
+
+ const header = req.header('Authorization');
+ if (header && header.startsWith('Bearer ')) {
+ return header.replace('Bearer ', '').trim();
+ }
+ return null;
+};
+
+export const AuthMiddleware = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const userReq = req as RequestWithUser;
+ const token = getAuthorization(userReq);
+ if (!token) return next(new HttpException(401, 'Authentication token missing'));
+
+ let payload: DataStoredInToken;
+ try {
+ payload = verify(token, SECRET_KEY as string) as DataStoredInToken;
+ } catch (err) {
+ if (err instanceof TokenExpiredError) {
+ return next(new HttpException(401, 'Authentication token expired'));
+ }
+ if (err instanceof JsonWebTokenError) {
+ return next(new HttpException(401, 'Invalid authentication token'));
+ }
+ return next(new HttpException(401, 'Authentication failed'));
+ }
+
+ // 타입 일치 유의 (number/string)
+ const userRepo = container.resolve(UsersRepository);
+ const findUser = await userRepo.findById(String(payload.id));
+ if (!findUser) return next(new HttpException(401, 'User not found with this token'));
+
+ (req as RequestWithUser).user = findUser;
+ next();
+ } catch {
+ next(new HttpException(500, 'Authentication middleware error'));
+ }
+};
diff --git a/templates/default/src/middlewares/error.middleware.ts b/templates/default/src/middlewares/error.middleware.ts
new file mode 100644
index 0000000..06307bc
--- /dev/null
+++ b/templates/default/src/middlewares/error.middleware.ts
@@ -0,0 +1,20 @@
+import { Request, Response } from 'express';
+import { NODE_ENV } from '@config/env';
+import { HttpException } from '@exceptions/httpException';
+import { logger } from '@utils/logger';
+
+export const ErrorMiddleware = (error: HttpException, req: Request, res: Response) => {
+ const status = error.status || 500;
+ const message = error.message || 'Something went wrong';
+ logger.error(`[${req.method}] ${req.originalUrl} | Status: ${status} | Message: ${message}`);
+
+ res.status(status).json({
+ success: false,
+ error: {
+ code: status,
+ message,
+ ...(NODE_ENV === 'development' && error.stack ? { stack: error.stack } : {}),
+ ...(typeof error.data === 'object' && error.data !== null ? { data: error.data } : {}),
+ },
+ });
+};
diff --git a/templates/default/src/middlewares/validation.middleware.ts b/templates/default/src/middlewares/validation.middleware.ts
new file mode 100644
index 0000000..a508c99
--- /dev/null
+++ b/templates/default/src/middlewares/validation.middleware.ts
@@ -0,0 +1,15 @@
+import { NextFunction, Request, Response } from 'express';
+import type { ZodTypeAny } from 'zod';
+import { HttpException } from '@exceptions/httpException';
+
+export function ValidationMiddleware(schema: ZodTypeAny) {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const result = schema.safeParse(req.body);
+ if (!result.success) {
+ const message = result.error.issues.map(e => e.message).join(', ');
+ return next(new HttpException(400, message));
+ }
+ req.body = result.data;
+ next();
+ };
+}
diff --git a/templates/default/src/repositories/users.repository.ts b/templates/default/src/repositories/users.repository.ts
new file mode 100644
index 0000000..93699d0
--- /dev/null
+++ b/templates/default/src/repositories/users.repository.ts
@@ -0,0 +1,52 @@
+import { singleton } from 'tsyringe';
+import { User } from '@interfaces/users.interface';
+
+export interface IUsersRepository {
+ findAll(): Promise;
+ findById(id: string): Promise;
+ findByEmail(email: string): Promise;
+ save(user: User): Promise;
+ update(id: string, update: User): Promise;
+ delete(id: string): Promise;
+}
+
+@singleton()
+export class UsersRepository implements IUsersRepository {
+ private users: User[] = [];
+
+ async findAll(): Promise {
+ return this.users;
+ }
+
+ async findById(id: string): Promise {
+ return this.users.find(u => u.id === id);
+ }
+
+ async findByEmail(email: string): Promise {
+ return this.users.find(u => u.email === email);
+ }
+
+ async save(user: User): Promise {
+ this.users.push(user);
+ return user;
+ }
+
+ async update(id: string, user: User): Promise {
+ const idx = this.users.findIndex(u => u.id === id);
+ if (idx === -1) return undefined;
+
+ this.users[idx] = { ...this.users[idx], password: user.password };
+ return this.users[idx];
+ }
+
+ async delete(id: string): Promise {
+ const idx = this.users.findIndex(u => u.id === id);
+ if (idx === -1) return false;
+ this.users.splice(idx, 1);
+ return true;
+ }
+
+ reset() {
+ this.users = [];
+ }
+}
diff --git a/templates/default/src/routes/auth.route.ts b/templates/default/src/routes/auth.route.ts
new file mode 100644
index 0000000..53d09d6
--- /dev/null
+++ b/templates/default/src/routes/auth.route.ts
@@ -0,0 +1,23 @@
+import { Router } from 'express';
+import { injectable, inject } from 'tsyringe';
+import { AuthController } from '@controllers/auth.controller';
+import { createUserSchema } from '@dtos/users.dto';
+import { Routes } from '@interfaces/routes.interface';
+import { AuthMiddleware } from '@middlewares/auth.middleware';
+import { ValidationMiddleware } from '@middlewares/validation.middleware';
+
+@injectable()
+export class AuthRoute implements Routes {
+ public router: Router = Router();
+ public path = '/auth';
+
+ constructor(@inject(AuthController) private authController: AuthController) {
+ this.initializeRoutes();
+ }
+
+ private initializeRoutes() {
+ this.router.post(`${this.path}/signup`, ValidationMiddleware(createUserSchema), this.authController.signUp);
+ this.router.post(`${this.path}/login`, ValidationMiddleware(createUserSchema), this.authController.logIn);
+ this.router.post(`${this.path}/logout`, AuthMiddleware, this.authController.logOut);
+ }
+}
diff --git a/templates/default/src/routes/users.route.ts b/templates/default/src/routes/users.route.ts
new file mode 100644
index 0000000..594001d
--- /dev/null
+++ b/templates/default/src/routes/users.route.ts
@@ -0,0 +1,24 @@
+import { Router } from 'express';
+import { injectable, inject } from 'tsyringe';
+import { UsersController } from '@controllers/users.controller';
+import { createUserSchema, updateUserSchema } from '@dtos/users.dto';
+import { Routes } from '@interfaces/routes.interface';
+import { ValidationMiddleware } from '@middlewares/validation.middleware';
+
+@injectable()
+export class UsersRoute implements Routes {
+ public router: Router = Router();
+ public path = '/users';
+
+ constructor(@inject(UsersController) private userController: UsersController) {
+ this.initializeRoutes();
+ }
+
+ private initializeRoutes() {
+ this.router.get(this.path, this.userController.getUsers);
+ this.router.get(`${this.path}/:id`, this.userController.getUserById);
+ this.router.post(this.path, ValidationMiddleware(createUserSchema), this.userController.createUser);
+ this.router.put(`${this.path}/:id`, ValidationMiddleware(updateUserSchema), this.userController.updateUser);
+ this.router.delete(`${this.path}/:id`, this.userController.deleteUser);
+ }
+}
diff --git a/templates/default/src/server.ts b/templates/default/src/server.ts
new file mode 100644
index 0000000..05add38
--- /dev/null
+++ b/templates/default/src/server.ts
@@ -0,0 +1,35 @@
+import 'reflect-metadata';
+import '@config/env';
+import { container } from 'tsyringe';
+import App from '@/app';
+import { UsersRepository } from '@repositories/users.repository';
+import { AuthRoute } from '@routes/auth.route';
+import { UsersRoute } from '@routes/users.route';
+
+// DI 등록
+container.registerInstance(UsersRepository, new UsersRepository());
+
+// 라우트 모듈을 필요에 따라 동적으로 배열화 가능
+const routes = [container.resolve(UsersRoute), container.resolve(AuthRoute)];
+
+// API prefix는 app.ts에서 기본값 세팅, 필요하면 인자로 전달
+const appInstance = new App(routes);
+
+// listen()이 서버 객체(http.Server)를 반환하도록 app.ts를 살짝 수정
+const server = appInstance.listen(); // PORT를 쓰려면 이렇게 전달도 가능
+
+// Graceful Shutdown: 운영환경에서 필수!
+if (server && typeof server.close === 'function') {
+ ['SIGINT', 'SIGTERM'].forEach(signal => {
+ process.on(signal, () => {
+ console.log(`Received ${signal}, closing server...`);
+ server.close(() => {
+ console.log('HTTP server closed gracefully');
+ // 필요하면 DB/Redis 등 외부 자원 해제 코드 추가
+ process.exit(0);
+ });
+ });
+ });
+}
+
+export default server;
diff --git a/templates/default/src/services/auth.service.ts b/templates/default/src/services/auth.service.ts
new file mode 100644
index 0000000..8199c9a
--- /dev/null
+++ b/templates/default/src/services/auth.service.ts
@@ -0,0 +1,63 @@
+import { hash, compare } from 'bcryptjs';
+import { sign } from 'jsonwebtoken';
+import { injectable, inject } from 'tsyringe';
+import { NODE_ENV, SECRET_KEY } from '@config/env';
+import { HttpException } from '@exceptions/httpException';
+import { DataStoredInToken, TokenData } from '@interfaces/auth.interface';
+import { User } from '@interfaces/users.interface';
+import { UsersRepository } from '@repositories/users.repository';
+import type { IUsersRepository } from '@repositories/users.repository';
+
+@injectable()
+export class AuthService {
+ constructor(@inject(UsersRepository) private usersRepository: IUsersRepository) {}
+
+ private createToken(user: User): TokenData {
+ if (!SECRET_KEY) throw new Error('SECRET_KEY is not defined');
+
+ if (user.id === undefined) {
+ throw new Error('User id is undefined');
+ }
+
+ const dataStoredInToken: DataStoredInToken = { id: user.id };
+ const expiresIn = 60 * 60; // 1h
+ const token = sign(dataStoredInToken, SECRET_KEY as string, { expiresIn });
+ return { expiresIn, token };
+ }
+
+ private createCookie(tokenData: TokenData): string {
+ return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn}; Path=/; SameSite=Lax;${NODE_ENV === 'production' ? ' Secure;' : ''}`;
+ }
+
+ public async signup(userData: User): Promise {
+ const findUser = await this.usersRepository.findByEmail(userData.email);
+ if (findUser) throw new HttpException(409, `Email is already in use`);
+
+ const hashedPassword = await hash(userData.password, 10);
+ const newUser: User = { id: String(Date.now()), email: userData.email, password: hashedPassword };
+
+ await this.usersRepository.save(newUser);
+ return newUser;
+ }
+
+ public async login(loginData: User): Promise<{ cookie: string; user: User }> {
+ const findUser = await this.usersRepository.findByEmail(loginData.email);
+ if (!findUser) throw new HttpException(401, `Invalid email or password.`);
+
+ const isPasswordMatching = await compare(loginData.password, findUser.password);
+ if (!isPasswordMatching) throw new HttpException(401, 'Password is incorrect');
+
+ const tokenData = this.createToken(findUser);
+ const cookie = this.createCookie(tokenData);
+
+ return { cookie, user: findUser };
+ }
+
+ public async logout(user: User): Promise {
+ // 로그아웃은 실제 서비스에서는 서버에서 세션/리프레시토큰을 블랙리스트 처리 등 구현 가능
+ // 여기서는 클라이언트의 쿠키를 삭제하면 충분
+ console.log(`User with email ${user.email} logged out.`);
+
+ return;
+ }
+}
diff --git a/templates/default/src/services/users.service.ts b/templates/default/src/services/users.service.ts
new file mode 100644
index 0000000..2f1af5f
--- /dev/null
+++ b/templates/default/src/services/users.service.ts
@@ -0,0 +1,48 @@
+import { hash } from 'bcryptjs';
+import { injectable, inject } from 'tsyringe';
+import { HttpException } from '@exceptions/httpException';
+import { User } from '@interfaces/users.interface';
+import { UsersRepository } from '@repositories/users.repository';
+import type { IUsersRepository } from '@repositories/users.repository';
+
+@injectable()
+export class UsersService {
+ constructor(@inject(UsersRepository) private usersRepository: IUsersRepository) {}
+
+ async getAllUsers(): Promise {
+ return this.usersRepository.findAll();
+ }
+
+ async getUserById(id: string): Promise {
+ const user = await this.usersRepository.findById(id);
+ if (!user) throw new HttpException(404, 'User not found');
+ return user;
+ }
+
+ async createUser(user: User): Promise {
+ const exists = await this.usersRepository.findByEmail(user.email);
+ if (exists) throw new HttpException(409, 'Email already exists');
+
+ const hashedPassword = await hash(user.password, 10);
+ const created: User = { id: String(Date.now()), email: user.email, password: hashedPassword };
+ await this.usersRepository.save(created);
+ return created;
+ }
+
+ async updateUser(id: string, update: User): Promise {
+ const exists = await this.usersRepository.findById(id);
+ if (!exists) throw new HttpException(404, 'User not found');
+
+ const hashedPassword = await hash(update.password, 10);
+ const user = { ...update, password: hashedPassword };
+
+ const updated = await this.usersRepository.update(id, user);
+ if (!updated) throw new HttpException(404, 'User not found');
+ return updated;
+ }
+
+ async deleteUser(id: string): Promise {
+ const deleted = await this.usersRepository.delete(id);
+ if (!deleted) throw new HttpException(404, 'User not found');
+ }
+}
diff --git a/templates/default/src/utils/logger.ts b/templates/default/src/utils/logger.ts
new file mode 100644
index 0000000..e37fc92
--- /dev/null
+++ b/templates/default/src/utils/logger.ts
@@ -0,0 +1,57 @@
+import { existsSync, mkdirSync } from 'fs';
+import { join } from 'path';
+import pino from 'pino';
+import { LOG_DIR, LOG_LEVEL, NODE_ENV } from '@config/env';
+
+const logDir: string = join(__dirname, LOG_DIR || '/logs');
+if (!existsSync(logDir)) {
+ mkdirSync(logDir, { recursive: true });
+}
+
+// 파일 로깅용 경로
+const debugLogPath = join(logDir, 'debug.log');
+const errorLogPath = join(logDir, 'error.log');
+
+// 로그 레벨 및 환경 설정
+const isProd = NODE_ENV === 'production';
+const logLevel = LOG_LEVEL || 'info';
+
+// Pino 인스턴스
+export const logger = pino(
+ {
+ level: logLevel,
+ formatters: {
+ level: label => ({ level: label }),
+ },
+ transport: !isProd
+ ? {
+ // 개발환경: 예쁜 콘솔 출력
+ target: 'pino-pretty',
+ options: {
+ colorize: true,
+ translateTime: 'yyyy-mm-dd HH:MM:ss',
+ ignore: 'pid,hostname',
+ },
+ }
+ : undefined,
+ },
+ isProd ? pino.destination(debugLogPath) : undefined,
+);
+
+// 파일 로깅은 에러만 별도 핸들러로 예시
+export const errorLogger = isProd ? pino(pino.destination(errorLogPath)) : logger;
+
+// morgan stream 인터페이스
+export const stream = {
+ write: (msg: string) => logger.info(msg.trim()),
+};
+
+// 전역 에러 핸들링 (필요하면)
+process.on('uncaughtException', err => {
+ logger.error(`Uncaught Exception: ${err.message}`);
+ process.exit(1);
+});
+process.on('unhandledRejection', reason => {
+ logger.error(`Unhandled Rejection: ${JSON.stringify(reason)}`);
+ process.exit(1);
+});
diff --git a/templates/default/swagger.yaml b/templates/default/swagger.yaml
new file mode 100644
index 0000000..2f6bb18
--- /dev/null
+++ b/templates/default/swagger.yaml
@@ -0,0 +1,124 @@
+openapi: 3.0.0
+info:
+ title: Express Typescript API
+ description: User API Docs
+ version: "1.0.0"
+schemes:
+ - https
+ - http
+tags:
+ - name: users
+ description: users API
+paths:
+ /users:
+ get:
+ tags: [users]
+ summary: Find All Users
+ responses:
+ 200:
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/User'
+ 500:
+ description: Server Error
+ post:
+ tags: [users]
+ summary: Add User
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ responses:
+ 201:
+ description: Created
+ 400:
+ description: Bad Request
+ 409:
+ description: Conflict
+ 500:
+ description: Server Error
+ /users/{id}:
+ get:
+ tags: [users]
+ summary: Find User By Id
+ parameters:
+ - name: id
+ in: path
+ description: User Id
+ required: true
+ schema:
+ type: string
+ responses:
+ 200:
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ 404:
+ description: Not Found
+ 500:
+ description: Server Error
+ put:
+ tags: [users]
+ summary: Update User By Id
+ parameters:
+ - name: id
+ in: path
+ description: user Id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ responses:
+ 200:
+ description: OK
+ 400:
+ description: Bad Request
+ 409:
+ description: Conflict
+ 500:
+ description: Server Error
+ delete:
+ tags: [users]
+ summary: Delete User By Id
+ parameters:
+ - name: id
+ in: path
+ description: user Id
+ required: true
+ schema:
+ type: string
+ responses:
+ 204:
+ description: No Content
+ 404:
+ description: Not Found
+ 500:
+ description: Server Error
+
+components:
+ schemas:
+ User:
+ type: object
+ required:
+ - email
+ - password
+ properties:
+ id:
+ type: string
+ email:
+ type: string
+ password:
+ type: string
diff --git a/lib/default/tsconfig.json b/templates/default/tsconfig.json
similarity index 68%
rename from lib/default/tsconfig.json
rename to templates/default/tsconfig.json
index d18ecef..2fd24be 100644
--- a/lib/default/tsconfig.json
+++ b/templates/default/tsconfig.json
@@ -1,39 +1,40 @@
{
- "compileOnSave": false,
"compilerOptions": {
- "target": "es2017",
- "lib": ["es2017", "esnext.asynciterable"],
- "typeRoots": ["node_modules/@types"],
- "allowSyntheticDefaultImports": true,
- "experimentalDecorators": true,
- "emitDecoratorMetadata": true,
- "forceConsistentCasingInFileNames": true,
- "moduleResolution": "node",
+ "target": "es2020",
"module": "commonjs",
- "pretty": true,
- "sourceMap": true,
- "declaration": true,
+ "moduleResolution": "node",
"outDir": "dist",
- "allowJs": true,
- "noEmit": false,
- "esModuleInterop": true,
- "resolveJsonModule": true,
- "importHelpers": true,
+ "declaration": true,
+ "sourceMap": true,
+ "strict": true,
+ "lib": ["es2020", "esnext.asynciterable"],
+
"baseUrl": "src",
"paths": {
"@/*": ["*"],
- "@config": ["config"],
+ "@config/*": ["config/*"],
"@controllers/*": ["controllers/*"],
"@dtos/*": ["dtos/*"],
"@exceptions/*": ["exceptions/*"],
"@interfaces/*": ["interfaces/*"],
"@middlewares/*": ["middlewares/*"],
- "@models/*": ["models/*"],
+ "@repositories/*": ["repositories/*"],
"@routes/*": ["routes/*"],
"@services/*": ["services/*"],
"@utils/*": ["utils/*"]
- }
+ },
+
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "importHelpers": true,
+ "forceConsistentCasingInFileNames": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "pretty": true,
+ "allowJs": false,
+ "noEmit": false
},
- "include": ["src/**/*.ts", "src/**/*.json", ".env"],
- "exclude": ["node_modules", "src/http", "src/logs"]
+ "include": ["src/**/*.ts", "src/**/*.json"],
+ "exclude": ["node_modules", "dist", "coverage", "logs", "src/http"]
}