├── .dockerignore ├── .editorconfig ├── .env.example ├── .github ├── FUNDING.yml └── workflows │ ├── code_review.yml │ ├── deploy.yml │ ├── pre_commit.yml │ ├── pull_request.yml │ ├── spectral.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .pre-commit-config.yaml ├── .spectral.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── _templates ├── action │ └── new │ │ └── hello.ejs.t └── service │ ├── crud │ ├── create.ejs.t │ ├── delete.ejs.t │ ├── list.ejs.t │ ├── service.ejs.t │ ├── service.test.ejs.t │ ├── update.ejs.t │ └── view.ejs.t │ └── new │ ├── hello.ejs.t │ └── hello.test.ejs.t ├── biome.json ├── cli.ts ├── cspell-tool.txt ├── cspell.json ├── deta.json ├── docker-compose.env ├── docker-compose.yml ├── examples ├── README.md ├── express │ └── index.ts ├── socket │ ├── index.ts │ └── public │ │ └── index.html └── web │ └── client.ts ├── fly.toml ├── k8s.yaml ├── logger.ts ├── moleculer.config.ts ├── openapi-ts.config.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── docs │ ├── api.html │ ├── index.html │ └── open-api.json └── index.html ├── renovate.json ├── server.ts ├── services ├── api.service.ts ├── dtos │ ├── product-dto.swagger.yaml │ └── product.dto.ts ├── greeter.service.ts └── product.service.ts ├── test ├── e2e │ ├── greeter.hurl │ ├── health.hurl │ └── product.hurl └── unit │ └── services │ ├── greeter.spec.ts │ └── product.spec.ts ├── tsconfig.json ├── tsup.config.ts ├── vite.config.ts └── wait-on.config.cjs /.dockerignore: -------------------------------------------------------------------------------- 1 | docker-compose.env 2 | docker-compose.yml 3 | Dockerfile 4 | node_modules 5 | test 6 | .vscode 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = tab 11 | indent_size = 4 12 | space_after_anon_function = true 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [{package,bower}.json] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.{yml,yaml}] 30 | trim_trailing_whitespace = false 31 | indent_style = space 32 | indent_size = 2 33 | 34 | [*.js] 35 | quote_type = "double" 36 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NAMESPACE=api 2 | LOGGER=true 3 | LOGLEVEL=debug 4 | SERVICEDIR=dist/services 5 | SEVER_HOSTNAME=127.0.0.1 6 | 7 | PORT=3000 8 | TRANSPORTER=TCP 9 | # Or use NATS, you need to install nats-server on your machine. 10 | # TRANSPORTER=nats://localhost:4222 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jellydn] 2 | ko_fi: dunghd 3 | -------------------------------------------------------------------------------- /.github/workflows/code_review.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | review: 8 | name: runner / Biome 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: pnpm/action-setup@v3 16 | name: Install pnpm 17 | with: 18 | version: 9 19 | run_install: true 20 | - uses: mongolyy/reviewdog-action-biome@v1 21 | with: 22 | github_token: ${{ secrets.github_token }} 23 | reporter: github-pr-review 24 | misspell: 25 | name: runner / Misspell 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | pull-requests: write 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: misspell 33 | uses: reviewdog/action-misspell@v1 34 | with: 35 | github_token: ${{ secrets.github_token }} 36 | locale: "US" 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | pull_request: {} 8 | 9 | jobs: 10 | lint: 11 | name: ⬣ Linter 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 🛑 Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.12.1 16 | 17 | - name: ⬇️ Checkout repo 18 | uses: actions/checkout@v4 19 | 20 | - name: ⎔ Setup node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 18 24 | 25 | - name: 📥 Install pnpm 26 | run: npm install -g pnpm 27 | 28 | - name: 📥 Download deps 29 | run: pnpm install 30 | 31 | - name: 🔬 Lint 32 | run: pnpm run lint 33 | 34 | typecheck: 35 | name: ʦ TypeScript 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: 🛑 Cancel Previous Runs 39 | uses: styfle/cancel-workflow-action@0.12.1 40 | 41 | - name: ⬇️ Checkout repo 42 | uses: actions/checkout@v4 43 | 44 | - name: ⎔ Setup node 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: 18 48 | 49 | - name: 📥 Install pnpm 50 | run: npm install -g pnpm 51 | 52 | - name: 📥 Download deps 53 | run: pnpm install 54 | 55 | - name: 🔎 Type check 56 | run: pnpm run typecheck 57 | 58 | test: 59 | name: ⚡ Vitest 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: 🛑 Cancel Previous Runs 63 | uses: styfle/cancel-workflow-action@0.12.1 64 | 65 | - name: ⬇️ Checkout repo 66 | uses: actions/checkout@v4 67 | 68 | - name: ⎔ Setup node 69 | uses: actions/setup-node@v4 70 | with: 71 | node-version: 18 72 | 73 | - name: 📥 Install pnpm 74 | run: npm install -g pnpm 75 | 76 | - name: 📥 Download deps 77 | run: pnpm install 78 | 79 | - name: ⚡ Run vitest 80 | run: pnpm run test 81 | 82 | build: 83 | name: 🐳 Build 84 | # only build/deploy main branch on pushes 85 | if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: 🛑 Cancel Previous Runs 89 | uses: styfle/cancel-workflow-action@0.12.1 90 | 91 | - name: ⬇️ Checkout repo 92 | uses: actions/checkout@v4 93 | 94 | - name: 👀 Read app name 95 | uses: SebRollen/toml-action@v1.2.0 96 | id: app_name 97 | with: 98 | file: "fly.toml" 99 | field: "app" 100 | 101 | - name: 🐳 Set up Docker Buildx 102 | uses: docker/setup-buildx-action@v3 103 | 104 | # Setup cache 105 | - name: ⚡️ Cache Docker layers 106 | uses: actions/cache@v4 107 | with: 108 | path: /tmp/.buildx-cache 109 | key: ${{ runner.os }}-buildx-${{ github.sha }} 110 | restore-keys: | 111 | ${{ runner.os }}-buildx- 112 | 113 | - name: 🔑 Fly Registry Auth 114 | uses: docker/login-action@v3 115 | with: 116 | registry: registry.fly.io 117 | username: x 118 | password: ${{ secrets.FLY_API_TOKEN }} 119 | 120 | - name: 🐳 Docker build 121 | uses: docker/build-push-action@v6 122 | with: 123 | context: . 124 | push: true 125 | tags: registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }} 126 | build-args: | 127 | COMMIT_SHA=${{ github.sha }} 128 | cache-from: type=local,src=/tmp/.buildx-cache 129 | cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new 130 | 131 | # This ugly bit is necessary if you don't want your cache to grow forever 132 | # till it hits GitHub's limit of 5GB. 133 | # Temp fix 134 | # https://github.com/docker/build-push-action/issues/252 135 | # https://github.com/moby/buildkit/issues/1896 136 | - name: 🚚 Move cache 137 | run: | 138 | rm -rf /tmp/.buildx-cache 139 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 140 | 141 | deploy: 142 | name: 🚀 Deploy 143 | runs-on: ubuntu-latest 144 | needs: [lint, typecheck, test, build] 145 | # only build/deploy main branch on pushes 146 | if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} 147 | 148 | steps: 149 | - name: 🛑 Cancel Previous Runs 150 | uses: styfle/cancel-workflow-action@0.12.1 151 | 152 | - name: ⬇️ Checkout repo 153 | uses: actions/checkout@v4 154 | 155 | - name: 👀 Read app name 156 | uses: SebRollen/toml-action@v1.2.0 157 | id: app_name 158 | with: 159 | file: "fly.toml" 160 | field: "app" 161 | 162 | - name: Setup flyctl 163 | uses: superfly/flyctl-actions/setup-flyctl@master 164 | - name: 🚀 Deploy Production 165 | run: "flyctl deploy --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" 166 | env: 167 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 168 | -------------------------------------------------------------------------------- /.github/workflows/pre_commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | # Give the default GITHUB_TOKEN write permission to commit and push the 13 | # added or changed files to the repository. 14 | contents: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | - name: Install pre-commit with pipx 19 | run: | 20 | python3 -m pip install --user pipx 21 | python3 -m pipx ensurepath 22 | pipx install pre-commit 23 | 24 | - name: Run pre-commit 25 | run: pre-commit run --all-files 26 | continue-on-error: true # Continue even if pre-commit fails 27 | 28 | # Commit all changed files back to the repository 29 | - uses: stefanzweifel/git-auto-commit-action@v5 30 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | quality: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | # Give the default GITHUB_TOKEN write permission to commit and push the 12 | # added or changed files to the repository. 13 | contents: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup Biome 18 | uses: biomejs/setup-biome@v2 19 | with: 20 | version: latest 21 | - name: Run Biome 22 | run: biome ci . 23 | continue-on-error: true # Continue even if pre-commit fails 24 | # Commit all changed files back to the repository 25 | - uses: stefanzweifel/git-auto-commit-action@v5 26 | 27 | oxlint: 28 | name: Lint JS 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - run: npx --yes oxlint@latest --deny-warnings # 33 | -------------------------------------------------------------------------------- /.github/workflows/spectral.yml: -------------------------------------------------------------------------------- 1 | name: Run Spectral on Pull Requests 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | linter: 8 | name: Run Spectral 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Check out the repository 12 | - uses: actions/checkout@v4 13 | 14 | # Run Spectral 15 | - uses: stoplightio/spectral-action@latest 16 | with: 17 | file_glob: "public/docs/*.json" 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Install bun 22 | uses: oven-sh/setup-bun@v2 23 | - name: Install dependencies 24 | run: bun install 25 | 26 | - name: Run tests 27 | run: bun run test 28 | 29 | e2e-test: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Install bun 36 | uses: oven-sh/setup-bun@v2 37 | - name: Install dependencies 38 | run: bun install 39 | 40 | - name: Install wait-on 41 | run: bun add -g wait-on 42 | 43 | - name: Integration test 44 | run: | 45 | PORT=8888 TRANSPORTER=TCP bun run start & 46 | curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.0.0/hurl_4.0.0_amd64.deb 47 | sudo dpkg -i hurl_4.0.0_amd64.deb 48 | wait-on http://0.0.0.0:8888/api/health/check -l -i 500 -d 1000 49 | hurl --variable PORT=8888 --test test/e2e/*.hurl 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # JetBrains IDE 64 | .idea 65 | 66 | # Build folder 67 | dist 68 | 69 | # Envrc file 70 | .envrc 71 | generated/sdk 72 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-manager-strict=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-prettier 3 | rev: "v4.0.0-alpha.8" # Use the sha or tag you want to point at 4 | hooks: 5 | - id: prettier 6 | # Those are not supported by biomejs yet, refer https://biomejs.dev/internals/language-support/ 7 | types_or: [html, css, markdown] 8 | - repo: https://github.com/biomejs/pre-commit 9 | rev: "v0.1.0" # Use the sha / tag you want to point at 10 | hooks: 11 | - id: biome-check 12 | additional_dependencies: ["@biomejs/biome@1.6.3"] 13 | -------------------------------------------------------------------------------- /.spectral.yaml: -------------------------------------------------------------------------------- 1 | extends: ["spectral:oas", "spectral:asyncapi"] 2 | rules: 3 | no-$ref-siblings: off 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js 20 alpine image as builder 2 | FROM node:22.15.0-alpine as builder 3 | 4 | # Set up working directory 5 | RUN mkdir /app 6 | WORKDIR /app 7 | 8 | # Install git and other dependencies 9 | RUN apk --no-cache add git 10 | 11 | # Copy essential files 12 | COPY package.json pnpm-lock.yaml *.ts ./ 13 | COPY services services 14 | COPY public public 15 | 16 | # Install pnpm and dependencies 17 | RUN npm install -g pnpm 18 | ENV COREPACK_ENABLE_STRICT=0 19 | RUN pnpm install 20 | ENV NODE_ENV=production 21 | 22 | # Set up the production image 23 | FROM node:22.15.0-alpine 24 | WORKDIR /app 25 | 26 | # Copy build output from builder 27 | COPY --from=builder /app . 28 | 29 | # Expose port for application 30 | EXPOSE 8080 31 | 32 | # Start the application 33 | CMD ["npm", "run", "start"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Huynh Duc Dung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to moleculer-typescript-template 👋 2 | 3 | ![Version](https://img.shields.io/badge/version-0.1.1-blue.svg?cacheSeconds=2592000) 4 | ![Prerequisite](https://img.shields.io/badge/node-%3E%3D%2014.x.x-blue.svg) 5 | [![Twitter: jellydn](https://img.shields.io/twitter/follow/jellydn.svg?style=social)](https://twitter.com/jellydn) 6 | 7 | > My Moleculer-based microservices project 8 | 9 | [![Moleculer - Progressive microservices framework for Node.js ](https://img.youtube.com/vi/peb2OflRu-4/0.jpg)](https://www.youtube.com/watch?v=peb2OflRu-4) 10 | 11 | [![IT Man - Automating API Client Generation with openapi-ts](https://i.ytimg.com/vi/LwfcoOWlyOw/hqdefault.jpg)](https://www.youtube.com/watch?v=LwfcoOWlyOw) 12 | 13 | [![IT Man - Logdy Deep Dive: Streamlining Log Management with a Powerful Web UI](https://i.ytimg.com/vi/wO5MTD3Lawg/hqdefault.jpg)](https://www.youtube.com/watch?v=wO5MTD3Lawg) 14 | 15 | [![ITMan - Automate API Linting: Spectral Integration with VS Code and GitHub Actions](https://i.ytimg.com/vi/sTjIgGBhfMs/hqdefault.jpg)](https://www.youtube.com/watch?v=sTjIgGBhfMs) 16 | 17 | 18 | 19 | ## Prerequisites 20 | 21 | - node >= 18.17.x 22 | 23 | ## Init new project 24 | 25 | ```sh 26 | npx degit jellydn/moleculer-typescript-template [PROJECT-NAME] 27 | ``` 28 | 29 | ## Features 30 | 31 | - ⚡️ Progressive microservices framework for Node.js. 32 | [Moleculer](https://moleculer.services/) with [Typescript](https://www.typescriptlang.org/) template 33 | - 📦 [hygen](http://www.hygen.io/) - The scalable code generator that saves you time. 34 | - 🦾 [pino](https://getpino.io) - super fast, all natural json logger 35 | - 🔥 [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc/blob/v6/docs/README.md) - Generates swagger/openapi specification based on jsDoc comments and YAML files. 36 | - ✨ [moleculer-zod-validator](https://github.com/TheAppleFreak/moleculer-zod-validator) - A validator for the Moleculer microservice framework to allow the use of [Zod](https://zod.dev/). 37 | - 🔏 [asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi#defining-schemas) - A library that generates OpenAPI (Swagger) docs from Zod schemas. 38 | - 🪄 [hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) - Turn your OpenAPI specification into a beautiful TypeScript client. 39 | 40 | ## Install 41 | 42 | ```sh 43 | pnpm install 44 | ``` 45 | 46 | ## Usage 47 | 48 | ```sh 49 | # Copy env file 50 | cp .env.example .env 51 | pnpm dev 52 | ``` 53 | 54 | After starting, open the http://localhost:3000/ URL in your browser. 55 | On the welcome page you can test the generated services via API Gateway and check the nodes & services. 56 | ![https://gyazo.com/c8a8c8b05319504d36922458d9807db2.gif](https://gyazo.com/c8a8c8b05319504d36922458d9807db2.gif) 57 | 58 | ```sh 59 | pnpm cli --ns api 60 | ``` 61 | 62 | ![https://gyazo.com/235f710ab3fd906f80768261e793eb13](https://gyazo.com/235f710ab3fd906f80768261e793eb13.gif) 63 | 64 | In the terminal, try the following commands: 65 | 66 | - `nodes` - List all connected nodes. 67 | - `actions` - List all registered service actions. 68 | - `call greeter.hello` - Call the `greeter.hello` action. 69 | - `call greeter.welcome --username dunghd` - Call the `greeter.welcome` action with the `username` parameter. 70 | 71 | ![https://gyazo.com/3aca1c4e1992ad1c10da8060d7e21a6c.gif](https://gyazo.com/3aca1c4e1992ad1c10da8060d7e21a6c.gif) 72 | 73 | This project uses [hygen](http://www.hygen.io/) to generate code templates, saving you time and ensuring consistency across your codebase. 74 | 75 | ### Adding a New Service 76 | 77 | To add a new service to your project, use the following command: 78 | 79 | ```sh 80 | pnpm generate:service [service-name] 81 | ``` 82 | 83 | ### Adding a New Action to a Service 84 | 85 | To add a new action to an existing service, use the following command: 86 | 87 | ```sh 88 | pnpm generate:action [action-name] --service [service-name] 89 | ``` 90 | 91 | ### Generating CRUD Services 92 | 93 | To generate a service with Create, Read, Update, and Delete (CRUD) operations, use the following command: 94 | 95 | ```sh 96 | pnpm generate:crud [service-name] 97 | ``` 98 | 99 | ## API Documentation 100 | 101 | This template also reads your [JSDoc-annotated](https://github.com/Surnet/swagger-jsdoc/blob/v6/docs/README.md) source code and generates an OpenAPI (Swagger) specification. 102 | 103 | Run the following command to generate the Swagger documentation: 104 | 105 | ```sh 106 | pnpm generate:swagger 107 | ``` 108 | 109 | Open the http://localhost:3000/docs URL in your browser, you will see the Swagger UI as 110 | 111 | ![https://gyazo.com/a4fe2413414c94dde636a531eee1a4a0.gif](https://gyazo.com/a4fe2413414c94dde636a531eee1a4a0.gif) 112 | 113 | ## Run tests 114 | 115 | ```sh 116 | pnpm test 117 | ``` 118 | 119 | ## Deployment 120 | 121 | This template comes with two GitHub Actions that handle automatically deploying your app to production and staging environments. 122 | 123 | Prior to your first deployment, you'll need to do a few things: 124 | 125 | - [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/) 126 | 127 | - Sign up and log in to Fly 128 | 129 | ```sh 130 | fly auth signup 131 | ``` 132 | 133 | - Create two apps on Fly, one for staging and one for production: 134 | 135 | ```sh 136 | fly create moleculer-typescript 137 | fly create moleculer-typescript-staging 138 | ``` 139 | 140 | - Create a new [GitHub Repository](https://repo.new) 141 | 142 | - Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user settings on Fly and create a new [token](https://web.fly.io/user/personal_access_tokens/new), then add it to [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) with the name `FLY_API_TOKEN`. 143 | 144 | Now that every is set up you can commit and push your changes to your repo. Every commit to your `main` branch will trigger a deployment to your production environment, and every commit to your `dev` branch will trigger a deployment to your staging environment. 145 | 146 | ## GitHub Actions 147 | 148 | We use GitHub Actions for continuous integration and deployment. Anything that gets into the `main` branch will be deployed to production after running tests/build/etc. Anything in the `dev` branch will be deployed to staging. 149 | 150 | ## Useful links 151 | 152 | - Moleculer website: https://moleculer.services/ 153 | - Moleculer Documentation: https://moleculer.services/docs/0.14/ 154 | 155 | ## NPM scripts 156 | 157 | - `pnpm dev`: Start development mode (load all services locally with hot-reload & watch) 158 | - `pnpm start`: Start production mode (set `SERVICES` env variable to load certain services) 159 | - `pnpm cli`: Start a CLI and connect to production. Don't forget to set production namespace with `--ns` argument in script 160 | - `pnpm ci`: Run continuous test mode with watching 161 | - `pnpm test`: Run tests & generate coverage report 162 | - `pnpm dc:up`: Start the stack with Docker Compose 163 | - `pnpm dc:down`: Stop the stack with Docker Compose 164 | 165 | ## Pre-commit hooks 166 | 167 | This template uses [Pre-commit](https://pre-commit.com/) to run checks before you commit your code. This ensures that your code is formatted correctly and passes all tests before you push it to your repository. 168 | 169 | ```sh 170 | pre-commit install 171 | ``` 172 | 173 | To run the checks manually, use the following command: 174 | 175 | ```sh 176 | pre-commit run --all-files 177 | ``` 178 | 179 | ## Author 180 | 181 | 👤 **Dung Huynh** 182 | 183 | - Website: https://productsway.com/ 184 | - Twitter: [@jellydn](https://twitter.com/jellydn) 185 | - Github: [@jellydn](https://github.com/jellydn) 186 | 187 | ## Show your support 188 | 189 | [![Star History Chart](https://api.star-history.com/svg?repos=jellydn/moleculer-typescript-template&type=Date)](https://star-history.com/#jellydn/moleculer-typescript-template) 190 | 191 | Give a ⭐️ if this project helped you! 192 | 193 | [![kofi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/dunghd) 194 | [![paypal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/dunghd) 195 | [![buymeacoffee](https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/dunghd) 196 | -------------------------------------------------------------------------------- /_templates/action/new/hello.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= service %>/actions/<%= name %>.action.ts 3 | --- 4 | import type { Context, ServiceActionsSchema } from "moleculer"; 5 | 6 | /** 7 | * The <%= name %> action. 8 | * 9 | * @swagger 10 | * /welcome: 11 | * get: 12 | * summary: Returns a greeting and calculates the age in days. 13 | * parameters: 14 | * - in: query 15 | * name: name 16 | * schema: 17 | * type: string 18 | * required: true 19 | * description: The name to greet. 20 | * - in: query 21 | * name: age 22 | * schema: 23 | * type: number 24 | * required: true 25 | * description: The age of the person to calculate the days. 26 | * responses: 27 | * 200: 28 | * description: A greeting message and the age in days. 29 | * content: 30 | * text/plain: 31 | * schema: 32 | * type: string 33 | * example: "Hello John, you are 10950 days old!" 34 | */ 35 | const <%= name %>Action: ServiceActionsSchema = { 36 | rest: { 37 | method: "GET", 38 | path: "/welcome", 39 | }, 40 | params: { 41 | name: "string", 42 | age: "number", 43 | }, 44 | handler: <%= name %>Handler, 45 | }; 46 | 47 | /** 48 | * Handler for the <%= name %> action. 49 | */ 50 | function <%= name %>Handler( 51 | ctx: Context<{ 52 | name: string; 53 | age: number; 54 | }>, 55 | ): string { 56 | // Calculate the age in days 57 | const ageInDays = ctx.params.age * 365; 58 | 59 | return `Hello ${ctx.params.name}, you are ${ageInDays} days old!`; 60 | } 61 | 62 | export default <%= name %>Action; 63 | 64 | -------------------------------------------------------------------------------- /_templates/service/crud/create.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>/actions/create<%= h.capitalize(name) %>.action.ts 3 | --- 4 | 5 | import type { Context, ServiceActionsSchema } from 'moleculer'; 6 | 7 | /** 8 | * Handler for the create action. 9 | */ 10 | function createHandler( 11 | ctx: Context<{ 12 | <%= name %>: Record; 13 | }>, 14 | ) { 15 | return { 16 | message: 'Created successfully', 17 | data: ctx.params, 18 | }; 19 | } 20 | 21 | /** 22 | * The create project action. 23 | * 24 | * @swagger 25 | * /api/<%= name %>: 26 | * post: 27 | * summary: Create a new <%= name %>. 28 | * security: 29 | * - bearerAuth: [] 30 | * tags: 31 | * - <%= name %> 32 | * requestBody: 33 | * required: true 34 | * content: 35 | * application/json: 36 | * schema: 37 | * type: object 38 | * properties: 39 | * name: 40 | * type: string 41 | * description: The name to greet. 42 | * age: 43 | * type: number 44 | * description: The age of the person to calculate the days. 45 | * responses: 46 | * 200: 47 | * description: <%= name %> created successfully. 48 | */ 49 | const create<%= h.capitalize(name) %>Action: ServiceActionsSchema = { 50 | rest: { 51 | method: 'POST', 52 | path: '/', 53 | }, 54 | auth: true, 55 | permissions: [], 56 | params: { 57 | <%= name %>: { type: "object" } 58 | }, 59 | handler: createHandler, 60 | }; 61 | 62 | export default create<%= h.capitalize(name) %>Action; 63 | -------------------------------------------------------------------------------- /_templates/service/crud/delete.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>/actions/delete<%= h.capitalize(name) %>.action.ts 3 | --- 4 | 5 | import type { Context, ServiceActionsSchema } from 'moleculer'; 6 | 7 | /** 8 | * Handler for the delete action. 9 | */ 10 | function deleteHandler( 11 | ctx: Context<{ 12 | id: string; 13 | }>, 14 | ) { 15 | const { id } = ctx.params; 16 | return { 17 | id, 18 | }; 19 | } 20 | 21 | /** 22 | * The delete action. 23 | * 24 | * @swagger 25 | * /api/<%= name %>/{id}: 26 | * delete: 27 | * summary: Delete a <%= name %> by ID. 28 | * security: 29 | * - bearerAuth: [] 30 | * tags: 31 | * - <%= name %> 32 | * parameters: 33 | * - in: query 34 | * name: id 35 | * schema: 36 | * type: string 37 | * required: true 38 | * description: The ID of the <%= name %> to delete. 39 | * responses: 40 | * 200: 41 | * description: Delete <%= name %> successfully. 42 | */ 43 | const delete<%= h.capitalize(name) %>Action: ServiceActionsSchema = { 44 | rest: { 45 | method: 'DELETE', 46 | path: '/:id', 47 | }, 48 | auth: true, 49 | permissions: [], 50 | params: { 51 | id: 'string', 52 | }, 53 | handler: deleteHandler, 54 | }; 55 | 56 | export default delete<%= h.capitalize(name) %>Action; 57 | -------------------------------------------------------------------------------- /_templates/service/crud/list.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>/actions/list<%= h.capitalize(name) %>.action.ts 3 | --- 4 | 5 | import type { ServiceActionsSchema } from 'moleculer'; 6 | 7 | /** 8 | * Handler for the list action. 9 | */ 10 | function listHandler() { 11 | return []; 12 | } 13 | 14 | /** 15 | * The list action. 16 | * 17 | * @swagger 18 | * /api/<%= name %>: 19 | * get: 20 | * summary: Return a list of <%= name %>. 21 | * security: 22 | * - bearerAuth: [] 23 | * tags: 24 | * - <%= name %> 25 | * parameters: 26 | * - in: query 27 | * name: page 28 | * schema: 29 | * type: string 30 | * - in: query 31 | * name: limit 32 | * schema: 33 | * type: string 34 | * responses: 35 | * 200: 36 | * description: The <%= name %> list. 37 | */ 38 | const list<%= h.capitalize(name) %>Action: ServiceActionsSchema = { 39 | rest: { 40 | method: 'GET', 41 | path: '/', 42 | }, 43 | auth: true, 44 | permissions: [], 45 | params: { 46 | page: 'string', 47 | limit: 'string', 48 | }, 49 | handler: listHandler, 50 | }; 51 | 52 | export default list<%= h.capitalize(name) %>Action; 53 | -------------------------------------------------------------------------------- /_templates/service/crud/service.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>/<%= name %>.service.ts 3 | --- 4 | import type { ServiceSchema } from "moleculer"; 5 | 6 | import createAction from "./actions/create<%= h.capitalize(name) %>.action"; 7 | import editAction from "./actions/update<%= h.capitalize(name) %>.action"; 8 | import viewAction from "./actions/view<%= h.capitalize(name) %>.action"; 9 | import deleteAction from "./actions/delete<%= h.capitalize(name) %>.action"; 10 | import listAction from "./actions/list<%= h.capitalize(name) %>.action"; 11 | 12 | type ServiceSettings = { 13 | defaultName: string; 14 | }; 15 | 16 | /** 17 | * Define common components for the <%= name %> service. 18 | * 19 | * @swagger 20 | * components: 21 | * securitySchemes: 22 | * bearerAuth: 23 | * type: http 24 | * scheme: bearer 25 | * bearerFormat: JWT 26 | */ 27 | const <%= name %>Service: ServiceSchema = { 28 | name: "<%= name %>", 29 | 30 | /** 31 | * Settings 32 | */ 33 | settings: { 34 | defaultName: "Moleculer", 35 | }, 36 | 37 | /** 38 | * Dependencies 39 | */ 40 | dependencies: [], 41 | 42 | /** 43 | * Actions 44 | */ 45 | actions: { 46 | create: createAction, 47 | update: editAction, 48 | view: viewAction, 49 | delete: deleteAction, 50 | list: listAction, 51 | }, 52 | 53 | /** 54 | * Events 55 | */ 56 | events: {}, 57 | 58 | /** 59 | * Methods 60 | */ 61 | methods: { 62 | /** 63 | * A simple method example. 64 | * 65 | * @example 66 | * let upper = this.uppercase("John"); 67 | * // "JOHN" 68 | * */ 69 | uppercase(str: string): string { 70 | return str.toUpperCase(); 71 | }, 72 | }, 73 | 74 | /** 75 | * Service created lifecycle event handler 76 | */ 77 | created() { 78 | this.logger.info(`The ${this.name} service created.`); 79 | }, 80 | 81 | /** 82 | * Service started lifecycle event handler 83 | */ 84 | started() { 85 | this.logger.info(`The ${this.name} service started.`); 86 | }, 87 | 88 | /** 89 | * Service stopped lifecycle event handler 90 | */ 91 | stopped() { 92 | this.logger.info(`The ${this.name} service stopped.`); 93 | }, 94 | }; 95 | 96 | export default <%= name %>Service; 97 | 98 | -------------------------------------------------------------------------------- /_templates/service/crud/service.test.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: test/unit/<%= name %>.spec.ts 3 | --- 4 | import { Errors, ServiceBroker } from 'moleculer'; 5 | 6 | import TestService from '../../services/<%= name %>/<%= name %>.service'; 7 | 8 | const { ValidationError } = Errors; 9 | 10 | describe("Test '<%= name %>' service", () => { 11 | const broker = new ServiceBroker({ logger: false }); 12 | broker.createService(TestService); 13 | 14 | beforeAll(async () => broker.start()); 15 | afterAll(async () => broker.stop()); 16 | 17 | describe("Test '<%= name %>.create' action", () => { 18 | it("should return with 'create'", async () => { 19 | const response = await broker.call<{ message: string }, any>('<%= name %>.create', { 20 | <%= name %>: { 21 | location: 'Singapore', 22 | }, 23 | }); 24 | expect(response.message).toBe('Created successfully'); 25 | }); 26 | 27 | it('should reject an ValidationError', async () => { 28 | expect.assertions(1); 29 | try { 30 | await broker.call('<%= name %>.create'); 31 | } catch (error: unknown) { 32 | expect(error).toBeInstanceOf(ValidationError); 33 | } 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /_templates/service/crud/update.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>/actions/update<%= h.capitalize(name) %>.action.ts 3 | --- 4 | 5 | import type { Context, ServiceActionsSchema } from 'moleculer'; 6 | 7 | /** 8 | * Handler for the update action. 9 | */ 10 | function updateHandler( 11 | ctx: Context<{ 12 | id: string; 13 | }>, 14 | ) { 15 | const { id } = ctx.params; 16 | return { 17 | id, 18 | }; 19 | } 20 | 21 | /** 22 | * The update action. 23 | * 24 | * @swagger 25 | * /api/<%= name %>/{id}: 26 | * put: 27 | * summary: Update a <%= name %> by ID. 28 | * security: 29 | * - bearerAuth: [] 30 | * tags: 31 | * - <%= name %> 32 | * parameters: 33 | * - in: query 34 | * name: id 35 | * schema: 36 | * type: string 37 | * required: true 38 | * description: The ID of the <%= name %> to update. 39 | * responses: 40 | * 200: 41 | * description: Updated <%= name %> successfully. 42 | */ 43 | const update<%= h.capitalize(name) %>Action: ServiceActionsSchema = { 44 | rest: { 45 | method: 'PUT', 46 | path: '/:id', 47 | }, 48 | auth: true, 49 | permissions: [], 50 | params: { 51 | id: 'string', 52 | }, 53 | handler: updateHandler, 54 | }; 55 | 56 | export default update<%= h.capitalize(name) %>Action; 57 | -------------------------------------------------------------------------------- /_templates/service/crud/view.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>/actions/view<%= h.capitalize(name) %>.action.ts 3 | --- 4 | 5 | import type { Context, ServiceActionsSchema } from 'moleculer'; 6 | 7 | /** 8 | * Handler for the view action. 9 | */ 10 | function viewHandler( 11 | ctx: Context<{ 12 | id: string; 13 | }>, 14 | ) { 15 | const { id } = ctx.params; 16 | return { 17 | id, 18 | }; 19 | } 20 | 21 | /** 22 | * The view action. 23 | * 24 | * @swagger 25 | * /api/<%= name %>/{id}: 26 | * get: 27 | * summary: Return a <%= name %> by ID. 28 | * security: 29 | * - bearerAuth: [] 30 | * tags: 31 | * - <%= name %> 32 | * parameters: 33 | * - in: query 34 | * name: id 35 | * schema: 36 | * type: string 37 | * required: true 38 | * description: The ID of the <%= name %> to view. 39 | * responses: 40 | * 200: 41 | * description: The <%= name %> was found. 42 | * 404: 43 | * description: The <%= name %> was not found. 44 | */ 45 | const view<%= h.capitalize(name) %>Action: ServiceActionsSchema = { 46 | rest: { 47 | method: 'GET', 48 | path: '/:id', 49 | }, 50 | auth: true, 51 | permissions: [], 52 | params: { 53 | id: 'string', 54 | }, 55 | handler: viewHandler, 56 | }; 57 | 58 | export default view<%= h.capitalize(name) %>Action; 59 | -------------------------------------------------------------------------------- /_templates/service/new/hello.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: services/<%= name %>.service.ts 3 | --- 4 | import type { Context, Service, ServiceSchema } from "moleculer"; 5 | 6 | export type ActionHelloParams = { 7 | name: string; 8 | }; 9 | 10 | type ServiceSettings = { 11 | defaultName: string; 12 | }; 13 | 14 | type ServiceMethods = { 15 | uppercase(str: string): string; 16 | }; 17 | 18 | type ServiceThis = Service & ServiceMethods; 19 | 20 | const <%= name %>Service: ServiceSchema = { 21 | name: "<%= name %>", 22 | 23 | /** 24 | * Settings 25 | */ 26 | settings: { 27 | defaultName: "Moleculer", 28 | }, 29 | 30 | /** 31 | * Dependencies 32 | */ 33 | dependencies: [], 34 | 35 | /** 36 | * Actions 37 | */ 38 | actions: { 39 | /** 40 | * Say a 'Hello' action. 41 | * 42 | * @returns 43 | */ 44 | hello: { 45 | rest: { 46 | method: "GET", 47 | path: "/hello", 48 | }, 49 | handler(this: ServiceThis): string { 50 | return `Hello ${this.settings.defaultName}`; 51 | }, 52 | }, 53 | 54 | /** 55 | * Welcome, a username 56 | * 57 | * @param {String} name - User name 58 | */ 59 | welcome: { 60 | rest: "GET /welcome/:name", 61 | params: { 62 | name: "string", 63 | }, 64 | handler( 65 | this: ServiceThis, 66 | ctx: Context, 67 | ): string { 68 | return `Welcome, ${ctx.params.name}`; 69 | }, 70 | }, 71 | }, 72 | 73 | /** 74 | * Events 75 | */ 76 | events: {}, 77 | 78 | /** 79 | * Methods 80 | */ 81 | methods: { 82 | /** 83 | * A simple method example. 84 | * 85 | * @example 86 | * let upper = this.uppercase("John"); 87 | * // "JOHN" 88 | * */ 89 | uppercase(str: string): string { 90 | return str.toUpperCase(); 91 | }, 92 | }, 93 | 94 | /** 95 | * Service created lifecycle event handler 96 | */ 97 | created() { 98 | this.logger.info(`The ${this.name} service created.`); 99 | }, 100 | 101 | /** 102 | * Service started lifecycle event handler 103 | */ 104 | async started() { 105 | this.logger.info(`The ${this.name} service started.`); 106 | }, 107 | 108 | /** 109 | * Service stopped lifecycle event handler 110 | */ 111 | async stopped() { 112 | this.logger.info(`The ${this.name} service stopped.`); 113 | }, 114 | }; 115 | 116 | export default <%= name %>Service; 117 | 118 | -------------------------------------------------------------------------------- /_templates/service/new/hello.test.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: test/unit/services/<%= name %>.spec.ts 3 | --- 4 | import { Errors, ServiceBroker, type ServiceSchema } from "moleculer"; 5 | 6 | import TestService from "../../../services/<%= name %>.service"; 7 | 8 | const { ValidationError } = Errors; 9 | 10 | describe("Test '<%= name %>' service", () => { 11 | const broker = new ServiceBroker({ logger: false }); 12 | broker.createService(TestService as unknown as ServiceSchema); 13 | 14 | beforeAll(async () => broker.start()); 15 | afterAll(async () => broker.stop()); 16 | 17 | describe("Test '<%= name %>.hello' action", () => { 18 | it("should return with 'Hello Moleculer'", async () => { 19 | const response = await broker.call("<%= name %>.hello"); 20 | expect(response).toBe("Hello Moleculer"); 21 | }); 22 | }); 23 | 24 | describe("Test '<%= name %>.welcome' action", () => { 25 | it("should return with 'Welcome'", async () => { 26 | const response = await broker.call("<%= name %>.welcome", { 27 | name: "Adam", 28 | }); 29 | expect(response).toBe("Welcome, Adam"); 30 | }); 31 | 32 | it("should reject an ValidationError", async () => { 33 | expect.assertions(1); 34 | try { 35 | await broker.call("<%= name %>.welcome"); 36 | } catch (error: unknown) { 37 | expect(error).toBeInstanceOf(ValidationError); 38 | } 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "suspicious": { 11 | "noExplicitAny": "warn", 12 | "noCommentText": "warn", 13 | "noShadowRestrictedNames": "warn" 14 | }, 15 | "complexity": { 16 | "noForEach": "off" 17 | } 18 | } 19 | }, 20 | "formatter": { 21 | "enabled": true, 22 | "indentStyle": "space", 23 | "indentWidth": 4, 24 | "lineWidth": 100 25 | }, 26 | "json": { 27 | "formatter": { 28 | "indentWidth": 2 29 | } 30 | }, 31 | "vcs": { 32 | "enabled": true, 33 | "clientKind": "git", 34 | "useIgnoreFile": true, 35 | "defaultBranch": "main" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cli.ts: -------------------------------------------------------------------------------- 1 | // Usage: npx tsx cli.ts or pnpm run generate:swagger 2 | import { cli } from "cleye"; 3 | import swaggerJsdoc, { type OAS3Definition, type Options } from "swagger-jsdoc"; 4 | 5 | import { writeFileSync } from "node:fs"; 6 | import { join } from "node:path"; 7 | 8 | type SwaggerOptions = Options & { 9 | apiFolder?: string; 10 | schemaFolders?: string[]; 11 | definition: OAS3Definition; 12 | outputFile?: string; 13 | }; 14 | 15 | // Parse argv 16 | const argv = cli({ 17 | name: "apidoc-cli", 18 | 19 | parameters: [ 20 | "[title]", // Title is optional 21 | "[description]", // Description is optional 22 | ], 23 | 24 | // Define flags/options 25 | // Becomes available in .flags 26 | flags: { 27 | // Parses `--output` as a string 28 | output: { 29 | type: String, 30 | description: "Output file path", 31 | default: "public/docs/open-api.json", 32 | }, 33 | }, 34 | }); 35 | 36 | const defaultSwaggerOptions: SwaggerOptions = { 37 | definition: { 38 | openapi: "3.0.0", 39 | info: { 40 | title: argv._.title ?? "API Documentation", 41 | description: argv._.description ?? "Your awesome API Documentation generated by CLI.", 42 | termsOfService: "https://productsway.com/terms", 43 | version: "1.0", 44 | contact: { 45 | name: "Dung Huynh Duc", 46 | email: "dung@productsway.com", 47 | url: "https://productsway.com", 48 | }, 49 | }, 50 | }, 51 | }; 52 | 53 | function createSwaggerSpec({ 54 | apiFolder = "services", 55 | schemaFolders = [], 56 | ...swaggerOptions 57 | }: SwaggerOptions) { 58 | const scanFolders = [apiFolder, ...schemaFolders]; 59 | const apis = scanFolders.flatMap((folder) => { 60 | const apiDirectory = join(process.cwd(), folder); 61 | const publicDirectory = join(process.cwd(), "public"); 62 | const fileTypes = ["ts", "tsx", "jsx", "js", "json", "swagger.yaml"]; 63 | return [ 64 | ...fileTypes.map((fileType) => `${apiDirectory}/**/*.${fileType}`), 65 | // Support load static files from public directory 66 | ...["swagger.yaml", "json"].map((fileType) => `${publicDirectory}/**/*.${fileType}`), 67 | ]; 68 | }); 69 | 70 | const options = { 71 | ...swaggerOptions, 72 | apis, // Files containing annotations for swagger/openapi 73 | }; 74 | const spec = swaggerJsdoc(options); 75 | 76 | return spec; 77 | } 78 | 79 | const spec = createSwaggerSpec({ 80 | ...defaultSwaggerOptions, 81 | apiFolder: "services", 82 | }); 83 | 84 | writeFileSync(argv.flags.output, JSON.stringify(spec, null, 2)); 85 | console.log(`Swagger spec written to ${argv.flags.output}`); 86 | -------------------------------------------------------------------------------- /cspell-tool.txt: -------------------------------------------------------------------------------- 1 | cleye 2 | apidoc 3 | Huynh 4 | moleculer 5 | Moleculer 6 | traefik 7 | exposedbydefault 8 | LOGLEVEL 9 | dunghd 10 | Parameterusername 11 | SERVICEDIR 12 | RETRYPOLICY 13 | MYCACHE 14 | mycache 15 | Notepack 16 | retryable 17 | Zipkin 18 | logdy 19 | tsup 20 | hygen 21 | asteasolutions 22 | defaultsdeep 23 | biomejs 24 | flydotio 25 | dotenv 26 | pino 27 | vitest 28 | vite 29 | ampproject 30 | apidevtools 31 | bcoe 32 | esbuild 33 | loong 34 | riscv 35 | sunos 36 | fastify 37 | hapi 38 | isaacs 39 | istanbuljs 40 | jridgewell 41 | jsdevtools 42 | nodelib 43 | pkgjs 44 | rollup 45 | magicast 46 | monocart 47 | amqplib 48 | avsc 49 | cbor 50 | ioredis 51 | mqtt 52 | msgpack 53 | notepack 54 | protobufjs 55 | redlock 56 | jiti 57 | iojs 58 | lightningcss 59 | sugarss 60 | jsdom 61 | bufferutil 62 | picocolors 63 | yargs 64 | hoek 65 | typebox 66 | scandir 67 | fastq 68 | undici 69 | tinyrainbow 70 | pathe 71 | tinyspy 72 | fflate 73 | sirv 74 | estree 75 | picomatch 76 | sprintf 77 | depd 78 | unpipe 79 | chokidar 80 | confbox 81 | defu 82 | giget 83 | mlly 84 | ohash 85 | pathval 86 | anymatch 87 | readdirp 88 | fsevents 89 | consola 90 | memoizee 91 | gopd 92 | esutils 93 | esniff 94 | encodeurl 95 | etag 96 | finalhandler 97 | parseurl 98 | setprototypeof 99 | reusify 100 | minimatch 101 | asynckit 102 | jsonfile 103 | universalify 104 | minipass 105 | hasown 106 | citty 107 | nypm 108 | jackspeak 109 | realpath 110 | minimist 111 | wordwrap 112 | toidentifier 113 | degit 114 | execa 115 | wrappy 116 | extglob 117 | cliui 118 | parseargs 119 | filelist 120 | topo 121 | argparse 122 | esprima 123 | yallist 124 | rimraf 125 | kleur 126 | clui 127 | isstream 128 | eventemitter 129 | ipaddr 130 | thenify 131 | nkeys 132 | tweetnacl 133 | whatwg 134 | memorystream 135 | pidtree 136 | wcwidth 137 | yocto 138 | colorette 139 | dateformat 140 | joycon 141 | lilconfig 142 | destr 143 | eabi 144 | gnueabihf 145 | musleabihf 146 | msvc 147 | microtask 148 | mrmime 149 | totalist 150 | fullwidth 151 | globby 152 | eastasianwidth 153 | mergewith 154 | chownr 155 | minizlib 156 | mkdirp 157 | typer 158 | tinybench 159 | tinypool 160 | axios 161 | webidl 162 | sortby 163 | isexe 164 | siginfo 165 | stackback 166 | isequal 167 | jellydn 168 | Logdy 169 | signup 170 | kofi 171 | buymeacoffee 172 | automerge 173 | dtos 174 | codepaths -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "language": "en", 5 | "globRoot": ".", 6 | "dictionaryDefinitions": [ 7 | { 8 | "name": "cspell-tool", 9 | "path": "./cspell-tool.txt", 10 | "addWords": true 11 | } 12 | ], 13 | "dictionaries": ["cspell-tool"], 14 | "ignorePaths": ["node_modules", "dist", "build", "/cspell-tool.txt"] 15 | } 16 | -------------------------------------------------------------------------------- /deta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-typescript-template", 3 | "description": "My Moleculer-based microservices project", 4 | "runtime": "nodejs14.x", 5 | "env": [] 6 | } 7 | -------------------------------------------------------------------------------- /docker-compose.env: -------------------------------------------------------------------------------- 1 | NAMESPACE= 2 | LOGGER=true 3 | LOGLEVEL=info 4 | SERVICEDIR=dist/services 5 | 6 | TRANSPORTER=nats://nats:4222 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | api: 5 | build: 6 | context: . 7 | image: moleculer-typescript 8 | env_file: docker-compose.env 9 | environment: 10 | SERVICES: api 11 | PORT: 3000 12 | depends_on: 13 | - nats 14 | labels: 15 | - "traefik.enable=true" 16 | - "traefik.http.routers.api-gw.rule=PathPrefix(`/`)" 17 | - "traefik.http.services.api-gw.loadbalancer.server.port=3000" 18 | networks: 19 | - internal 20 | 21 | greeter: 22 | build: 23 | context: . 24 | image: moleculer-typescript 25 | env_file: docker-compose.env 26 | environment: 27 | SERVICES: greeter 28 | depends_on: 29 | - nats 30 | networks: 31 | - internal 32 | 33 | product: 34 | build: 35 | context: . 36 | image: moleculer-typescript 37 | env_file: docker-compose.env 38 | environment: 39 | SERVICES: product 40 | depends_on: 41 | - nats 42 | networks: 43 | - internal 44 | 45 | nats: 46 | image: nats:2 47 | networks: 48 | - internal 49 | 50 | traefik: 51 | image: traefik:v3.3 52 | command: 53 | - "--api.insecure=true" # Don't do that in production! 54 | - "--providers.docker=true" 55 | - "--providers.docker.exposedbydefault=false" 56 | ports: 57 | - 3000:80 58 | - 3001:8080 59 | volumes: 60 | - /var/run/docker.sock:/var/run/docker.sock:ro 61 | networks: 62 | - internal 63 | - default 64 | 65 | networks: 66 | internal: 67 | 68 | volumes: 69 | data: 70 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Apollo Server (GraphQL) 4 | 5 | ```sh 6 | npx ts-node apollo-server 7 | ``` 8 | 9 | ## Express 10 | 11 | ```sh 12 | npx ts-node express 13 | ``` 14 | 15 | ## Socket.io 16 | 17 | ```sh 18 | npx ts-node socket 19 | serve socket/public -p 3001 20 | ``` 21 | -------------------------------------------------------------------------------- /examples/express/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "node:http"; 2 | import { resolve } from "node:path"; 3 | import express from "express"; 4 | import defaultsDeep from "lodash/defaultsDeep"; 5 | import { ServiceBroker, type ServiceSchema } from "moleculer"; 6 | 7 | import config from "../../moleculer.config"; 8 | import apiService from "../../services/api.service"; 9 | 10 | // Create broker 11 | const broker = new ServiceBroker(config); 12 | 13 | // Load other services 14 | const enableServices = ["greeter"]; 15 | const folder = __dirname.includes("dist") 16 | ? resolve(__dirname.substring(0, __dirname.indexOf("dist")), "dist") 17 | : resolve(__dirname, "dist"); 18 | enableServices.forEach((serviceName) => 19 | broker.loadServices(folder, `**/${serviceName}.service.js`), 20 | ); 21 | // Load API Gateway 22 | const svc = broker.createService( 23 | defaultsDeep(apiService, { 24 | settings: { 25 | server: false, 26 | }, 27 | }) as ServiceSchema, 28 | ); 29 | 30 | // Create express and HTTP server 31 | const app = express(); 32 | const httpServer = createServer(app); 33 | 34 | // Use ApiGateway as middleware 35 | app.use("/", svc.express()); 36 | 37 | async function main() { 38 | // Now that our HTTP server is fully set up, actually listen. 39 | httpServer.listen(Number(process.env?.PORT ?? 3000)); 40 | 41 | // Start server 42 | await broker.start(); 43 | } 44 | 45 | main().catch(console.error); 46 | -------------------------------------------------------------------------------- /examples/socket/index.ts: -------------------------------------------------------------------------------- 1 | import type { ServiceSchema } from "moleculer"; 2 | import { ServiceBroker } from "moleculer"; 3 | import SocketIOService from "moleculer-io"; 4 | import ApiService from "moleculer-web"; 5 | 6 | import config from "../../moleculer.config"; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker(config); 10 | broker.createService({ 11 | name: "gateway", 12 | mixins: [ApiService, SocketIOService as ServiceSchema], // Should after moleculer-web 13 | settings: { 14 | // Exposed port 15 | port: Number(process.env?.PORT ?? 3000), 16 | 17 | // Exposed IP 18 | ip: process.env?.SERVER_HOSTNAME ?? "0.0.0.0", 19 | 20 | cors: { 21 | origin: "*", 22 | allowedHeaders: [], 23 | exposedHeaders: [], 24 | credentials: false, 25 | maxAge: 3600, 26 | }, 27 | }, 28 | }); 29 | 30 | // Math service 31 | broker.createService({ 32 | name: "math", 33 | actions: { 34 | add(ctx) { 35 | return Number(ctx.params.a) + Number(ctx.params.b); 36 | }, 37 | }, 38 | }); 39 | 40 | broker 41 | .start() 42 | .then(() => broker.repl()) 43 | .catch(console.error); 44 | -------------------------------------------------------------------------------- /examples/socket/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello Moleculer IO

