├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── .utils ├── api-client-generator-launcher.js └── dev-dummy-page-generator.js ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── client ├── .gitignore ├── .proxy.conf.json ├── README.md ├── angular.json ├── karma.conf.js ├── package.json ├── public │ ├── .gitkeep │ └── favicon.ico ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.routes.ts │ ├── environments │ │ ├── environment.development.ts │ │ └── environment.ts │ ├── index.html │ ├── main.ts │ ├── material.module.ts │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── heroku.yml ├── package-lock.json ├── package.json ├── render.yaml └── server ├── .gitignore ├── README.md ├── eslint.config.mjs ├── nest-cli.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── entities │ └── article.entity.ts ├── helper.ts ├── main.ts └── openapi-doc-generator.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | node_modules/ 3 | client/node_modules/ 4 | server/node_modules/ 5 | client/dist/ 6 | server/dist/ 7 | client/TESTS-*.xml 8 | server/junit.xml 9 | 10 | # VCS files 11 | .git/ 12 | 13 | # Other files 14 | README.md 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Install Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | 18 | - name: Install dependencies 19 | id: install_deps 20 | run: npm install 21 | env: 22 | CI: true 23 | 24 | - name: Test for Server (Unit) 25 | run: npm run test --workspace=server -- --ci --reporters=default --reporters=jest-junit 26 | env: 27 | JEST_JUNIT_OUTPUT_NAME: 'junit-unit.xml' 28 | continue-on-error: true 29 | 30 | - name: Test for Server (E2E) 31 | run: npm run test:e2e --workspace=server -- --ci --reporters=default --reporters=jest-junit 32 | env: 33 | JEST_JUNIT_OUTPUT_NAME: 'junit-e2e.xml' 34 | continue-on-error: true 35 | 36 | - name: Test for Client 37 | run: | 38 | npm run build-api-client 39 | npm run test --workspace=client -- --browsers=ChromeHeadless --reporters=progress,junit --watch=false 40 | continue-on-error: true 41 | 42 | - name: Upload test report for Server (Unit) 43 | uses: mikepenz/action-junit-report@eb1a2b2dbd4c45341235503b2c3edfa46d2ec3de 44 | if: always() 45 | with: 46 | check_name: 'Test Report - Server (Unit)' 47 | report_paths: 'server/junit-unit.xml' 48 | fail_on_failure: True 49 | require_tests: True 50 | 51 | - name: Upload test report for Server (E2E) 52 | uses: mikepenz/action-junit-report@eb1a2b2dbd4c45341235503b2c3edfa46d2ec3de 53 | if: always() 54 | with: 55 | check_name: 'Test Report - Server (E2E)' 56 | report_paths: 'server/junit-e2e.xml' 57 | fail_on_failure: True 58 | require_tests: True 59 | 60 | - name: Upload test report for Client 61 | uses: mikepenz/action-junit-report@eb1a2b2dbd4c45341235503b2c3edfa46d2ec3de 62 | if: always() 63 | with: 64 | check_name: 'Test Report - Client' 65 | report_paths: 'client/TESTS-*.xml' 66 | fail_on_failure: True 67 | require_tests: True 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.utils/api-client-generator-launcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Launcher for API Client Generator 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const fs = require('fs'); 8 | const crypto = require('crypto'); 9 | const childProcess = require('child_process'); 10 | const fetch = require('node-fetch'); 11 | const json2yaml = require('json2yaml'); 12 | 13 | class APIClientGeneratorLauncher { 14 | static API_JSON_URL = 'http://localhost:3000/api/docs-json'; 15 | static SERVER_DIR = `${__dirname}/../server`; 16 | static CLIENT_DIR = `${__dirname}/../client`; 17 | static API_DOC_YAML_PATH = `${APIClientGeneratorLauncher.SERVER_DIR}/api.yaml`; 18 | static API_CLIENT_OUTPUT_PATH = `${APIClientGeneratorLauncher.CLIENT_DIR}/src/.api-client`; 19 | 20 | constructor() {} 21 | 22 | async start() { 23 | let apiDoc = null; 24 | if (1 < process.argv.length && process.argv.indexOf('online') != -1) { 25 | try { 26 | apiDoc = await this.getApiDocOnline(); 27 | } catch (e) { 28 | console.warn( 29 | 'Failed to get API document from API server. Trying again offline...', 30 | ); 31 | } 32 | } 33 | if (!apiDoc) { 34 | apiDoc = this.getApiDocOffline(); 35 | } 36 | 37 | const apiDocYaml = json2yaml.stringify(apiDoc); 38 | if (!this.shouldGenerateApiClient(apiDocYaml)) { 39 | return; 40 | } 41 | fs.writeFileSync(APIClientGeneratorLauncher.API_DOC_YAML_PATH, apiDocYaml); 42 | 43 | this.generateApiClient(); 44 | console.log( 45 | 'API Client was generated to ', 46 | APIClientGeneratorLauncher.API_CLIENT_OUTPUT_PATH, 47 | ); 48 | } 49 | 50 | async getApiDocOnline() { 51 | const res = await fetch(APIClientGeneratorLauncher.API_JSON_URL); 52 | return await res.json(); 53 | } 54 | 55 | getApiDocOffline() { 56 | const result = childProcess.execSync( 57 | `ts-node --project \"${APIClientGeneratorLauncher.SERVER_DIR}/tsconfig.json\" --require tsconfig-paths/register \"${APIClientGeneratorLauncher.SERVER_DIR}/src/openapi-doc-generator.ts\"`, 58 | ); 59 | return JSON.parse(result.toString()); 60 | } 61 | 62 | shouldGenerateApiClient(yaml) { 63 | if (!this.existsApiClient()) return true; 64 | 65 | if (!fs.existsSync(APIClientGeneratorLauncher.API_DOC_YAML_PATH)) 66 | return true; 67 | 68 | let savedHash; 69 | try { 70 | const savedSum = crypto.createHash('sha1'); 71 | savedSum.update( 72 | fs 73 | .readFileSync(APIClientGeneratorLauncher.API_DOC_YAML_PATH) 74 | .toString(), 75 | ); 76 | savedHash = savedSum.digest('hex'); 77 | } catch (e) { 78 | return true; 79 | } 80 | 81 | const yamlSum = crypto.createHash('sha1'); 82 | yamlSum.update(yaml); 83 | const yamlHash = yamlSum.digest('hex'); 84 | 85 | return yamlHash !== savedHash; 86 | } 87 | 88 | existsApiClient() { 89 | return fs.existsSync( 90 | `${APIClientGeneratorLauncher.API_CLIENT_OUTPUT_PATH}/api.module.ts`, 91 | ); 92 | } 93 | 94 | generateApiClient() { 95 | childProcess.execSync( 96 | `ng-openapi-gen --input \"${APIClientGeneratorLauncher.API_DOC_YAML_PATH}\" --output \"${APIClientGeneratorLauncher.API_CLIENT_OUTPUT_PATH}\"`, 97 | ); 98 | } 99 | } 100 | 101 | const launcher = new APIClientGeneratorLauncher(); 102 | launcher.start(); 103 | -------------------------------------------------------------------------------- /.utils/dev-dummy-page-generator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script that generates a dummy page 3 | * for you to open the correct port in the development environment. 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const fs = require('fs'); 9 | const mkdirp = require('mkdirp'); 10 | 11 | class DummyPageGenerator { 12 | static CLIENT_DIR = `${__dirname}/../client`; 13 | 14 | constructor() {} 15 | 16 | start() { 17 | const clientDistDir = `${DummyPageGenerator.CLIENT_DIR}/dist/browser/`; 18 | mkdirp.sync(clientDistDir); 19 | 20 | const html = ` 21 | 22 | 23 | 24 | API Server 25 | 26 | 27 |