9 | 10 | 11 |
12 |

13 | 		
14 |
15 |
16 | 17 | 18 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/web/client.ts: -------------------------------------------------------------------------------- 1 | // Usage: npx tsx client.ts | npx pino-pretty 2 | import { MoleculerApi } from "../../generated/sdk"; 3 | import { logger } from "../../logger"; 4 | 5 | // API client with fetch 6 | const apiClient = new MoleculerApi({ 7 | BASE: "http://localhost:4567", 8 | }); 9 | 10 | logger.level = "debug"; 11 | 12 | async function main() { 13 | logger.debug("Client started"); 14 | try { 15 | const hello = await apiClient.greeter.getApiGreeterHello(); 16 | logger.info("%s", hello); 17 | 18 | const welcome = await apiClient.greeter.getApiGreeterWelcome({ username: "IT Man" }); 19 | logger.info(welcome); 20 | 21 | const item = await apiClient.product.postApiProductCart({ 22 | requestBody: { 23 | name: "Iphone", 24 | qty: 1, 25 | }, 26 | }); 27 | logger.info(item); 28 | } catch (err) { 29 | logger.error(err); 30 | } 31 | } 32 | 33 | main() 34 | .then(() => process.exit(0)) 35 | .catch((error) => { 36 | logger.error(error); 37 | process.exit(1); 38 | }); 39 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for moleculer-typescript-template on 2024-04-06T16:26:54+08:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'moleculer-typescript-template' 7 | primary_region = 'sin' 8 | 9 | [build] 10 | 11 | [env] 12 | LOGGER = "true" 13 | LOGLEVEL = "info" 14 | NAMESPACE = "api" 15 | NODE_ENV = "production" 16 | PORT = "8080" 17 | SERVER_HOSTNAME = "0.0.0.0" 18 | TRANSPORTER = "TCP" 19 | 20 | [http_service] 21 | internal_port = 8080 22 | force_https = true 23 | auto_stop_machines = true 24 | auto_start_machines = true 25 | min_machines_running = 0 26 | processes = ['app'] 27 | 28 | [checks] 29 | [checks.api_health_check] 30 | grace_period = "30s" 31 | interval = "15s" 32 | method = "get" 33 | path = "/api/health/check" 34 | port = 8080 35 | timeout = "10s" 36 | type = "http" 37 | 38 | [[vm]] 39 | size = 'shared-cpu-1x' 40 | -------------------------------------------------------------------------------- /k8s.yaml: -------------------------------------------------------------------------------- 1 | ######################################################### 2 | # Common Environment variables ConfigMap 3 | ######################################################### 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: common-env 8 | data: 9 | NAMESPACE: "" 10 | LOGLEVEL: info 11 | SERVICEDIR: services 12 | TRANSPORTER: nats://nats:4222 13 | 14 | --- 15 | ######################################################### 16 | # Service for Moleculer API Gateway service 17 | ######################################################### 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: api 22 | spec: 23 | selector: 24 | app: api 25 | ports: 26 | - port: 3000 27 | targetPort: 3000 28 | 29 | --- 30 | ######################################################### 31 | # Ingress for Moleculer API Gateway 32 | ######################################################### 33 | apiVersion: networking.k8s.io/v1 34 | kind: Ingress 35 | metadata: 36 | name: ingress 37 | spec: 38 | rules: 39 | - host: moleculer-typescript.local 40 | http: 41 | paths: 42 | - path: / 43 | pathType: Prefix 44 | backend: 45 | service: 46 | name: api 47 | port: 48 | number: 3000 49 | --- 50 | ######################################################### 51 | # API Gateway service 52 | ######################################################### 53 | apiVersion: apps/v1 54 | kind: Deployment 55 | metadata: 56 | name: api 57 | spec: 58 | selector: 59 | matchLabels: 60 | app: api 61 | replicas: 2 62 | template: 63 | metadata: 64 | labels: 65 | app: api 66 | spec: 67 | containers: 68 | - name: api 69 | image: moleculer-typescript:latest 70 | imagePullPolicy: IfNotPresent 71 | envFrom: 72 | - configMapRef: 73 | name: common-env 74 | env: 75 | --- 76 | ######################################################### 77 | # Greeter service 78 | ######################################################### 79 | apiVersion: apps/v1 80 | kind: Deployment 81 | metadata: 82 | name: greeter 83 | spec: 84 | selector: 85 | matchLabels: 86 | app: greeter 87 | replicas: 2 88 | template: 89 | metadata: 90 | labels: 91 | app: greeter 92 | spec: 93 | containers: 94 | - name: greeter 95 | image: moleculer-typescript:latest 96 | imagePullPolicy: IfNotPresent 97 | 98 | envFrom: 99 | - configMapRef: 100 | name: common-env 101 | env: 102 | - name: SERVICES 103 | value: greeter 104 | 105 | --- 106 | ######################################################### 107 | # NATS transporter service 108 | ######################################################### 109 | apiVersion: v1 110 | kind: Service 111 | metadata: 112 | name: nats 113 | spec: 114 | selector: 115 | app: nats 116 | ports: 117 | - port: 4222 118 | name: nats 119 | targetPort: 4222 120 | 121 | --- 122 | ######################################################### 123 | # NATS transporter 124 | ######################################################### 125 | apiVersion: apps/v1 126 | kind: Deployment 127 | metadata: 128 | name: nats 129 | spec: 130 | selector: 131 | matchLabels: 132 | app: nats 133 | replicas: 1 134 | strategy: 135 | type: Recreate 136 | template: 137 | metadata: 138 | labels: 139 | app: nats 140 | spec: 141 | containers: 142 | - name: nats 143 | image: nats 144 | ports: 145 | - containerPort: 4222 146 | name: nats 147 | -------------------------------------------------------------------------------- /logger.ts: -------------------------------------------------------------------------------- 1 | import pino, { stdTimeFunctions } from "pino"; 2 | 3 | // Usage: logger[logLevel]([mergingObject],[message]) 4 | export const logger = pino({ 5 | level: process.env.LOGLEVEL ?? "info", 6 | timestamp: stdTimeFunctions.isoTime, 7 | }); 8 | -------------------------------------------------------------------------------- /moleculer.config.ts: -------------------------------------------------------------------------------- 1 | import type { BrokerOptions } from "moleculer"; 2 | 3 | import { stdTimeFunctions } from "pino"; 4 | 5 | /** 6 | * Moleculer ServiceBroker configuration file 7 | * 8 | * More info about options: 9 | * https://moleculer.services/docs/0.14/configuration.html 10 | * 11 | * 12 | * Overwriting options in production: 13 | * ================================ 14 | * You can overwrite any option with environment variables. 15 | * For example to overwrite the "logLevel" value, use `LOGLEVEL=warn` env var. 16 | * To overwrite a nested parameter, e.g. retryPolicy.retries, use `RETRYPOLICY_RETRIES=10` env var. 17 | * 18 | * To overwrite broker’s deeply nested default options, which are not presented in "moleculer.config.js", 19 | * use the `MOL_` prefix and double underscore `__` for nested properties in .env file. 20 | * For example, to set the cacher prefix to `MYCACHE`, you should declare an env var as `MOL_CACHER__OPTIONS__PREFIX=mycache`. 21 | * It will set this: 22 | * { 23 | * cacher: { 24 | * options: { 25 | * prefix: "mycache" 26 | * } 27 | * } 28 | * } 29 | */ 30 | const config: BrokerOptions = { 31 | // Namespace of nodes to segment your nodes on the same network. 32 | namespace: "api", 33 | // Unique node identifier. Must be unique in a namespace. 34 | nodeID: `api-${Math.random().toString(36).slice(2, 15)}${Math.random() 35 | .toString(36) 36 | .slice(2, 15)}`, 37 | // Custom metadata store. Store here what you want. Accessing: `this.broker.metadata` 38 | metadata: {}, 39 | 40 | // Enable/disable logging or use custom logger. More info: https://moleculer.services/docs/0.14/logging.html 41 | // Available logger types: "Console", "File", "Pino", "Winston", "Bunyan", "debug", "Log4js", "Datadog" 42 | logger: { 43 | // Note: Change to Console if you want to see the logger output 44 | type: "Pino", 45 | options: { 46 | pino: { 47 | // More info: http://getpino.io/#/docs/api?id=options-object 48 | options: { 49 | timestamp: stdTimeFunctions.isoTime, 50 | }, 51 | }, 52 | }, 53 | }, 54 | // Default log level for built-in console logger. It can be overwritten in logger options above. 55 | // Available values: trace, debug, info, warn, error, fatal 56 | logLevel: "info", 57 | 58 | // Define transporter. 59 | // More info: https://moleculer.services/docs/0.14/networking.html 60 | // Note: During the development, you don't need to define it because all services will be loaded locally. 61 | // In production you can set it via `TRANSPORTER=nats://localhost:4222` environment variable. 62 | transporter: "NATS", // "NATS" 63 | 64 | // Define a cacher. 65 | // More info: https://moleculer.services/docs/0.14/caching.html 66 | // cacher: null, 67 | 68 | // Define a serializer. 69 | // Available values: "JSON", "Avro", "ProtoBuf", "MsgPack", "Notepack", "Thrift". 70 | // More info: https://moleculer.services/docs/0.14/networking.html#Serialization 71 | serializer: "JSON", 72 | 73 | // Number of milliseconds to wait before reject a request with a RequestTimeout error. Disabled: 0 74 | requestTimeout: 10 * 1000, 75 | 76 | // Retry policy settings. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Retry 77 | retryPolicy: { 78 | // Enable feature 79 | enabled: false, 80 | // Count of retries 81 | retries: 5, 82 | // First delay in milliseconds. 83 | delay: 100, 84 | // Maximum delay in milliseconds. 85 | maxDelay: 1000, 86 | // Backoff factor for delay. 2 means exponential backoff. 87 | factor: 2, 88 | // A function to check failed requests. 89 | check: (error: Error & { retryable?: boolean }) => error && Boolean(error.retryable), 90 | }, 91 | 92 | // Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection) 93 | maxCallLevel: 100, 94 | 95 | // Number of seconds to send heartbeat packet to other nodes. 96 | heartbeatInterval: 10, 97 | // Number of seconds to wait before setting node to unavailable status. 98 | heartbeatTimeout: 30, 99 | 100 | // Cloning the params of context if enabled. High performance impact, use it with caution! 101 | contextParamsCloning: false, 102 | 103 | // Tracking requests and waiting for running requests before shutting down. More info: https://moleculer.services/docs/0.14/context.html#Context-tracking 104 | tracking: { 105 | // Enable feature 106 | enabled: false, 107 | // Number of milliseconds to wait before shooting down the process. 108 | shutdownTimeout: 5000, 109 | }, 110 | 111 | // Disable built-in request & emit balancer. (Transporter must support it, as well.). More info: https://moleculer.services/docs/0.14/networking.html#Disabled-balancer 112 | disableBalancer: false, 113 | 114 | // Settings of Service Registry. More info: https://moleculer.services/docs/0.14/registry.html 115 | registry: { 116 | // Define balancing strategy. More info: https://moleculer.services/docs/0.14/balancing.html 117 | // Available values: "RoundRobin", "Random", "CpuUsage", "Latency", "Shard" 118 | strategy: "RoundRobin", 119 | // Enable local action call preferring. Always call the local action instance if available. 120 | preferLocal: true, 121 | }, 122 | 123 | // Settings of Circuit Breaker. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Circuit-Breaker 124 | circuitBreaker: { 125 | // Enable feature 126 | enabled: false, 127 | // Threshold value. 0.5 means that 50% should be failed for tripping. 128 | threshold: 0.5, 129 | // Minimum request count. Below it, CB does not trip. 130 | minRequestCount: 20, 131 | // Number of seconds for time window. 132 | windowTime: 60, 133 | // Number of milliseconds to switch from open to half-open state 134 | halfOpenTime: 10 * 1000, 135 | // A function to check failed requests. 136 | // @ts-expect-error Property 'code' does not exist on type 'Error'.ts(2339) 137 | check: (error) => error && error.code >= 500, 138 | }, 139 | 140 | // Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead 141 | bulkhead: { 142 | // Enable feature. 143 | enabled: false, 144 | // Maximum concurrent executions. 145 | concurrency: 10, 146 | // Maximum size of queue 147 | maxQueueSize: 100, 148 | }, 149 | 150 | // Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html 151 | validator: true, 152 | 153 | // ErrorHandler: null, 154 | 155 | // Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html 156 | metrics: { 157 | enabled: false, 158 | // Available built-in reporters: "Console", "CSV", "Event", "Prometheus", "Datadog", "StatsD" 159 | reporter: { 160 | type: "Console", 161 | options: { 162 | // Custom logger 163 | logger: null, 164 | // Using colors 165 | colors: true, 166 | // Width of row 167 | width: 100, 168 | // Gauge width in the row 169 | gaugeWidth: 40, 170 | }, 171 | }, 172 | }, 173 | 174 | // Enable built-in tracing function. More info: https://moleculer.services/docs/0.14/tracing.html 175 | tracing: { 176 | enabled: true, 177 | // Available built-in exporters: "Console", "Datadog", "Event", "EventLegacy", "Jaeger", "Zipkin" 178 | exporter: [ 179 | { 180 | type: "Event", 181 | options: { 182 | // Name of event 183 | eventName: "$tracing.spans", 184 | // Send event when a span started 185 | sendStartSpan: false, 186 | // Send event when a span finished 187 | sendFinishSpan: true, 188 | // Broadcast or emit event 189 | broadcast: false, 190 | // Event groups 191 | groups: null, 192 | // Sending time interval in seconds 193 | interval: 5, 194 | // Custom span object converter before sending 195 | spanConverter: null, 196 | // Default tags. They will be added into all span tags. 197 | defaultTags: null, 198 | }, 199 | }, 200 | ], 201 | }, 202 | 203 | // Register custom middlewares 204 | middlewares: [], 205 | 206 | // Register custom REPL commands. 207 | // replCommands: null, 208 | 209 | // Called after broker created. 210 | created(broker) { 211 | broker.logger.warn("[%s] Broker created!", broker.namespace); 212 | }, 213 | 214 | // Called after broker started. 215 | async started(broker) { 216 | broker.logger.warn("[%s] Broker started!", broker.namespace); 217 | }, 218 | 219 | // Called after broker stopped. 220 | async stopped(broker) { 221 | broker.logger.warn("[%s] Broker stopped!", broker.namespace); 222 | }, 223 | }; 224 | 225 | export default config; 226 | -------------------------------------------------------------------------------- /openapi-ts.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@hey-api/openapi-ts"; 2 | 3 | // Refer to https://heyapi.vercel.app/openapi-ts/migrating.html#openapi-typescript-codegen 4 | export default defineConfig({ 5 | input: "public/docs/open-api.json", 6 | output: "generated/sdk", 7 | name: "MoleculerApi", 8 | useOptions: true, 9 | client: "legacy/fetch", 10 | }); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-typescript-template", 3 | "version": "0.3.0", 4 | "description": "My Moleculer-based microservices project", 5 | "keywords": ["microservices", "moleculer", "moleculer typescript template"], 6 | "license": "MIT", 7 | "author": "Huynh Duc Dung", 8 | "scripts": { 9 | "dev": "run-p -l dev:*", 10 | "dev:api": "npx tsx watch ./server.ts | npx pino-pretty", 11 | "logdy": "npx tsx watch ./server.ts | logdy --no-analytics --port 4567", 12 | "dev:swagger": "wait-on -c ./wait-on.config.cjs -l && npm run generate:swagger", 13 | "start": "npx tsx ./server.ts", 14 | "cli": "NODE_OPTIONS='--import tsx' moleculer-connect --env --config moleculer.config.ts --transporter NATS", 15 | "ci": "biome ci .", 16 | "test:ci": "vitest --watch", 17 | "test": "vitest --run", 18 | "test:coverage": "vitest --coverage", 19 | "test:ui": "vitest --ui", 20 | "build": "tsup --env.NODE_ENV production", 21 | "format": "biome format . --write", 22 | "check": "biome check --write .", 23 | "lint": "biome lint .", 24 | "build:watch": "tsc --watch --incremental", 25 | "dc:up": "docker compose up --build -d", 26 | "dc:logs": "docker compose logs -f", 27 | "dc:down": "docker compose down", 28 | "generate:service": "hygen service new --name", 29 | "generate:crud": "hygen service crud --name", 30 | "generate:action": "hygen action new --name", 31 | "generate:swagger": "npx tsx cli.ts", 32 | "generate:sdk": "openapi-ts", 33 | "typecheck": "npm run generate:sdk && tsc -b" 34 | }, 35 | "dependencies": { 36 | "@asteasolutions/zod-to-openapi": "7.3.0", 37 | "dotenv": "16.5.0", 38 | "helmet": "8.1.0", 39 | "lodash.defaultsdeep": "4.6.1", 40 | "moleculer": "0.14.35", 41 | "moleculer-web": "0.10.8", 42 | "moleculer-zod-validator": "3.3.1", 43 | "nats": "2.29.3", 44 | "openapi-types": "12.1.3", 45 | "pino": "9.6.0", 46 | "swagger-jsdoc": "6.2.8", 47 | "yaml": "2.7.1", 48 | "zod": "3.24.3" 49 | }, 50 | "devDependencies": { 51 | "@biomejs/biome": "1.9.4", 52 | "@flydotio/dockerfile": "0.7.10", 53 | "@hey-api/openapi-ts": "0.66.6", 54 | "@types/express": "5.0.1", 55 | "@types/jest": "29.5.14", 56 | "@types/lodash": "4.17.16", 57 | "@types/node": "22.15.2", 58 | "@types/swagger-jsdoc": "6.0.4", 59 | "@types/ws": "8.18.1", 60 | "@vitest/ui": "3.1.2", 61 | "c8": "10.1.3", 62 | "cleye": "1.3.4", 63 | "express": "5.1.0", 64 | "graphql": "16.11.0", 65 | "graphql-ws": "6.0.4", 66 | "hygen": "6.2.11", 67 | "moleculer-connect": "0.2.2", 68 | "moleculer-io": "2.2.0", 69 | "moleculer-repl": "0.7.4", 70 | "npm-run-all2": "7.0.2", 71 | "pino-pretty": "13.0.0", 72 | "sort-package-json": "3.0.0", 73 | "tsup": "8.4.0", 74 | "tsx": "4.19.3", 75 | "typescript": "5.8.3", 76 | "vite": "6.3.3", 77 | "vitest": "3.1.2", 78 | "wait-on": "8.0.3", 79 | "ws": "8.18.1" 80 | }, 81 | "packageManager": "pnpm@10.9.0", 82 | "engines": { 83 | "node": ">= 18.17.x" 84 | }, 85 | "pnpm": { 86 | "onlyBuiltDependencies": ["esbuild"] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /public/docs/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | API Documentation 10 | 11 | 12 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/docs/open-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "API Documentation", 5 | "description": "Your awesome API Documentation generated by CLI.", 6 | "termsOfService": "https://productsway.com/terms", 7 | "version": "1.0", 8 | "contact": { 9 | "name": "Dung Huynh Duc", 10 | "email": "dung@productsway.com", 11 | "url": "https://productsway.com" 12 | } 13 | }, 14 | "paths": { 15 | "/api/greeter/hello": { 16 | "get": { 17 | "description": "Returns the Hello Moleculer", 18 | "tags": ["greeter"], 19 | "responses": { 20 | "200": { 21 | "description": "Hello Moleculer", 22 | "content": { 23 | "text/plain": { 24 | "schema": { 25 | "type": "string", 26 | "example": "Hello Moleculer" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "/api/greeter/welcome": { 35 | "get": { 36 | "description": "Returns Welcome, a username", 37 | "tags": ["greeter"], 38 | "parameters": [ 39 | { 40 | "$ref": "#/components/parameters/username", 41 | "in": "query" 42 | } 43 | ], 44 | "responses": { 45 | "200": { 46 | "description": "Welcome, a username", 47 | "content": { 48 | "text/plain": { 49 | "schema": { 50 | "$ref": "#/components/schemas/welcomeResponseDTO" 51 | } 52 | } 53 | } 54 | }, 55 | "422": { 56 | "description": "Invalid username" 57 | } 58 | } 59 | } 60 | }, 61 | "/api/product/cart": { 62 | "post": { 63 | "description": "Add a product to the cart", 64 | "tags": ["product"], 65 | "requestBody": { 66 | "required": true, 67 | "content": { 68 | "application/json": { 69 | "schema": { 70 | "$ref": "#/components/schemas/addCartDTO" 71 | } 72 | } 73 | } 74 | }, 75 | "responses": { 76 | "200": { 77 | "description": "Product added to cart", 78 | "content": { 79 | "application/json": { 80 | "schema": { 81 | "$ref": "#/components/schemas/addCartResponseDTO" 82 | } 83 | } 84 | } 85 | }, 86 | "422": { 87 | "description": "Validation error" 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "components": { 94 | "schemas": { 95 | "welcomeResponseDTO": { 96 | "type": "string", 97 | "example": "Welcome, dunghd" 98 | }, 99 | "addCartDTO": { 100 | "type": "object", 101 | "properties": { 102 | "name": { 103 | "type": "string", 104 | "description": "Name of the product", 105 | "example": "Iphone" 106 | }, 107 | "qty": { 108 | "type": "number", 109 | "description": "Quantity of the product", 110 | "example": 1 111 | }, 112 | "price": { 113 | "type": "number", 114 | "description": "Price of the product", 115 | "example": 1000 116 | }, 117 | "billing": { 118 | "type": "object", 119 | "properties": { 120 | "address": { 121 | "type": "string" 122 | }, 123 | "city": { 124 | "type": "string" 125 | }, 126 | "zip": { 127 | "type": "number" 128 | }, 129 | "country": { 130 | "type": "string" 131 | } 132 | }, 133 | "required": ["city", "zip", "country"] 134 | } 135 | }, 136 | "required": ["name", "qty"], 137 | "description": "Add cart DTO" 138 | }, 139 | "addCartResponseDTO": { 140 | "type": "object", 141 | "properties": { 142 | "success": { 143 | "type": "boolean", 144 | "description": "Success flag", 145 | "example": true 146 | }, 147 | "message": { 148 | "type": "string", 149 | "description": "Message", 150 | "example": "Product added to cart" 151 | } 152 | }, 153 | "required": ["success", "message"], 154 | "description": "Add cart response DTO" 155 | } 156 | }, 157 | "parameters": { 158 | "username": { 159 | "name": "username", 160 | "in": "query", 161 | "description": "User name", 162 | "required": true, 163 | "schema": { 164 | "type": "string", 165 | "minLength": 6, 166 | "maxLength": 25 167 | } 168 | } 169 | } 170 | }, 171 | "tags": [] 172 | } 173 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | moleculer-typescript - Moleculer Microservices Project 10 | 14 | 18 | 23 | 24 | 371 | 372 | 373 | 374 |
375 |
376 | 381 | 392 |
393 | 394 |
395 |
396 |
397 |