28 | This port (3000) is the API server port by NestJS.
29 | Instead, you should open the 30 | Angular development server port (4200). 31 |

32 | 33 | 51 | 52 | `; 53 | fs.writeFileSync(`${clientDistDir}/index.html`, html); 54 | } 55 | } 56 | 57 | const generator = new DummyPageGenerator(); 58 | generator.start(); 59 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Debug for client", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start:dev", 10 | "webRoot": "${workspaceFolder}/client", 11 | "url": "http://localhost:4200/" 12 | }, 13 | { 14 | "name": "ng test", 15 | "type": "chrome", 16 | "request": "launch", 17 | "preLaunchTask": "npm: test", 18 | "url": "http://localhost:9876/debug.html" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start:dev", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-slim 2 | 3 | WORKDIR /opt/app/ 4 | 5 | EXPOSE 3000 6 | 7 | ARG NODE_ENV="production" 8 | ENV NODE_ENV "${NODE_ENV}" 9 | 10 | # Install npm modules for app 11 | COPY package.json ./ 12 | COPY client/package.json ./client/ 13 | COPY server/package.json ./server/ 14 | 15 | RUN echo "Installing npm modules..." && \ 16 | NODE_ENV=development npm install || exit 1 && \ 17 | echo "npm modules installed." && \ 18 | npm cache clean --force 19 | 20 | # Copy files for app 21 | COPY . /opt/app/ 22 | 23 | # Build for production env 24 | RUN echo "Building app...\n" && \ 25 | npm run build || exit 1 && \ 26 | echo "build was completed." 27 | 28 | # Start app 29 | CMD ["npm", "start"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-nest 2 | 3 | Simple web app template with Angular + NestJS + ng-openapi-gen + Angular Material. 4 | 5 | Deployable on Heroku, Render, Google App Engine (GAE), Cloud Run and other hosting services using with/without Docker ([learn more](https://github.com/mugifly/angular-nest/wiki/#Deployments)). 6 | 7 | [![Build](https://github.com/mugifly/angular-nest/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/mugifly/angular-nest/actions/workflows/build.yml) 8 | 9 | --- 10 | 11 | ## Key Elements 12 | 13 | There is some variations... https://github.com/mugifly/angular-nest/wiki#variations 14 | 15 | - Angular -- for Frontend app. 16 | - NestJS -- for Backend app. 17 | - ng-openapi-gen -- for API Client generation. 18 | - Angular Material -- for UI. 19 | - NOTE: There is also a [vanilla branch](https://github.com/mugifly/angular-nest/tree/vanilla) that doesn't include Angular Material. 20 | - Docker -- for Production environment. 21 | - NOTE: Deployments without Docker are also supported. 22 | - Karma + Jasmine -- for Unit testing of Frontend (with Puppeteer + Headless Chromium) 23 | - Jest -- for Unit testing of Backend 24 | - GitHub Actions -- for CI 25 | - npm Workspaces - for Monorepo structure. 26 | 27 | --- 28 | 29 | ## Quick Start for Development 30 | 31 | ### StackBlitz 32 | 33 | StackBlitz lets you quickly run and edit your source code in your browser. 34 | 35 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mugifly/angular-nest/tree/master?file=client%2Fsrc%2Fapp%2Fapp.component.html) 36 | 37 | ### Local 38 | 39 | Before you start, you should install the following softwares: 40 | 41 | - Git 42 | 43 | - Node.js v22+ 44 | 45 | - Visual Studio Code 46 | 47 | Next, please [Create a new repository from this repository](https://github.com/mugifly/angular-nest/generate). 48 | 49 | Then, execute as the following in your terminal: 50 | 51 | ``` 52 | $ git clone git@github.com:YOUR_GITHUB_NAME/angular-nest.git 53 | $ cd angular-nest/ 54 | 55 | $ npm install 56 | 57 | $ npm run start:dev 58 | ``` 59 | 60 | Finally, open the web browser and navigate to `http://localhost:4200/`. 61 | Also, when you edit the frontend source-code, auto-reloading applies your changes to the browser immediately. 62 | 63 | An API documentation is generated by Swagger UI, and you can access it at `http://localhost:4200/api/docs`. 64 | 65 | See the [Wiki](https://github.com/mugifly/angular-nest/wiki/) for additional information. 66 | You'll find tips for implementing database connectivity, guides for future updates, and more. 67 | 68 | --- 69 | 70 | ## Quick Start for Deployment 71 | 72 | This app supports direct deployment to [various hosting services](https://github.com/mugifly/angular-nest/wiki/#Deployments) as a production environment. 73 | 74 | It's also very easy to automatic-deployment, because you don't have to run the build process locally or in CI. 75 | 76 | ### Deployment on Heroku 77 | 78 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 79 | 80 | This app can be deployed either to the `Heroku` stack (Node.js buildpack based) or the `Container` stack (Docker based). 81 | 82 | Please see [Deploy to Heroku](https://github.com/mugifly/angular-nest/wiki/Deploy-to-Heroku) page for more informations. 83 | 84 | ### Deployment on Render 85 | 86 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) 87 | 88 | This app can be deployed either as a Node.js application or Docker based application. 89 | 90 | Please see [Deploy to Render](https://github.com/mugifly/angular-nest/wiki/Deploy-to-Render) page for more informations. 91 | 92 | ### Deployment on other servers (with/without Docker) 93 | 94 | https://github.com/mugifly/angular-nest/wiki/#Deployments 95 | 96 | --- 97 | 98 | ## CLI Commands 99 | 100 | ### Start 101 | 102 | - Start the development server (watch mode):   `npm run dev` 103 | - Start the production app:   `npm run build && npm run start` 104 | 105 | ### Angular CLI & Nest CLI 106 | 107 | - Angular CLI (@angular/cli):   `npm run ng` 108 | - Generate component:   `npm run ng -- generate component foo-bar` 109 | - Other `ng` commands can also be run by typing it after `npm run ng -- `. See [here](https://angular.io/cli#command-overview) for a list of the available commands. 110 | - Nest CLI (@nestjs/cli):   `npm run nest` 111 | - Generate controller:   `npm run nest -- generate controller foo-bar` 112 | - Other `nest` commands can also be run by typing it after `npm run nest -- `. See [here](https://docs.nestjs.com/cli/usages) for a list of the available commands. 113 | 114 | ### Testing 115 | 116 | - Unit tests for frontend:   `npm run test -w client`   (Google Chrome and [dependencies of Puppeteer](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) required) 117 | - NOTE: If you have never generated an API client (or run `npm run dev`), you should run `npm run build-api-client` before running the test. 118 | - Unit tests for backend:   `npm run test -w server` 119 | - E2E tests for backend:   `npm run test:e2e -w server` 120 | 121 | ### Install npm modules 122 | 123 | - Install for frontend: `npm install -w client XXXXX` 124 | - Install for backend: `npm install -w server XXXXX`
(e.g. `npm install -w server @nestjs/typeorm typeorm`) 125 | 126 | --- 127 | 128 | ## License and Author 129 | 130 | This project is released under the [CC0 1.0 Universal](https://github.com/mugifly/angular-nest/blob/master/LICENSE) license, by Masanori Ohgita ([mugifly](https://github.com/mugifly)). 131 | 132 | Therefore, a copyright notice is NOT required. 133 | Feel free to use or copy it to your project :) 134 | 135 | NOTE: However, some sample codes and documents (e.g. client/README.md and server/README.md) that generated by Angular CLI or Nest CLI may be based on their respective licenses. 136 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-nest", 3 | "env": {}, 4 | "addons": [] 5 | } 6 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | TESTS-*.xml 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | # API Client 46 | .api-client/ 47 | 48 | -------------------------------------------------------------------------------- /client/.proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3000/", 4 | "secure": false, 5 | "logLevel": "debug" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 28 | -------------------------------------------------------------------------------- /client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "client": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": { 21 | "base": "dist" 22 | }, 23 | "index": "src/index.html", 24 | "browser": "src/main.ts", 25 | "polyfills": ["zone.js"], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | { 30 | "glob": "**/*", 31 | "input": "public" 32 | } 33 | ], 34 | "styles": [ 35 | "../node_modules/@angular/material/prebuilt-themes/azure-blue.css", 36 | "src/styles.scss" 37 | ], 38 | "scripts": [] 39 | }, 40 | "configurations": { 41 | "production": { 42 | "budgets": [ 43 | { 44 | "type": "initial", 45 | "maximumWarning": "500kB", 46 | "maximumError": "1MB" 47 | }, 48 | { 49 | "type": "anyComponentStyle", 50 | "maximumWarning": "2kB", 51 | "maximumError": "4kB" 52 | } 53 | ], 54 | "outputHashing": "all" 55 | }, 56 | "development": { 57 | "optimization": false, 58 | "extractLicenses": false, 59 | "sourceMap": true, 60 | "fileReplacements": [ 61 | { 62 | "replace": "src/environments/environment.ts", 63 | "with": "src/environments/environment.development.ts" 64 | } 65 | ] 66 | } 67 | }, 68 | "defaultConfiguration": "production" 69 | }, 70 | "serve": { 71 | "builder": "@angular-devkit/build-angular:dev-server", 72 | "configurations": { 73 | "production": { 74 | "buildTarget": "client:build:production" 75 | }, 76 | "development": { 77 | "buildTarget": "client:build:development" 78 | } 79 | }, 80 | "defaultConfiguration": "development" 81 | }, 82 | "extract-i18n": { 83 | "builder": "@angular-devkit/build-angular:extract-i18n" 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "polyfills": ["zone.js", "zone.js/testing"], 89 | "tsConfig": "tsconfig.spec.json", 90 | "karmaConfig": "karma.conf.js", 91 | "inlineStyleLanguage": "scss", 92 | "assets": [ 93 | { 94 | "glob": "**/*", 95 | "input": "public" 96 | } 97 | ], 98 | "styles": [ 99 | "../node_modules/@angular/material/prebuilt-themes/azure-blue.css", 100 | "src/styles.scss" 101 | ], 102 | "scripts": [] 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('karma-junit-reporter'), 14 | require('@angular-devkit/build-angular/plugins/karma'), 15 | ], 16 | client: { 17 | jasmine: { 18 | // you can add configuration options for Jasmine here 19 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 20 | // for example, you can disable the random execution with `random: false` 21 | // or set a specific seed with `seed: 4321` 22 | }, 23 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 24 | }, 25 | jasmineHtmlReporter: { 26 | suppressAll: true, // removes the duplicated traces 27 | }, 28 | coverageReporter: { 29 | dir: require('path').join(__dirname, './coverage/client'), 30 | subdir: '.', 31 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 32 | }, 33 | reporters: ['progress', 'kjhtml'], 34 | browsers: ['Chrome'], 35 | restartOnFileChange: true, 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "start:dev": "ng serve --proxy-config .proxy.conf.json", 8 | "build": "ng build", 9 | "watch": "ng build --watch --configuration development", 10 | "test": "ng test" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^19.0.1", 15 | "@angular/cdk": "^19.0.1", 16 | "@angular/common": "^19.0.1", 17 | "@angular/compiler": "^19.0.1", 18 | "@angular/core": "^19.0.1", 19 | "@angular/forms": "^19.0.1", 20 | "@angular/material": "^19.0.1", 21 | "@angular/platform-browser": "^19.0.1", 22 | "@angular/platform-browser-dynamic": "^19.0.1", 23 | "@angular/router": "^19.0.1", 24 | "rxjs": "^7.8.1", 25 | "tslib": "^2.3.0", 26 | "zone.js": "^0.15.0" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^19.0.2", 30 | "@angular/cli": "^19.0.2", 31 | "@angular/compiler-cli": "^19.0.1", 32 | "@types/jasmine": "~3.10.0", 33 | "@types/node": "^12.11.1", 34 | "jasmine-core": "~4.0.0", 35 | "karma": "~6.3.0", 36 | "karma-chrome-launcher": "~3.1.0", 37 | "karma-coverage": "~2.1.0", 38 | "karma-jasmine": "~4.0.0", 39 | "karma-jasmine-html-reporter": "~1.7.0", 40 | "karma-junit-reporter": "^2.0.1", 41 | "typescript": "5.6.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugifly/angular-nest/37ae15f959c2d5d1c1d79b6191bce5abc2728198/client/public/.gitkeep -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugifly/angular-nest/37ae15f959c2d5d1c1d79b6191bce5abc2728198/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | angular-nest 3 | 4 | @if (!isProduction) { 5 |   6 | (dev) 7 | } 8 | 9 | 10 |
11 |
12 | 13 | 14 | What is this? 15 | 16 | 17 | Simple web app template with Angular + NestJS + ng-openapi-gen.
18 | 23 | https://github.com/mugifly/angular-nest 24 | 25 |
26 |
27 |
28 | 29 |
30 | 31 | 32 | Example request (plain text) 33 | 34 | 35 | {{ $exampleText | async }} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Example request (object) 45 | 46 | 47 | @if ($exampleArticle | async; as article) { 48 |
    49 |
  • Title: {{ article.title }}
  • 50 |
  • ID: {{ article.id }}
  • 51 |
  • 52 | Updated at: {{ article.updatedAt | date: 'yyyy-MM-dd HH:mm:ss' }} 53 |
  • 54 |
55 | } 56 |
57 | 58 | 59 | 60 |
61 |
62 | 63 |
64 | -------------------------------------------------------------------------------- /client/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | align-items: flex-start; 3 | background-color: #fafafa; 4 | box-sizing: border-box; 5 | padding: 2rem; 6 | min-height: calc(100vh - 64px); 7 | 8 | .cards { 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: flex-start; 12 | gap: 1rem; 13 | margin-bottom: 1rem; 14 | 15 | mat-card { 16 | display: relative; 17 | height: auto; 18 | width: 50%; 19 | } 20 | 21 | mat-card a { 22 | color: #3f51b5; 23 | } 24 | } 25 | } 26 | 27 | @media screen and (max-width: 640px) { 28 | .content { 29 | .cards { 30 | flex-direction: column; 31 | 32 | mat-card { 33 | width: 90% !important; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | provideHttpClient, 3 | withInterceptorsFromDi, 4 | } from '@angular/common/http'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { AppComponent } from './app.component'; 7 | import { provideRouter } from '@angular/router'; 8 | import { MaterialModule } from '../material.module'; 9 | 10 | describe('AppComponent', () => { 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [MaterialModule, AppComponent], 14 | providers: [ 15 | provideHttpClient(withInterceptorsFromDi()), 16 | provideRouter([]), 17 | ], 18 | }).compileComponents(); 19 | }); 20 | 21 | it('should create the app', () => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }); 26 | 27 | it('should render card', () => { 28 | const fixture = TestBed.createComponent(AppComponent); 29 | fixture.detectChanges(); 30 | const compiled = fixture.nativeElement as HTMLElement; 31 | expect(compiled.querySelector('mat-card-title')?.textContent).toContain( 32 | 'What is this?', 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { AsyncPipe, DatePipe } from '@angular/common'; 4 | import { RouterOutlet } from '@angular/router'; 5 | import { MaterialModule } from '../material.module'; 6 | import { ApiService } from '../.api-client/services/api.service'; 7 | import { Article } from '../.api-client/models/article'; 8 | import { environment } from '../environments/environment'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html', 13 | styleUrls: ['./app.component.scss'], 14 | imports: [RouterOutlet, AsyncPipe, DatePipe, MaterialModule] 15 | }) 16 | export class AppComponent { 17 | $exampleText!: Observable; 18 | $exampleArticle!: Observable
; 19 | 20 | isProduction = environment.production; 21 | 22 | constructor(private api: ApiService) {} 23 | 24 | getExampleText() { 25 | this.$exampleText = this.api.appControllerGetExampleText(); 26 | } 27 | 28 | getExampleArticle() { 29 | this.$exampleArticle = this.api.appControllerGetExampleArticle(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = []; 4 | -------------------------------------------------------------------------------- /client/src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client 6 | 7 | 8 | 9 | 10 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { importProvidersFrom } from '@angular/core'; 2 | import { bootstrapApplication } from '@angular/platform-browser'; 3 | import { provideAnimations } from '@angular/platform-browser/animations'; 4 | import { AppComponent } from './app/app.component'; 5 | import { ApiModule } from './.api-client/api.module'; 6 | import { 7 | provideHttpClient, 8 | withInterceptorsFromDi, 9 | } from '@angular/common/http'; 10 | import { provideRouter } from '@angular/router'; 11 | import { routes } from './app/app.routes'; 12 | 13 | bootstrapApplication(AppComponent, { 14 | providers: [ 15 | importProvidersFrom( 16 | ApiModule.forRoot({ 17 | rootUrl: '', 18 | }), 19 | ), 20 | provideHttpClient(withInterceptorsFromDi()), 21 | provideAnimations(), 22 | provideRouter(routes), 23 | ], 24 | }).catch((err) => console.error(err)); 25 | -------------------------------------------------------------------------------- /client/src/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 3 | import { MatBadgeModule } from '@angular/material/badge'; 4 | import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 7 | import { MatCardModule } from '@angular/material/card'; 8 | import { MatCheckboxModule } from '@angular/material/checkbox'; 9 | import { MatChipsModule } from '@angular/material/chips'; 10 | import { MatStepperModule } from '@angular/material/stepper'; 11 | import { MatDatepickerModule } from '@angular/material/datepicker'; 12 | import { MatDialogModule } from '@angular/material/dialog'; 13 | import { MatDividerModule } from '@angular/material/divider'; 14 | import { MatExpansionModule } from '@angular/material/expansion'; 15 | import { MatGridListModule } from '@angular/material/grid-list'; 16 | import { MatIconModule } from '@angular/material/icon'; 17 | import { MatInputModule } from '@angular/material/input'; 18 | import { MatListModule } from '@angular/material/list'; 19 | import { MatMenuModule } from '@angular/material/menu'; 20 | import { MatNativeDateModule, MatRippleModule } from '@angular/material/core'; 21 | import { MatPaginatorModule } from '@angular/material/paginator'; 22 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 23 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 24 | import { MatRadioModule } from '@angular/material/radio'; 25 | import { MatSelectModule } from '@angular/material/select'; 26 | import { MatSidenavModule } from '@angular/material/sidenav'; 27 | import { MatSliderModule } from '@angular/material/slider'; 28 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 29 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 30 | import { MatSortModule } from '@angular/material/sort'; 31 | import { MatTableModule } from '@angular/material/table'; 32 | import { MatTabsModule } from '@angular/material/tabs'; 33 | import { MatToolbarModule } from '@angular/material/toolbar'; 34 | import { MatTooltipModule } from '@angular/material/tooltip'; 35 | import { MatTreeModule } from '@angular/material/tree'; 36 | 37 | @NgModule({ 38 | exports: [ 39 | MatAutocompleteModule, 40 | MatBadgeModule, 41 | MatBottomSheetModule, 42 | MatButtonModule, 43 | MatButtonToggleModule, 44 | MatCardModule, 45 | MatCheckboxModule, 46 | MatChipsModule, 47 | MatStepperModule, 48 | MatDatepickerModule, 49 | MatDialogModule, 50 | MatDividerModule, 51 | MatExpansionModule, 52 | MatGridListModule, 53 | MatIconModule, 54 | MatInputModule, 55 | MatListModule, 56 | MatMenuModule, 57 | MatNativeDateModule, 58 | MatPaginatorModule, 59 | MatProgressBarModule, 60 | MatProgressSpinnerModule, 61 | MatRadioModule, 62 | MatRippleModule, 63 | MatSelectModule, 64 | MatSidenavModule, 65 | MatSliderModule, 66 | MatSlideToggleModule, 67 | MatSnackBarModule, 68 | MatSortModule, 69 | MatTableModule, 70 | MatTabsModule, 71 | MatToolbarModule, 72 | MatTooltipModule, 73 | MatTreeModule, 74 | ], 75 | }) 76 | export class MaterialModule {} 77 | -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | body { 8 | margin: 0; 9 | font-family: Roboto, 'Helvetica Neue', sans-serif; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": ["src/main.ts"], 10 | "include": ["src/**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "baseUrl": "./", 7 | "outDir": "./dist/out-tsc", 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "useDefineForClassFields": false, 23 | "lib": ["ES2022", "dom"] 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": ["jasmine"] 8 | }, 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | run: npm start 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-nest", 3 | "private": true, 4 | "engines": { 5 | "node": ">=22" 6 | }, 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=production npm run start:prod --workspace=server", 9 | "start:dev": "npm run dev", 10 | "dev": "npm run build-api-client && npm run generate-dev-dummy-page && cross-env NODE_ENV=development concurrently --names ng,nest,apigen --kill-others \"npm run start:dev --workspace=client\" \"npm run start:dev --workspace=server\" \"nodemon --watch server/src/ --ext ts .utils/api-client-generator-launcher.js online\"", 11 | "build": "npm run build --workspace=server && npm run build-api-client && npm run build --workspace=client", 12 | "build-api-client": "node .utils/api-client-generator-launcher.js", 13 | "test": "npm run test --workspace=server && npm run build-api-client && npm run test --workspace=client", 14 | "generate-dev-dummy-page": "node .utils/dev-dummy-page-generator.js", 15 | "ng": "npx --workspace=client ng", 16 | "nest": "npx --workspace=server nest" 17 | }, 18 | "devDependencies": { 19 | "concurrently": "^7.1.0", 20 | "mkdirp": "^3.0.1", 21 | "nodemon": "^2.0.15", 22 | "prettier": "^3.2.5" 23 | }, 24 | "workspaces": [ 25 | "client", 26 | "server" 27 | ], 28 | "dependencies": { 29 | "cross-env": "^7.0.3", 30 | "json2yaml": "^1.1.0", 31 | "ng-openapi-gen": "^0.51.0", 32 | "node-fetch": "^2.6.7", 33 | "typescript": "5.6.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: app 4 | env: node 5 | buildCommand: npm install --include=dev; npm run build 6 | startCommand: npm start 7 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | /junit.xml 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # API document 39 | api.yaml 40 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /server/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import globals from 'globals'; 6 | 7 | import eslintConfigPrettier from 'eslint-config-prettier'; 8 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 9 | 10 | export default [ 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommended, 13 | eslintConfigPrettier, 14 | eslintPluginPrettierRecommended, 15 | { 16 | languageOptions: { 17 | globals: { 18 | ...globals.node, 19 | ...globals.jest, 20 | }, 21 | }, 22 | ignores: ['.eslintrc.js'], 23 | rules: { 24 | '@typescript-eslint/interface-name-prefix': 'off', 25 | '@typescript-eslint/explicit-function-return-type': 'off', 26 | '@typescript-eslint/explicit-module-boundary-types': 'off', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | }, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "nest build", 8 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 9 | "start": "nest start", 10 | "start:dev": "nest start --watch", 11 | "start:debug": "nest start --debug --watch", 12 | "start:prod": "node dist/main", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:cov": "jest --coverage", 17 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 18 | "test:e2e": "jest --config ./test/jest-e2e.json" 19 | }, 20 | "dependencies": { 21 | "@nestjs/common": "^10.3.2", 22 | "@nestjs/core": "^10.3.2", 23 | "@nestjs/platform-express": "^10.3.2", 24 | "@nestjs/serve-static": "^4.0.2", 25 | "@nestjs/swagger": "^7.2.0", 26 | "reflect-metadata": "^0.1.13", 27 | "rimraf": "^3.0.2", 28 | "rxjs": "^7.8.1", 29 | "swagger-ui-express": "4.5" 30 | }, 31 | "devDependencies": { 32 | "@eslint/js": "^9.5.0", 33 | "@nestjs/cli": "^10.3.2", 34 | "@nestjs/schematics": "^10.1.1", 35 | "@nestjs/testing": "^10.3.2", 36 | "@types/eslint__js": "^8.42.3", 37 | "@types/express": "^4.17.13", 38 | "@types/jest": "^29.5.14", 39 | "@types/node": "^20.8.7", 40 | "@types/supertest": "^2.0.11", 41 | "eslint": "^9.5.0", 42 | "eslint-config-prettier": "^9.1.0", 43 | "eslint-plugin-prettier": "^5.1.3", 44 | "globals": "^15.6.0", 45 | "jest": "^29.7.0", 46 | "jest-junit": "^16.0.0", 47 | "source-map-support": "^0.5.20", 48 | "supertest": "^6.1.3", 49 | "ts-jest": "^29.2.5", 50 | "ts-loader": "^9.2.3", 51 | "ts-node": "^10.0.0", 52 | "tsconfig-paths": "^3.10.1", 53 | "typescript": "5.6.3", 54 | "typescript-eslint": "^8.26.1" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".*\\.spec\\.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node", 72 | "moduleNameMapper": { 73 | "src/(.*)": "/$1" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from 'src/app.controller'; 3 | import { AppService } from 'src/app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return string with "Hello World"', () => { 19 | expect(appController.getExampleText()).toContain('Hello World'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { ApiOperation, ApiProduces, ApiResponse } from '@nestjs/swagger'; 4 | import { Article } from './entities/article.entity'; 5 | 6 | @Controller() 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | 10 | // /api/text 11 | @Get('text') 12 | @ApiOperation({ summary: 'Get example text (string)' }) 13 | @ApiResponse({ 14 | type: String, // Tells Angular that this method returns a string 15 | }) 16 | @ApiProduces('text/plain') // Tells Angular to treat the response as plain text (Don't parse it as JSON) 17 | getExampleText(): string { 18 | return this.appService.getExampleString(); 19 | } 20 | 21 | // /api/article 22 | @Get('article') 23 | @ApiOperation({ summary: 'Get example article (object)' }) 24 | @ApiResponse({ 25 | type: Article, // Tells Angular that this method returns an Article object 26 | }) 27 | getExampleArticle(): Article { 28 | return this.appService.getExampleArticle(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ServeStaticModule } from '@nestjs/serve-static'; 3 | import { join } from 'path'; 4 | import { AppController } from './app.controller'; 5 | import { AppService } from './app.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | // Serve the frontend (Angular) app (../../client/dist/browser/) as a static files 10 | // (for production environment) 11 | ServeStaticModule.forRoot({ 12 | rootPath: join(__dirname, '..', '..', 'client', 'dist', 'browser'), 13 | }), 14 | ], 15 | controllers: [AppController], 16 | providers: [AppService], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /server/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Article } from './entities/article.entity'; 3 | 4 | @Injectable() 5 | export class AppService { 6 | getExampleString(): string { 7 | return `Hello World! (${new Date()})`; 8 | } 9 | 10 | getExampleArticle(): Article { 11 | const randomNumber = Math.floor(Math.random() * 1000); 12 | return { 13 | id: randomNumber, 14 | title: `Example article ${randomNumber}`, 15 | updatedAt: new Date(), 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/entities/article.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class Article { 4 | @ApiProperty({ 5 | example: 1, 6 | description: 'The ID of the article', 7 | }) 8 | id: number; 9 | 10 | @ApiProperty({ 11 | example: 'Example article 1', 12 | description: 'The title of the article', 13 | }) 14 | title: string; 15 | 16 | @ApiProperty({ 17 | description: 'The updated date of the article', 18 | }) 19 | updatedAt: Date; 20 | } 21 | -------------------------------------------------------------------------------- /server/src/helper.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | 4 | export class Helper { 5 | static getOpenAPIDoc(app: INestApplication) { 6 | const doc_options = new DocumentBuilder() 7 | .setTitle(`API Document`) 8 | // Output securitySchemes to support Bearer authentication 9 | // See: https://docs.nestjs.com/openapi/security#bearer-authentication 10 | .addBearerAuth() 11 | .build(); 12 | 13 | const doc = SwaggerModule.createDocument(app, doc_options); 14 | return doc; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | import { AppModule } from './app.module'; 4 | import { Helper } from './helper'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | 9 | // Change the URL prefix to `/api` on backend 10 | app.setGlobalPrefix('api'); 11 | 12 | // Enable Swagger UI for development env 13 | if (process.env.NODE_ENV && process.env.NODE_ENV === 'development') { 14 | // Build the OpenAPI document (published under `/api/docs`) with Swagger 15 | const doc = Helper.getOpenAPIDoc(app); 16 | SwaggerModule.setup('api/docs', app, doc); 17 | } 18 | 19 | // Start the server 20 | await app.listen(process.env.PORT || 3000); 21 | } 22 | bootstrap(); 23 | -------------------------------------------------------------------------------- /server/src/openapi-doc-generator.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 3 | import * as fs from 'fs'; 4 | import { AppModule } from './app.module'; 5 | import { Helper } from './helper'; 6 | 7 | async function bootstrap(): Promise { 8 | // Get the modules included by AppModule 9 | const imports = Reflect.getMetadata('imports', AppModule); 10 | const modules = imports.filter((item: any) => { 11 | return typeof item === 'function'; 12 | }); 13 | 14 | // Get the controllers and providers 15 | let controllers = Reflect.getMetadata('controllers', AppModule); 16 | let providers = Reflect.getMetadata('providers', AppModule); 17 | for (const mod of modules) { 18 | controllers = controllers.concat(Reflect.getMetadata('controllers', mod)); 19 | providers = providers.concat(Reflect.getMetadata('providers', mod)); 20 | } 21 | 22 | // Generate the mock providers 23 | const mockedProviders = providers 24 | .filter((provider: any) => { 25 | return provider !== undefined; 26 | }) 27 | .map((provider: any) => { 28 | return { 29 | provide: provider, 30 | useValue: {}, 31 | }; 32 | }); 33 | 34 | // Generate an application instance 35 | const testingModule = await Test.createTestingModule({ 36 | imports: [AppModule], 37 | controllers: controllers, 38 | providers: mockedProviders, 39 | }).compile(); 40 | 41 | const app = testingModule.createNestApplication(); 42 | 43 | // Change the URL prefix to `/api` on backend 44 | app.setGlobalPrefix('api'); 45 | 46 | // Generate API JSON 47 | const doc = Helper.getOpenAPIDoc(app); 48 | const docJson = JSON.stringify(doc); 49 | 50 | // Check the argument 51 | let outputPath = null; 52 | for (const arg of process.argv) { 53 | if (arg.match(/^--output=(.+)$/)) { 54 | outputPath = RegExp.$1; 55 | break; 56 | } 57 | } 58 | 59 | // Output JSON document 60 | if (outputPath) { 61 | fs.writeFileSync(outputPath, docJson); 62 | } else { 63 | console.log(docJson); 64 | } 65 | } 66 | 67 | bootstrap(); 68 | -------------------------------------------------------------------------------- /server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/test (GET)', () => { 19 | return request(app.getHttpServer()).get('/text').expect(200); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "moduleNameMapper": { 10 | "src/(.*)": "/../src/$1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | }, 21 | "watchOptions": { 22 | "watchFile": "fixedPollingInterval" 23 | } 24 | } 25 | --------------------------------------------------------------------------------