398 | Welcome to your Moleculer microservices project! 399 |

400 |

401 | Check out the 402 | Moleculer documentation 407 | to learn how to customize this project. 408 |

409 |
410 |
411 | 412 | 467 | 468 |
469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 500 | 508 | 509 | 510 |
Node IDTypeVersionIPHostnameStatusCPU
{{ node.id }}{{ node.client.type }}{{ node.client.version }}{{ node.ipList[0] }}{{ node.hostname }} 492 |
496 | {{ node.available ? "Online": "Offline" 497 | }} 498 |
499 |
501 |
505 | {{ node.cpu != null ? 506 | Number(node.cpu).toFixed(0) + '%' : '-' }} 507 |
511 |
512 |
513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 588 | 589 |
Service/Action nameRESTParametersInstancesStatus
590 |
591 |
592 | 593 | 624 |
625 | 904 | 905 | 906 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "group:allNonMajor", ":pinAllExceptPeerDependencies"], 4 | "lockFileMaintenance": { 5 | "enabled": true 6 | }, 7 | "automerge": true, 8 | "major": { 9 | "automerge": false 10 | }, 11 | "ignoreDeps": ["moleculer-repl"] 12 | } 13 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import os from "node:os"; 2 | import { config } from "dotenv"; 3 | import defaultsDeep from "lodash/defaultsDeep"; 4 | import { type BrokerOptions, type LogLevels, ServiceBroker } from "moleculer"; 5 | 6 | import moleculerConfig from "./moleculer.config"; 7 | 8 | config(); 9 | 10 | export function getMoleculerConfig(moleculerFileConfig: BrokerOptions) { 11 | const mergedConfig = defaultsDeep( 12 | moleculerFileConfig, 13 | ServiceBroker.defaultOptions, 14 | ) as unknown as BrokerOptions; 15 | 16 | // Override broker options from .env 17 | if (process.env.NAMESPACE) { 18 | mergedConfig.namespace = process.env.NAMESPACE; 19 | } 20 | 21 | // 22 | if (process.env.TRANSPORTER) { 23 | mergedConfig.transporter = process.env.TRANSPORTER; 24 | } 25 | 26 | if (process.env.LOGLEVEL) { 27 | mergedConfig.logLevel = process.env.LOGLEVEL as unknown as LogLevels; 28 | } 29 | 30 | // NOTE: Support more .env variables here if needed 31 | 32 | if (!mergedConfig.nodeID) { 33 | const nodeId = `${os.hostname().toLowerCase()}-${process.pid}`; 34 | mergedConfig.nodeID = nodeId; 35 | } 36 | 37 | return mergedConfig; 38 | } 39 | 40 | // Create a ServiceBroker 41 | const broker = new ServiceBroker(getMoleculerConfig(moleculerConfig)); 42 | broker.logger.info("Booting Moleculer server..."); 43 | // Start the broker 44 | broker 45 | .start() 46 | .then(() => { 47 | broker.logger.info("Moleculer server started."); 48 | 49 | // Load services from "services" folder 50 | broker.logger.info("Loading services..."); 51 | broker.loadServices("./services", "**/*.service.ts"); 52 | }) 53 | .catch((err) => { 54 | broker.logger.error(err); 55 | }); 56 | -------------------------------------------------------------------------------- /services/api.service.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage } from "node:http"; 2 | import process from "node:process"; 3 | import helmet from "helmet"; 4 | import type { Context, ServiceAction, ServiceSchema } from "moleculer"; 5 | import ApiGateway from "moleculer-web"; 6 | 7 | const apiService: ServiceSchema = { 8 | name: "api", 9 | mixins: [ApiGateway], 10 | 11 | // More info about settings: https://moleculer.services/docs/0.14/moleculer-web.html 12 | settings: { 13 | // Exposed port 14 | port: Number(process.env?.PORT ?? 3000), 15 | 16 | // Exposed IP 17 | ip: process.env?.SERVER_HOSTNAME ?? "0.0.0.0", 18 | 19 | // Global Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares 20 | use: [helmet()], 21 | 22 | routes: [ 23 | { 24 | path: "/api/health", 25 | 26 | // Route CORS settings (overwrite global settings) 27 | cors: { 28 | origin: "*", 29 | }, 30 | aliases: { 31 | check: "$node.health", 32 | }, 33 | }, 34 | { 35 | path: "/api", 36 | 37 | whitelist: ["**"], 38 | 39 | // Route-level Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares 40 | use: [], 41 | 42 | // Enable/disable parameter merging method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Disable-merging 43 | mergeParams: true, 44 | 45 | // Enable authentication. Implement the logic into `authenticate` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authentication 46 | authentication: false, 47 | 48 | // Enable authorization. Implement the logic into `authorize` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authorization 49 | authorization: false, 50 | 51 | // The auto-alias feature allows you to declare your route alias directly in your services. 52 | // The gateway will dynamically build the full routes from service schema. 53 | autoAliases: true, 54 | 55 | /** 56 | * Before call hook. You can check the request. 57 | * @param {Context} ctx 58 | * @param {Object} route 59 | * @param {IncomingRequest} req 60 | * @param {ServerResponse} res 61 | * @param {Object} data 62 | * 63 | onBeforeCall(ctx, route, req, res) { 64 | // Set request headers to context meta 65 | ctx.meta.userAgent = req.headers["user-agent"]; 66 | }, */ 67 | 68 | /** 69 | * After call hook. You can modify the data. 70 | * @param {Context} ctx 71 | * @param {Object} route 72 | * @param {IncomingRequest} req 73 | * @param {ServerResponse} res 74 | * @param {Object} data 75 | onAfterCall(ctx, route, req, res, data) { 76 | // Async function which return with Promise 77 | return doSomething(ctx, res, data); 78 | }, */ 79 | 80 | // Calling options. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Calling-options 81 | callingOptions: {}, 82 | 83 | aliases: {}, 84 | 85 | bodyParsers: { 86 | json: { 87 | strict: false, 88 | limit: "1MB", 89 | }, 90 | urlencoded: { 91 | extended: true, 92 | limit: "1MB", 93 | }, 94 | }, 95 | 96 | // Mapping policy setting. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Mapping-policy 97 | mappingPolicy: "all", // Available values: "all", "restrict" 98 | 99 | // Enable/disable logging 100 | logging: true, 101 | }, 102 | ], 103 | 104 | // Do not log client side errors (does not log an error response when the error.code is 400<=X<500) 105 | // log4XXResponses: false, 106 | // Logging the request parameters. Set to any log level to enable it. E.g. "info" 107 | logRequestParams: null, 108 | // Logging the response data. Set to any log level to enable it. E.g. "info" 109 | logResponseData: null, 110 | 111 | // Serve assets from "public" folder. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Serve-static-files 112 | assets: { 113 | folder: "public", 114 | 115 | // Options to `server-static` module 116 | options: {}, 117 | }, 118 | }, 119 | 120 | methods: { 121 | /** 122 | * Authenticate the request. It check the `Authorization` token value in the request header. 123 | * Check the token value & resolve the user by the token. 124 | * The resolved user will be available in `ctx.meta.user` 125 | * 126 | * PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION! 127 | */ 128 | async authenticate( 129 | _ctx: Context }>, 130 | _route: Record, 131 | request: IncomingMessage & { 132 | $action: ServiceAction & { auth: string }; 133 | }, 134 | ) { 135 | // Read the token from header 136 | const auth = request.headers.authorization; 137 | 138 | if (auth?.startsWith("Bearer")) { 139 | const token = auth.slice(7); 140 | 141 | // Check the token. Tip: call a service which verify the token. E.g. `accounts.resolveToken` 142 | if (token === "123456") { 143 | // Returns the resolved user. It will be set to the `ctx.meta.user` 144 | return { id: 1, name: "John Doe" }; 145 | } 146 | 147 | // Invalid token 148 | throw new ApiGateway.Errors.UnAuthorizedError( 149 | ApiGateway.Errors.ERR_INVALID_TOKEN, 150 | [], 151 | ); 152 | } 153 | // No token. Throw an error or do nothing if anonymous access is allowed. 154 | // throw new E.UnAuthorizedError(E.ERR_NO_TOKEN); 155 | return null; 156 | }, 157 | 158 | /** 159 | * Authorize the request. Check that the authenticated user has right to access the resource. 160 | * 161 | * PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION! 162 | */ 163 | async authorize( 164 | ctx: Context }>, 165 | _route: Record, 166 | request: IncomingMessage & { 167 | $action: ServiceAction & { auth: string }; 168 | }, 169 | ) { 170 | // Get the authenticated user. 171 | const { user } = ctx.meta; 172 | 173 | // It check the `auth` property in action schema. 174 | if (request.$action.auth === "required" && !user) { 175 | throw new ApiGateway.Errors.UnAuthorizedError("NO_RIGHTS", []); 176 | } 177 | }, 178 | }, 179 | }; 180 | 181 | export default apiService; 182 | -------------------------------------------------------------------------------- /services/dtos/product-dto.swagger.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | addCartDTO: 4 | type: object 5 | properties: 6 | name: 7 | type: string 8 | description: Name of the product 9 | example: Iphone 10 | qty: 11 | type: number 12 | description: Quantity of the product 13 | example: 1 14 | price: 15 | type: number 16 | description: Price of the product 17 | example: 1000 18 | billing: 19 | type: object 20 | properties: 21 | address: 22 | type: string 23 | city: 24 | type: string 25 | zip: 26 | type: number 27 | country: 28 | type: string 29 | required: 30 | - city 31 | - zip 32 | - country 33 | required: 34 | - name 35 | - qty 36 | description: Add cart DTO 37 | addCartResponseDTO: 38 | type: object 39 | properties: 40 | success: 41 | type: boolean 42 | description: Success flag 43 | example: true 44 | message: 45 | type: string 46 | description: Message 47 | example: Product added to cart 48 | required: 49 | - success 50 | - message 51 | description: Add cart response DTO 52 | parameters: {} 53 | -------------------------------------------------------------------------------- /services/dtos/product.dto.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "node:fs"; 2 | import { resolve } from "node:path"; 3 | import { OpenApiGeneratorV3, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; 4 | import yaml from "yaml"; 5 | import { z } from "zod"; 6 | 7 | // Extend Zod with OpenAPI 8 | extendZodWithOpenApi(z); 9 | 10 | export const addCartSchema = { 11 | name: z.string().openapi({ description: "Name of the product", example: "Iphone" }), 12 | qty: z.number().openapi({ 13 | description: "Quantity of the product", 14 | example: 1, 15 | }), 16 | price: z.number().optional().openapi({ 17 | description: "Price of the product", 18 | example: 1000, 19 | }), 20 | billing: z 21 | .object({ 22 | address: z.string().optional(), 23 | city: z.string(), 24 | zip: z.number(), 25 | country: z.string(), 26 | }) 27 | .optional(), 28 | }; 29 | 30 | // Write to same folder with the DTO file 31 | const outputDirectory = __dirname; 32 | 33 | const Schema = z.object(addCartSchema).openapi("addCartDTO", { 34 | description: "Add cart DTO", 35 | }); 36 | const ResponseSchema = z 37 | .object({ 38 | success: z.boolean().openapi({ description: "Success flag", example: true }), 39 | message: z.string().openapi({ description: "Message", example: "Product added to cart" }), 40 | }) 41 | .openapi("addCartResponseDTO", { 42 | description: "Add cart response DTO", 43 | }); 44 | const generator = new OpenApiGeneratorV3([Schema, ResponseSchema]); 45 | const components = generator.generateComponents(); 46 | 47 | // Write to YAML file 48 | writeFileSync(resolve(outputDirectory, "product-dto.swagger.yaml"), yaml.stringify(components)); 49 | -------------------------------------------------------------------------------- /services/greeter.service.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Service, ServiceSchema } from "moleculer"; 2 | 3 | type GreeterSettings = { 4 | defaultName: string; 5 | }; 6 | 7 | type GreeterMethods = { 8 | /** 9 | * Say a 'Hello' to a user. 10 | * @example 11 | * sayHello("John Doe"); 12 | * // Hello John Doe 13 | **/ 14 | sayHello(name: string): string; 15 | }; 16 | 17 | type GreeterThis = Service & GreeterMethods; 18 | 19 | /** 20 | * @swagger 21 | * components: 22 | * schemas: 23 | * welcomeResponseDTO: 24 | * type: string 25 | * example: Welcome, dunghd 26 | * parameters: 27 | * username: 28 | * name: username 29 | * in: query 30 | * description: User name 31 | * required: true 32 | * schema: 33 | * type: string 34 | * minLength: 6 35 | * maxLength: 25 36 | */ 37 | const greeterService: ServiceSchema = { 38 | name: "greeter", 39 | 40 | /** 41 | * Settings 42 | */ 43 | settings: { 44 | defaultName: "Moleculer", 45 | }, 46 | 47 | /** 48 | * Dependencies 49 | */ 50 | dependencies: [], 51 | 52 | /** 53 | * Actions 54 | */ 55 | actions: { 56 | /** 57 | * @swagger 58 | * /api/greeter/hello: 59 | * get: 60 | * description: Returns the Hello Moleculer 61 | * tags: 62 | * - greeter 63 | * responses: 64 | * 200: 65 | * description: Hello Moleculer 66 | * content: 67 | * text/plain: 68 | * schema: 69 | * type: string 70 | * example: Hello Moleculer 71 | */ 72 | hello: { 73 | rest: { 74 | method: "GET", 75 | path: "/hello", 76 | }, 77 | async handler(this: GreeterThis) { 78 | return this.sayHello(this.settings.defaultName); 79 | }, 80 | }, 81 | 82 | /** 83 | * Welcome, a username 84 | * 85 | * @param name - User name 86 | * @swagger 87 | * /api/greeter/welcome: 88 | * get: 89 | * description: Returns Welcome, a username 90 | * tags: 91 | * - greeter 92 | * parameters: 93 | * - $ref: '#/components/parameters/username' 94 | * in: query 95 | * responses: 96 | * 200: 97 | * description: Welcome, a username 98 | * content: 99 | * text/plain: 100 | * schema: 101 | * $ref: '#/components/schemas/welcomeResponseDTO' 102 | * 422: 103 | * description: Invalid username 104 | */ 105 | welcome: { 106 | rest: "/welcome", 107 | params: { 108 | username: { type: "string", min: 6, max: 25 }, 109 | }, 110 | /** 111 | * @param ctx - Request context 112 | * @returns Welcome, a username 113 | */ 114 | async handler( 115 | ctx: Context<{ 116 | username: string; 117 | }>, 118 | ) { 119 | return `Welcome, ${ctx.params.username}`; 120 | }, 121 | }, 122 | }, 123 | 124 | /** 125 | * Events 126 | */ 127 | events: {}, 128 | 129 | /** 130 | * Methods 131 | */ 132 | methods: { 133 | sayHello(name: string) { 134 | return `Hello ${name}`; 135 | }, 136 | }, 137 | 138 | /** 139 | * Service created lifecycle event handler 140 | */ 141 | created() { 142 | this.logger.info("[greeter] The service was created"); 143 | }, 144 | 145 | /** 146 | * Service started lifecycle event handler 147 | */ 148 | async started() { 149 | this.logger.info("[greeter] The service was started"); 150 | }, 151 | 152 | /** 153 | * Service stopped lifecycle event handler 154 | */ 155 | async stopped() { 156 | this.logger.info("[greeter] The service was stopped"); 157 | }, 158 | }; 159 | 160 | export default greeterService; 161 | -------------------------------------------------------------------------------- /services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Context, 3 | Errors, 4 | type GenericObject, 5 | type Service, 6 | type ServiceSchema, 7 | } from "moleculer"; 8 | import { ZodParams } from "moleculer-zod-validator"; 9 | import { z } from "zod"; 10 | 11 | import { logger } from "../logger"; 12 | import { addCartSchema } from "./dtos/product.dto"; 13 | 14 | type ServiceSettings = Record; 15 | 16 | type ServiceMethods = Record; 17 | 18 | type ServiceThis = Service & ServiceMethods; 19 | 20 | const orderItemValidator = new ZodParams(addCartSchema); 21 | 22 | // TODO: Move this to a shared utility 23 | const validateParams = ( 24 | ctx: Context, GenericObject>, 25 | schema: typeof addCartSchema, 26 | ) => { 27 | const compiled = z.object(schema).strict(); 28 | try { 29 | const parsedParams = compiled.parse(ctx.params); 30 | logger.info("Validated parameters: %o", parsedParams); 31 | } catch (err) { 32 | if (err instanceof z.ZodError) 33 | throw new Errors.ValidationError( 34 | "Parameters validation error!", 35 | "VALIDATION_ERROR", 36 | err.issues, 37 | ); 38 | 39 | throw err; 40 | } 41 | }; 42 | 43 | const productService: ServiceSchema = { 44 | name: "product", 45 | 46 | /** 47 | * Settings 48 | */ 49 | settings: {}, 50 | 51 | /** 52 | * Dependencies 53 | */ 54 | dependencies: [], 55 | 56 | /** 57 | * Actions 58 | */ 59 | actions: { 60 | /** 61 | * Add a product to the cart 62 | * @swagger 63 | * /api/product/cart: 64 | * post: 65 | * description: Add a product to the cart 66 | * tags: 67 | * - product 68 | * requestBody: 69 | * required: true 70 | * content: 71 | * application/json: 72 | * schema: 73 | * $ref: '#/components/schemas/addCartDTO' 74 | * responses: 75 | * 200: 76 | * description: Product added to cart 77 | * content: 78 | * application/json: 79 | * schema: 80 | * $ref: '#/components/schemas/addCartResponseDTO' 81 | * 422: 82 | * description: Validation error 83 | */ 84 | addToCart: { 85 | rest: { 86 | method: "POST", 87 | path: "/cart", 88 | }, 89 | hooks: { 90 | before(ctx) { 91 | this.logger.info("Validating parameters for addToCart action"); 92 | validateParams(ctx, addCartSchema); 93 | }, 94 | }, 95 | handler(this: ServiceThis, ctx: Context) { 96 | this.logger.info("addToCart action called with parameters: %o", ctx.params); 97 | const { name, qty, billing } = ctx.params; 98 | return { 99 | success: true, 100 | message: `You added ${qty} ${name} to your cart`, 101 | billing, 102 | }; 103 | }, 104 | }, 105 | }, 106 | 107 | /** 108 | * Events 109 | */ 110 | events: {}, 111 | 112 | /** 113 | * Methods 114 | */ 115 | methods: {}, 116 | 117 | /** 118 | * Service created lifecycle event handler 119 | */ 120 | created() { 121 | this.logger.info(`The ${this.name} service created.`); 122 | }, 123 | 124 | /** 125 | * Service started lifecycle event handler 126 | */ 127 | async started() { 128 | this.logger.info(`The ${this.name} service started.`); 129 | }, 130 | 131 | /** 132 | * Service stopped lifecycle event handler 133 | */ 134 | async stopped() { 135 | this.logger.info(`The ${this.name} service stopped.`); 136 | }, 137 | }; 138 | 139 | export default productService; 140 | -------------------------------------------------------------------------------- /test/e2e/greeter.hurl: -------------------------------------------------------------------------------- 1 | # Get hello message 2 | GET http://localhost:{{PORT}}/api/greeter/hello 3 | 4 | HTTP 200 5 | 6 | # Get welcome message 7 | GET http://localhost:{{PORT}}/api/greeter/welcome 8 | [QueryStringParams] 9 | username: itman 10 | 11 | HTTP 422 12 | [Asserts] 13 | jsonpath "$.message" == "Parameters validation error!" 14 | 15 | GET http://localhost:{{PORT}}/api/greeter/welcome 16 | [QueryStringParams] 17 | username: dunghd 18 | 19 | HTTP 200 20 | -------------------------------------------------------------------------------- /test/e2e/health.hurl: -------------------------------------------------------------------------------- 1 | # Get health check 2 | GET http://localhost:{{PORT}}/api/health/check 3 | 4 | HTTP 200 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/e2e/product.hurl: -------------------------------------------------------------------------------- 1 | # Add a product to cart 2 | POST http://localhost:{{PORT}}/api/product/cart 3 | { 4 | 5 | } 6 | HTTP 422 7 | [Asserts] 8 | jsonpath "$.message" == "Parameters validation error!" 9 | 10 | POST http://localhost:{{PORT}}/api/product/cart 11 | { 12 | "name": "Iphone 15 Pro" 13 | } 14 | 15 | HTTP 422 16 | [Asserts] 17 | jsonpath "$.message" == "Parameters validation error!" 18 | 19 | POST http://localhost:{{PORT}}/api/product/cart 20 | { 21 | "name": "Iphone 15 Pro", 22 | "qty": 1 23 | } 24 | 25 | HTTP 200 26 | [Asserts] 27 | jsonpath "$.success" == true 28 | jsonpath "$.message" == "You added 1 Iphone 15 Pro to your cart" 29 | jsonpath "$.billing" not exists 30 | 31 | POST http://localhost:{{PORT}}/api/product/cart 32 | { 33 | "name": "Iphone 15 Pro", 34 | "qty": 1, 35 | "billing": { 36 | "zip": 123456 37 | } 38 | } 39 | HTTP 422 40 | [Asserts] 41 | jsonpath "$.message" == "Parameters validation error!" 42 | 43 | POST http://localhost:{{PORT}}/api/product/cart 44 | { 45 | "name": "Iphone 15 Pro", 46 | "qty": 1, 47 | "billing": { 48 | "city": "Singapore", 49 | "country": "Singapore", 50 | "zip": 123456 51 | } 52 | } 53 | HTTP 200 54 | [Asserts] 55 | jsonpath "$.billing.city" == "Singapore" 56 | jsonpath "$.billing.country" == "Singapore" 57 | jsonpath "$.billing.zip" == 123456 58 | 59 | POST http://localhost:{{PORT}}/api/product/cart 60 | { 61 | "name": "Iphone 15 Pro", 62 | "qty": 1, 63 | "billing": { 64 | "city": "Singapore", 65 | "country": "Singapore", 66 | "zip": 123456 67 | }, 68 | "buyer": { 69 | "name": "Dung Huynh" 70 | } 71 | } 72 | HTTP 422 73 | [Asserts] 74 | jsonpath "$.message" == "Parameters validation error!" 75 | jsonpath "$.data[0].message" == "Unrecognized key(s) in object: 'buyer'" 76 | -------------------------------------------------------------------------------- /test/unit/services/greeter.spec.ts: -------------------------------------------------------------------------------- 1 | import { Errors, ServiceBroker, type ServiceSchema } from "moleculer"; 2 | 3 | import TestService from "../../../services/greeter.service"; 4 | 5 | const { ValidationError } = Errors; 6 | 7 | describe("Test 'greeter' service", () => { 8 | const broker = new ServiceBroker({ logger: false }); 9 | broker.createService(TestService as unknown as ServiceSchema); 10 | 11 | beforeAll(async () => broker.start()); 12 | afterAll(async () => broker.stop()); 13 | 14 | describe("Test 'greeter.hello' action", () => { 15 | it("should return with 'Hello Moleculer'", async () => { 16 | const response = await broker.call("greeter.hello"); 17 | expect(response).toBe("Hello Moleculer"); 18 | }); 19 | }); 20 | 21 | describe("Test 'greeter.welcome' action", () => { 22 | it("should return with 'Welcome'", async () => { 23 | const response = await broker.call("greeter.welcome", { 24 | username: "Dung Huynh", 25 | }); 26 | expect(response).toBe("Welcome, Dung Huynh"); 27 | }); 28 | 29 | it("should reject an ValidationError", async () => { 30 | expect.assertions(2); 31 | try { 32 | await broker.call("greeter.welcome"); 33 | } catch (error: unknown) { 34 | expect(error).toBeInstanceOf(ValidationError); 35 | } 36 | 37 | try { 38 | await broker.call("greeter.welcome", { 39 | username: "a1", 40 | }); 41 | } catch (error: unknown) { 42 | expect(error).toBeInstanceOf(ValidationError); 43 | } 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/unit/services/product.spec.ts: -------------------------------------------------------------------------------- 1 | import { Errors, ServiceBroker, type ServiceSchema } from "moleculer"; 2 | import { ZodValidator } from "moleculer-zod-validator"; 3 | 4 | import TestService from "../../../services/product.service"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/naming-convention 7 | const { ValidationError } = Errors; 8 | 9 | describe("Test 'product' service", () => { 10 | const broker = new ServiceBroker({ 11 | logger: false, 12 | validator: new ZodValidator(), 13 | }); 14 | broker.createService(TestService as unknown as ServiceSchema); 15 | 16 | beforeAll(async () => broker.start()); 17 | afterAll(async () => broker.stop()); 18 | 19 | describe("Test 'product.addToCart' action", () => { 20 | it("should return success message", async () => { 21 | const response = await broker.call("product.addToCart", { 22 | name: "iPhone", 23 | qty: 1, 24 | }); 25 | expect(response).toEqual({ 26 | success: true, 27 | message: "You added 1 iPhone to your cart", 28 | }); 29 | }); 30 | 31 | it("should reject an ValidationError", async () => { 32 | expect.assertions(1); 33 | try { 34 | await broker.call("product.addToCart"); 35 | } catch (error: unknown) { 36 | expect(error).toBeInstanceOf(ValidationError); 37 | } 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 20 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | /* Modules */ 24 | "module": "commonjs" /* Specify what module code is generated. */, 25 | // "rootDir": "./", /* Specify the root folder within your source files. */ 26 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 27 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 28 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 29 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 30 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 31 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 32 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 33 | // "resolveJsonModule": true, /* Enable importing .json files */ 34 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 35 | /* JavaScript Support */ 36 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 37 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 38 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 39 | /* Emit */ 40 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 41 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 42 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 43 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 44 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 45 | "outDir": "dist" /* Specify an output folder for all emitted files. */, 46 | // "removeComments": true, /* Disable emitting comments. */ 47 | // "noEmit": true, /* Disable emitting files from a compilation. */ 48 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 49 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 50 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 51 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 54 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 55 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 56 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 57 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 58 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 59 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 60 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 61 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 62 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 63 | /* Interop Constraints */ 64 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 65 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 66 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 67 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 68 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 69 | /* Type Checking */ 70 | "strict": true /* Enable all strict type-checking options. */, 71 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 72 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 73 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 74 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 75 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 76 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 77 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 78 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 79 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 80 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 81 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 82 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 83 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 84 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 85 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 86 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 87 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 88 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 89 | /* Completeness */ 90 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 91 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 92 | }, 93 | "include": ["services/**/*.ts", "addons/**/*.ts", "examples/**/*.ts", "test/**/*.spec.ts"], 94 | "files": ["server.ts", "moleculer.config.ts", "logger.ts", "cli.ts"] 95 | } 96 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["addons/**/*.ts", "logger.ts", "moleculer.config.ts", "services/**/*.ts"], 5 | splitting: false, 6 | sourcemap: true, 7 | format: ["esm", "cjs"], 8 | target: "es2022", 9 | clean: true, 10 | outExtension({ format }) { 11 | if (format.toLowerCase() === "esm") { 12 | return { js: ".mjs" }; 13 | } 14 | 15 | return { 16 | js: ".js", 17 | }; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /wait-on.config.cjs: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config(); 3 | 4 | const port = process.env.PORT || 3000; 5 | 6 | module.exports = { 7 | resources: [`tcp:${port}`], 8 | }; 9 | --------------------------------------------------------------------------------