├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── package.json ├── packages ├── client │ ├── .env │ ├── .eslintrc │ ├── @types │ │ └── globals.d.ts │ ├── Dockerfile │ ├── README.md │ ├── index.html │ ├── nginx.conf │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── components │ │ │ └── App │ │ │ │ ├── App.tsx │ │ │ │ └── index.ts │ │ ├── config.ts │ │ ├── index.css │ │ ├── index.tsx │ │ └── utils │ │ │ ├── checkServerVersion.ts │ │ │ ├── getApiUrl.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ ├── tsconfig.json │ └── vite.config.ts ├── domain │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── interfaces │ │ │ ├── dictionary.interface.ts │ │ │ └── index.ts │ ├── tsconfig-cjs.json │ ├── tsconfig-mjs.json │ └── tsconfig.json ├── lib │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig-cjs.json │ ├── tsconfig-mjs.json │ └── tsconfig.json └── server │ ├── Dockerfile │ ├── README.md │ ├── env │ └── example.env │ ├── nest-cli.json │ ├── package.json │ ├── src │ ├── jest-setup.ts │ ├── main.ts │ └── modules │ │ ├── api.module.ts │ │ ├── config │ │ ├── config.module.ts │ │ └── config.service.ts │ │ └── status │ │ ├── status.controller.test.ts │ │ ├── status.controller.ts │ │ ├── status.module.ts │ │ └── status.service.ts │ ├── test │ ├── app.e2e-test.ts │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── scripts ├── build_and_push.sh ├── fix-common-package-exports.sh ├── generate_version.sh └── getPackageVersion.js ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | **/env 2 | **/dist 3 | **/node_modules 4 | .editorconfig 5 | .gitignore 6 | .nvmrc 7 | .prettierrc 8 | .vscode 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "jest": true 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "project": "tsconfig.json", 12 | "ecmaFeatures": { 13 | "modules": true 14 | }, 15 | "ecmaVersion": 2020 16 | }, 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "prettier" 21 | ], 22 | "plugins": ["@typescript-eslint", "prettier"], 23 | "rules": { 24 | "consistent-return": "error", 25 | "constructor-super": "error", 26 | "curly": "error", 27 | "default-case": "error", 28 | "dot-notation": "error", 29 | "eqeqeq": ["error", "smart"], 30 | "guard-for-in": "error", 31 | "max-len": [1, { "code": 180, "ignoreUrls": true }], 32 | "no-bitwise": "error", 33 | "no-cond-assign": "error", 34 | "no-console": [ 35 | "warn", 36 | { 37 | "allow": [ 38 | "warn", 39 | "dir", 40 | "timeLog", 41 | "assert", 42 | "clear", 43 | "count", 44 | "countReset", 45 | "group", 46 | "groupEnd", 47 | "table", 48 | "dirxml", 49 | "groupCollapsed", 50 | "Console", 51 | "profile", 52 | "profileEnd", 53 | "timeStamp", 54 | "context" 55 | ] 56 | } 57 | ], 58 | "no-debugger": "error", 59 | "no-duplicate-case": "error", 60 | "no-duplicate-imports": "error", 61 | "no-empty": "error", 62 | "no-eval": "error", 63 | "no-fallthrough": "error", 64 | "no-invalid-this": "off", 65 | "no-multiple-empty-lines": "off", 66 | "no-new-wrappers": "error", 67 | "no-param-reassign": [ 68 | "error", 69 | { 70 | "props": false 71 | } 72 | ], 73 | "no-redeclare": "error", 74 | "no-return-assign": "error", 75 | "no-return-await": "error", 76 | "no-throw-literal": "error", 77 | "no-trailing-spaces": "off", 78 | "no-unsafe-finally": "error", 79 | "no-unused-expressions": "warn", 80 | "no-unused-labels": "error", 81 | "no-var": "error", 82 | "no-void": "error", 83 | "prefer-const": "error", 84 | "prettier/prettier": ["warn"], 85 | "radix": "error", 86 | "sort-imports": ["warn", { "ignoreDeclarationSort": true }], 87 | "spaced-comment": "error", 88 | "@typescript-eslint/adjacent-overload-signatures": "error", 89 | "@typescript-eslint/array-type": "error", 90 | "@typescript-eslint/explicit-function-return-type": [ 91 | "warn", 92 | { "allowExpressions": true } 93 | ], 94 | "@typescript-eslint/indent": "off", 95 | "@typescript-eslint/interface-name-prefix": "off", 96 | "@typescript-eslint/member-delimiter-style": [ 97 | "off", 98 | { 99 | "multiline": { 100 | "delimiter": "none", 101 | "requireLast": true 102 | }, 103 | "singleline": { 104 | "delimiter": "semi", 105 | "requireLast": false 106 | } 107 | } 108 | ], 109 | "@typescript-eslint/naming-convention": [ 110 | "warn", 111 | { 112 | "selector": "default", 113 | "format": ["camelCase"], 114 | "leadingUnderscore": "allow", 115 | "trailingUnderscore": "allow" 116 | }, 117 | { 118 | "selector": "variable", 119 | "format": ["camelCase", "UPPER_CASE"], 120 | "leadingUnderscore": "allow", 121 | "trailingUnderscore": "allow" 122 | }, 123 | { 124 | "selector": "typeLike", 125 | "format": ["PascalCase"] 126 | }, 127 | { 128 | "selector": "enumMember", 129 | "format": ["PascalCase"] 130 | } 131 | ], 132 | "@typescript-eslint/no-empty-function": "warn", 133 | "@typescript-eslint/no-empty-interface": "error", 134 | "@typescript-eslint/no-explicit-any": "warn", 135 | "@typescript-eslint/no-namespace": "error", 136 | "@typescript-eslint/no-non-null-assertion": "off", 137 | "@typescript-eslint/no-shadow": ["error"], 138 | "@typescript-eslint/no-throw-literal": "error", 139 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 140 | "@typescript-eslint/no-unused-vars": "warn", 141 | "@typescript-eslint/no-use-before-define": "off", 142 | "@typescript-eslint/prefer-for-of": "error", 143 | "@typescript-eslint/promise-function-async": "error", 144 | "@typescript-eslint/quotes": "off", 145 | "@typescript-eslint/semi": ["off", null], 146 | "@typescript-eslint/triple-slash-reference": "error", 147 | "@typescript-eslint/type-annotation-spacing": "error", 148 | "@typescript-eslint/unified-signatures": "error" 149 | }, 150 | "overrides": [ 151 | { 152 | "files": ["**/*.js"], 153 | "rules": { 154 | "@typescript-eslint/explicit-function-return-type": "off", 155 | "@typescript-eslint/no-var-requires": "off" 156 | } 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs and editors 2 | .idea 3 | .project 4 | .classpath 5 | .c9/ 6 | *.launch 7 | .settings/ 8 | *.sublime-workspace 9 | .vscode/* 10 | !.vscode/settings.json 11 | !.vscode/tasks.json 12 | !.vscode/launch.json 13 | !.vscode/extensions.json 14 | 15 | # dependencies 16 | node_modules/ 17 | .pnp/ 18 | .pnp.js 19 | 20 | # testing 21 | coverage 22 | .nyc_output 23 | 24 | # Environment files 25 | *.env 26 | !example.env 27 | docker-compose.*.yml 28 | 29 | # build 30 | build/ 31 | dist/ 32 | dist-ssr/ 33 | VERSION 34 | 35 | # misc 36 | .DS_Store 37 | 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.16.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "proseWrap": "preserve", 5 | "endOfLine": "lf", 6 | "singleQuote": true, 7 | "semi": true, 8 | "trailingComma": "es5", 9 | "tabWidth": 2, 10 | "useTabs": false 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8000", 12 | "webRoot": "${workspaceFolder}", 13 | "sourceMapPathOverrides": { 14 | "../*": "${webRoot}/*" 15 | } 16 | }, 17 | { 18 | "type": "chrome", 19 | "request": "attach", 20 | "name": "Attach to Chrome", 21 | "port": 9222, 22 | "webRoot": "${workspaceFolder}", 23 | "sourceMapPathOverrides": { 24 | "../*": "${webRoot}/*" 25 | } 26 | }, 27 | { 28 | "type": "node", 29 | "request": "attach", 30 | "name": "Server debug", 31 | "port": 9229, 32 | "restart": true 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "**/*.code-search": true, 7 | "**/.cache": true, 8 | "**/dist": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this Nest - React boilerplate 2 | 3 | ## Coding styles 4 | 5 | As the TypeScript project doesn't issue an "official style guide", this boilerplate mostly follows the [Standard JavaScript rules](https://standardjs.com/rules.html) for generic JavaScript declaration, and the [TypeScript book StyleGuide and Coding Conventions](https://basarat.gitbook.io/typescript/styleguide) for TypeScript specific syntax. 6 | 7 | As a quick summary, here are the main naming conventions: 8 | 9 | - Use `camelCase` for **variable** and **function** names 10 | 11 | - Use `PascalCase` for **class**, **interface**, **type** and **enum** names 12 | 13 | On top of these simple rules, this repository uses **exclusively named** exports and avoids the default exports for many reasons which are very well summarised in [this blog post](https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/) from the `ESLint` author. For similar reasons and to enable tree-shaking when possible, the "import all" syntax is avoided, replacing ~~`import * as libName from "libName";`~~ by `import { libFunc, libObject } from "libName";`. 14 | 15 | ## File structure and naming 16 | 17 | To ensure we can easily know what a file content is about, we enforce the following rules throughout the codebase: 18 | 19 | - **React component** files are named after their main exported component. This is why they are the ONLY files using the `PascalCase` for naming. They also have the `.tsx` extension. 20 | 21 | - Each **React component** lives in its own folder, right under the `packages/client/src/components` folder. 22 | 23 | - **All other TS** files are named using the `camelCase` convention. On top of that, when a file defines a common type of object, the type is appended to the file name. For example, the `Dictionary` interface is defined in the `dictionary.interface.ts` file, and the `HelloController` class is defined in the `hello.controller.ts`. 24 | 25 | ## Development tools 26 | 27 | ### Editorconfig 28 | 29 | Editorconfig easily integrates with [many text editors and IDEs](https://editorconfig.org/#download) — some natively, for example: 30 | 31 | - VS Code: [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 32 | 33 | - Vim / NeoVim: [EditorConfig plugin for Vim](https://github.com/editorconfig/editorconfig-vim) 34 | 35 | - JetBrains IDEs: [EditorConfig plugin](https://plugins.jetbrains.com/plugin/7294-editorconfig) 36 | 37 | ### ESLint 38 | 39 | ESLint easily integrates with [many text editors and IDEs](https://eslint.org/docs/user-guide/integrations), for example: 40 | 41 | - VS Code: [ESLint for VS Code](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 42 | 43 | - Vim / NeoVim: 44 | 45 | - [Ale](https://github.com/dense-analysis/ale) 46 | - [Syntastic](https://github.com/vim-syntastic/syntastic/tree/master/syntax_checkers/javascript) 47 | 48 | - JetBrains IDEs: [ESLint plugin](https://plugins.jetbrains.com/plugin/7494-eslint) 49 | 50 | ### Prettier 51 | 52 | Prettier easily integrates [many text editors and IDEs](https://prettier.io/), for example: 53 | 54 | - VS Code: [Prettier for VS Code](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 55 | 56 | - Vim / NeoVim: 57 | 58 | - [Neoformat](https://github.com/sbdchd/neoformat) 59 | - [Ale](https://github.com/w0rp/ale) 60 | - [Vim Prettier](https://github.com/prettier/vim-prettier) 61 | 62 | - JetBrains IDEs: built-in support 63 | 64 | This boilerplate has a `.vscode` folder with the following setting to help us have a smooth experience with formatting: 65 | 66 | ```json 67 | { 68 | "editor.formatOnSave": true 69 | } 70 | ``` 71 | 72 | Other IDEs and text editors usually offer similar features to help you ensure that the code is automatically formatted the way Prettier expects. 73 | 74 | ### TypeScript 75 | 76 | VS Code and WebStorm both fully support TypeScript natively. For [Vim / NeoVim](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support#vim), here are some tools to help you with the syntax highlighting and syntax error detections, etc.: 77 | 78 | - [TypeScript Syntax for Vim](https://github.com/leafgarland/typescript-vim): for syntax highlighting 79 | 80 | - [Tsuquyomi](https://github.com/Quramy/tsuquyomi): For essential IDE features like: completion (omni-completion), navigation to the location where a symbol is defined, showing location(s) where a symbol is referenced, displaying a list of syntax and semantics errors to Vim quickfix window, etc. 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nest - React boilerplate 2 | 3 | This is a basic boilerplate to quickly set up a web application **fully written in [TypeScript](https://www.typescriptlang.org/)** (^4.7.4) based on: 4 | 5 | - [NestJS](https://nestjs.com/) (^9.0.11) for the **server**: [> Go to the server package](./packages/server) 6 | 7 | > _« A progressive Node.js framework for building efficient, reliable and scalable server-side applications. »_ 8 | 9 | - [React + ReactDOM](https://reactjs.org/) (^18.2.0) for the **client**: [> Go to the client package](./packages/client) 10 | 11 | > _« A JavaScript library for building user interfaces »_ 12 | 13 | - [Vite](https://vitejs.dev/) (^3.0.9): Based on ESBuild and Rollup, this tool combines speed, performance and configurability to offer the best frontend DX possible 14 | 15 | > _« Next Generation Frontend Tooling »_ 16 | 17 | ## Features 18 | 19 | While being minimalistic, this boilerplate offers a number of features which can be very valuable for the Development Experience (DX): 20 | 21 | ### Global 22 | 23 | - Makes use of the [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) to centralise the package management system for all the internal packages. 24 | 25 | - TypeScript ^4.7.4 which comes with, for example, **optional chaining** and customised [import paths](#typescript-import-paths) already defined for each package. 26 | 27 | - EditorConfig + Prettier for [code formatting](#code-formatting). 28 | 29 | - Full ESLint configurations for [linting](#linting). 30 | 31 | - Consistent coding style following the standards. See [CONTRIBUTING](./CONTRIBUTING.md#coding-styles). 32 | 33 | - Development scripts: `yarn start:dev` can be run in any package. See [Development & builds](#development--builds) for more information. 34 | 35 | - Visual Studio Code [debug settings](.vscode/launch.json). 36 | 37 | ### Client 38 | 39 | - [Vite's Hot Module Replacement](https://vitejs.dev/guide/features.html#hot-module-replacement) combined with the [React Fast Refresh](https://github.com/facebook/react/tree/main/packages/react-refresh) offers an incredibly fast development process. When you edit and save a source file, it will only reload the corresponding module in the development server AND only **re-render the depending components without losing their state**! 40 | 41 | - Debugger tool so you can avoid using the native but synchronous and greed `console`'s methods. For more information, see the client README section about the [Debug library](./packages/client#debug-library). 42 | 43 | - Production ready [NGINX](https://nginx.org/) configuration example to optimise your frontend file delivery. 44 | 45 | - Production ready [Dockerfile](#docker-images). 46 | 47 | ### Server 48 | 49 | - NestJS basic package with all the Nest tools. See the [server README](./packages/server/) for more information. 50 | 51 | - A predefined **global config module** to handle all the configuration you would like to pass to your server at runtime. You can lean more in the server's README [Configuration module](./packages/server/README.md#configuration-module) section. 52 | 53 | - Production ready [Dockerfile](#docker-images). 54 | 55 | 56 | ### Client/Server versions 57 | 58 | 59 | While being minimalistic, this boilerplate provides straight-forward access to the client's or version's deployed version: 60 | 61 | 1. To check the server's version, simply call the [`/version`](http://localhost:4000/version) endpoint which returns a JSON looking like this: 62 | 63 | ```json 64 | { 65 | "GIT_SHORT_HASH": "568cfad", 66 | "GIT_BRANCH": "master", 67 | "REPO_VERSION": "1.0.0", 68 | "DOMAIN_VERSION": "1.0.0", 69 | "LIB_VERSION": "1.0.0", 70 | "SERVER_VERSION": "1.0.0" 71 | } 72 | ``` 73 | 74 | 2. To identify the client's deployed version, you can see the page's source code and look for the JS bundle name, which should look like: `index.39a2462@master.c177f4e7.js`. This corresponds to the pattern passed in the [`vite.config.ts`](./packages/client/vite.config.ts) file: `[name].${getBuildId()}.[hash].js`. Currently, the `buildId` is defined as `shortHash@branch` but you can adapt the `getBuildId` function to your needs. 75 | 76 | 3. Since the two applications are supposed to be deployed as separate Docker images, this boilerplate comes with a simple function embedded in the frontend: [`checkServerVersion`](./packages/client/src/utils/checkServerVersion.ts). If the server version doesn't satisfy the frontend **peer dependency**, an error message will be printed in the frontend console (using the debug library). 77 | 78 | --- 79 | 80 | ## How to use this boilerplate 81 | 82 | First, you'll need to download and adapt it to your project: 83 | 84 | 1. You can use the [Use this template](https://github.com/LandazuriPaul/nest-react/generate) feature from GitHub to generate a new project based on this boilerplate. Alternatively, you can clone this repository to a brand new folder named after your `new-project`: 85 | 86 | ```sh 87 | git clone git@github.com:LandazuriPaul/nest-react.git new-project 88 | ``` 89 | 90 | > For steps 2 to 5, a global `search in all files` command from any decent editor should help. You can simply search for `nest-react` and replace it by your `new-project`. 91 | 92 | 2. Change the main project's name, set in the root [`package.json`](./package.json)'s `name` field and in its `scripts` commands. 93 | 94 | 3. Change each package's name, set in its own `package.json`'s `name` field. 95 | 96 | 4. Update the `dependencies` of each package requiring one of the internal packages: 97 | 98 | - Server: [`package.json`](./packages/server/package.json) 99 | - Client: [`package.json`](./packages/client/package.json) 100 | 101 | 5. Change the client debug `LOGGER_PREFIX` which is set in the [`config.ts`](./packages/client/src/config.ts) file. For more information, see the client README section about the [Debug library](./packages/client#debug-library). 102 | 103 | 6. Adapt the [`packages/client/public`](./packages/client/public) folder to your project (with your icons, manifest, robots.txt files). 104 | 105 | ### Project installation 106 | 107 | Once you're done with the previous steps, you can properly install the project dependencies and link the packages together: 108 | 109 | 1. Basic requirements to run the repository: 110 | 111 | - [Node.js](https://nodejs.org/en/): The recommended way is via [`nvm`](https://github.com/nvm-sh/nvm). You can then install the version used for this project: 112 | ```sh 113 | nvm install 16.16.0 114 | ``` 115 | - [Yarn](https://classic.yarnpkg.com/): If you have `nvm` installed, you'd prefer to install `yarn` without the node dependency. To do so, the `bash` install is the easiest: 116 | ```sh 117 | curl -o- -L https://yarnpkg.com/install.sh | bash 118 | ``` 119 | 120 | > As the boilerplate makes use of the yarn workspaces, you shouldn't use `npm`. 121 | 122 | 2. Install dependencies with the classic: 123 | 124 | ```sh 125 | yarn install 126 | ``` 127 | 128 | > This will install all package dependencies in a common `node_modules` folder at the root of the project using a single `yarn.lock` file to avoid conflicting dependencies. The internal dependencies will be replaced by symbolic links to the corresponding packages. 129 | 130 | 3. Finally, in order to have the "common" packages (`lib` and `domain`) built so they can be used by both the `server` and the `client`, run: 131 | 132 | ```sh 133 | yarn build:common 134 | ``` 135 | 136 | Or if you want the common packages to be **watched for file changes**, you can run: 137 | 138 | ```sh 139 | yarn start:common 140 | ``` 141 | 142 | #### Note about subsequent installations 143 | 144 | When you want to add new dependencies to any of the packages, you can either: 145 | 146 | - Run `yarn add ` in the corresponding package folder. 147 | - Or run `yarn workspace add ` from the root folder. 148 | 149 | ### Development & Builds 150 | 151 | See each package's README to learn more about its development and build scripts: 152 | 153 | - [Client](./packages/client/README.md) 154 | 155 | - [Server](./packages/server/README.md) 156 | 157 | --- 158 | 159 | ## Code formatting 160 | 161 | - [EditorConfig](https://editorconfig.org/): _« helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. »_ 162 | 163 | - Rules are set in the root [`.editorconfig`](./.editorconfig) file. 164 | 165 | - [Prettier](https://prettier.io/) (^1.19.1): _« An opinionated code formatter »_ which _« saves you time and energy »_. 166 | 167 | - Rules are set in the root [`.prettierrc`](./.prettierrc) file. 168 | 169 | ## Linting 170 | 171 | [ESLint](https://eslint.org/) (^8.22.0) with [TypeScript parser](https://github.com/typescript-eslint/typescript-eslint) (^5.33.1): _« Find and fix problems in your JavaScript code »_ 172 | 173 | - Project rules are set in the root [`.eslintrc`](./.eslintrc) file. 174 | 175 | - As the client package requires specific React related rules, it has its own [`.eslintrc`](./packages/client/.eslintrc) file which extends the project one. 176 | 177 | To see how to integrates these tools with your favourite IDE or text editor, you can see the CONTRIBUTING [Development tools](./CONTRIBUTING.md#development-tools) section. 178 | 179 | Each package has its own 180 | 181 | ```sh 182 | yarn lint 183 | ``` 184 | 185 | command to ensure that its source code is written according to the ESLint rules. The project itself also has a root `yarn lint` command to sequentially run it in each internal package. 186 | 187 | ## TypeScript import paths 188 | 189 | As you can see in all packages' `tsconfig.json` files, both the `baseUrl` and `paths` properties are defined to help you avoid the cumbersome and error-prone `../../` import paths (amongst other options): 190 | 191 | ```json 192 | // packages' tsconfig.json 193 | { 194 | "extends": "../../tsconfig.json", 195 | "compilerOptions": { 196 | "baseUrl": ".", 197 | "outDir": "dist", 198 | "paths": { 199 | "~/*": ["src/*"] 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | This allows you to `import` any file from the **same package** with the `'~/path/to/file/'` notation, considering the `src` folder as the package's _home_ (i.e. `~`). 206 | 207 | ## Docker images 208 | 209 | This project comes with a `Dockerfile` for each package likely to be deployed. They are all based on the [alpine](https://alpinelinux.org/) project. 210 | 211 | To build the corresponding Docker images, you can use the [build_and_push.sh](./scripts/build_and_push.sh) script by setting the `PACKAGE` and optionally the `VERSION` — defaults to `latest` — as environment variables or simply use the dedicated `yarn` commands (the `latest` version will be applied): 212 | 213 | ```sh 214 | # To build and push the server 215 | yarn build-push:server 216 | 217 | # To build and push the client 218 | yarn build-push:client 219 | ``` 220 | 221 | ## Deployment 222 | 223 | The shipped [`docker-compose.yml`](./docker-compose.yml) file is mainly for demonstration purposes and local testing. 224 | 225 | In order to run the applications in a completely containerised environment, please refer to the [Docker documentation](https://docs.docker.com/). 226 | 227 | ## Improvements 228 | 229 | - #TODO: Add an automated script to run installation steps 2 to 5. 230 | 231 | ## License 232 | 233 | This project is licensed under the [GNU Lesser General Public License v3.0 or later](https://spdx.org/licenses/LGPL-3.0-or-later.html). You can learn more reading the [LICENSE](./LICENSE). 234 | 235 | ## Author 236 | 237 | Paul Landázuri 238 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This docker-compose is for local development / test only 2 | 3 | version: '3.6' 4 | 5 | services: 6 | # Server 7 | server: 8 | image: docker.pkg.github.com/landazuripaul/nest-react/nest-react-server:latest 9 | volumes: 10 | - ./packages/server/env/.env.local:/usr/src/nest-react/packages/server/env/.env.local 11 | ports: 12 | - '4000:4000' 13 | environment: 14 | - NODE_ENV=local 15 | 16 | # Client 17 | client: 18 | image: docker.pkg.github.com/landazuripaul/nest-react/nest-react-client:latest 19 | ports: 20 | - '8000:80' 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-react", 3 | "version": "1.0.0", 4 | "author": "Paul Landázuri", 5 | "description": "Simple boilerplate for a Nest + React project", 6 | "homepage": "https://github.com/LandazuriPaul/nest-react#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/LandazuriPaul/nest-react.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/LandazuriPaul/nest-react/issues" 13 | }, 14 | "directories": { 15 | "doc": "docs" 16 | }, 17 | "private": true, 18 | "workspaces": [ 19 | "packages/*" 20 | ], 21 | "references": [ 22 | { 23 | "path": "packages/domain" 24 | }, 25 | { 26 | "path": "packages/lib" 27 | } 28 | ], 29 | "scripts": { 30 | "generate:version": "./scripts/generate_version.sh", 31 | "start:domain": "yarn workspace @nest-react/domain start:dev", 32 | "start:lib": "yarn workspace @nest-react/lib start:dev", 33 | "start:common": "(yarn start:domain & yarn start:lib)", 34 | "build:domain": "yarn workspace @nest-react/domain build", 35 | "build:lib": "yarn workspace @nest-react/lib build", 36 | "build:common": "yarn build:domain && yarn build:lib", 37 | "build-push:server": "PACKAGE=server ./scripts/build_and_push.sh", 38 | "build-push:client": "PACKAGE=client ./scripts/build_and_push.sh", 39 | "lint": "yarn workspaces run lint" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/client/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE=My App 2 | -------------------------------------------------------------------------------- /packages/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc", "plugin:react/recommended"], 3 | "env": { 4 | "browser": true, 5 | "node": false 6 | }, 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "modules": true, 10 | "jsx": true 11 | }, 12 | "project": "tsconfig.json", 13 | "ecmaVersion": 2020, 14 | "useJSXTextNode": true 15 | }, 16 | "settings": { 17 | "react": { 18 | "version": "detect" 19 | } 20 | }, 21 | "plugins": ["@typescript-eslint", "react", "react-hooks"], 22 | "rules": { 23 | "react-hooks/exhaustive-deps": "warn", 24 | "react-hooks/rules-of-hooks": "error", 25 | "react/no-typos": "off", 26 | "react/forbid-prop-types": "off" 27 | }, 28 | "overrides": [ 29 | { 30 | "files": ["**/*.tsx"], 31 | "rules": { 32 | "@typescript-eslint/naming-convention": [ 33 | "warn", 34 | { 35 | "selector": "default", 36 | "format": ["camelCase"], 37 | "leadingUnderscore": "allow", 38 | "trailingUnderscore": "allow" 39 | }, 40 | { 41 | "selector": "variable", 42 | "format": ["camelCase", "UPPER_CASE", "PascalCase"], 43 | "leadingUnderscore": "allow", 44 | "trailingUnderscore": "allow" 45 | }, 46 | { 47 | "selector": "function", 48 | "format": ["camelCase", "PascalCase"] 49 | }, 50 | { 51 | "selector": "typeLike", 52 | "format": ["PascalCase"] 53 | }, 54 | { 55 | "selector": "enumMember", 56 | "format": ["PascalCase"] 57 | } 58 | ], 59 | "react/prop-types": "off" 60 | } 61 | }, 62 | { 63 | "files": ["vite.config.ts"], 64 | "env": { 65 | "browser": false, 66 | "node": true 67 | }, 68 | "rules": { 69 | "no-console": "off" 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /packages/client/@types/globals.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | declare const __REQUIRED_SERVER_VERSION__: string; 3 | -------------------------------------------------------------------------------- /packages/client/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder 2 | FROM node:16-alpine as builder 3 | 4 | # Copy client and domain + lib packages 5 | WORKDIR /usr/src/nest-react/ 6 | COPY .eslintrc . 7 | COPY .eslintignore . 8 | COPY package.json . 9 | COPY tsconfig.json . 10 | COPY yarn.lock . 11 | COPY scripts/fix-common-package-exports.sh scripts/fix-common-package-exports.sh 12 | COPY VERSION . 13 | 14 | COPY packages/client packages/client 15 | COPY packages/domain packages/domain 16 | COPY packages/lib packages/lib 17 | 18 | # Install domain + lib + client dependencies 19 | RUN yarn install --pure-lockfile --non-interactive 20 | 21 | # Build common packages 22 | RUN yarn build:common 23 | 24 | # Build client then 25 | WORKDIR /usr/src/nest-react/packages/client 26 | RUN yarn build 27 | 28 | # Runner 29 | FROM nginx:alpine as runner 30 | 31 | # Copy the nginx configuration 32 | COPY packages/client/nginx.conf /etc/nginx/nginx.conf 33 | 34 | # Copy the built static files to nginx + dictionaries 35 | COPY --from=builder /usr/src/nest-react/packages/client/dist /usr/share/nginx/html 36 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # Nest React boilerplate client 2 | 3 | ## Client dependencies 4 | 5 | As this boilerplate aims to be a minimal shell, it only ships 1 dependency on top of the React / ReactDOM pair and the internal dependencies: [Debug](https://github.com/visionmedia/debug) (^4.1.1). 6 | 7 | ### Debug library 8 | 9 | This simple package allows you to log messages to the browser's console without using the synchronous and greedy `console`'s methods. 10 | 11 | The boilerplate already ships a basic [`Logger`](./packages/client/src/utils/logger.ts) class which exposes basic static methods: 12 | 13 | - `info`: for debug information 14 | - `log`: alias for `info` 15 | - `warn`: for warnings 16 | - `error`: for error reporting. If you provide an instance of the `Error` type to this method's first argument, you will see its stack trace exposed in a similar way as the `console.error` would expose it. See the `useEffect` hook in the [`App`](./src/components/App/App.tsx) component for an example. 17 | 18 | Once you have successfully adapted the boilerplate to your project, as explained in main README's section [How to adapt the boilerplate](../../README.md#how-to-adapt-the-boilerplate), to see the debug messages in your browser, you just need to set up the localstorage `debug` variable. To do so, run `localStorage.debug = ':*'` in your browser console. 19 | 20 | To learn more about the debug library usage in the browser, you can check out [its documentation](https://github.com/visionmedia/debug#browser-support). 21 | 22 | ### Styling options 23 | 24 | Vite already comes with a complete set of [CSS support](https://vitejs.dev/guide/features.html#css). 25 | 26 | ## Running the app 27 | 28 | By default, the webpack dev server is listening to the [http://localhost:8000](http://localhost:8000) port. This can be configured in the [vite.config.js](./vite.config.js) file, changing the `DEV_SERVER_PORT` constant. 29 | 30 | ### In development 31 | 32 | ```sh 33 | # Runs the webpack dev server (using HMR) 34 | yarn start:dev 35 | ``` 36 | 37 | As Vite is based on ESBuild for both JavaScript and TypeScript files, it doesn't do any type check. This has a number of advantages like a much faster response hot update and avoiding many useless disrupting errors occurring during prototyping while the types aren't completely respected yet. But the disadvantage is that the bundler won't detect any TypeScript error unless it is a JavaScript error. 38 | 39 | To work around this limitation, the repository exposes a command to check your code is TypeScript compliant: 40 | 41 | ```sh 42 | yarn check-types 43 | ``` 44 | 45 | #### Pre-bundling on macOS 46 | 47 | When running for the first time in development mode, [Vite pre-bundles your dependencies](https://vitejs.dev/guide/dep-pre-bundling.html#dependency-pre-bundling). If you are running on macOS, you might see an error starting like this: `Error: ENFILE: file table overflow, scandir`. This is due to a limitation of concurrently opened files in recent macOS versions. Following [Dan MacTough's advice](http://blog.mact.me/2014/10/22/yosemite-upgrade-changes-open-file-limit), you can run the following to have this limit increased: 48 | 49 | ```sh 50 | echo kern.maxfiles=65536 | sudo tee -a /etc/sysctl.conf 51 | echo kern.maxfilesperproc=65536 | sudo tee -a /etc/sysctl.conf 52 | sudo sysctl -w kern.maxfiles=65536 53 | sudo sysctl -w kern.maxfilesperproc=65536 54 | ulimit -n 65536 65536 55 | ``` 56 | 57 | #### Debug 58 | 59 | The powerful debug feature allows any Node.js debugger tool to connect to the running process in order to define breakpoints, log points, and more. Any Chromium based browser comes with the Node inspector feature built-in. To learn more about the Node.js debugging tools, the [Node.js documentation](https://nodejs.org/de/docs/guides/debugging-getting-started/) is a nice starting point. 60 | 61 | If you use the VS Code IDE, this repository already ships 2 configurations to `launch` a Chrome debugging session or `attach` to an existing one. See the [.vscode/launch.json](../../.vscode/launch.json) file for more information. 62 | 63 | ### In production 64 | 65 | Once you are happy with your code, you can run a production version following these steps: 66 | 67 | 1. Build a production bundle: 68 | 69 | ```sh 70 | yarn build 71 | ``` 72 | 73 | 2. Then you can serve the `dist` folder from any webserver application like [NGINX](https://nginx.org/) for example. The [nginx.conf](./nginx.conf) can be used as an example of a working NGINX configuration. 74 | 75 | In order to further customise the production bundling, you can check [Vite's documentation](https://vitejs.dev/config/#build-options). Since it uses [Rollup](https://rollupjs.org/guide/en/) under the hood, you are free to adapt every single part of the bundling process and use external Rollup plugins. 76 | 77 | ### Docker image 78 | 79 | The client package has a [Dockerfile](./Dockerfile) which builds a lightweight (based on the [alpine](https://alpinelinux.org/) project) production ready Docker image. 80 | 81 | For more information about the Docker images, see the [main README.md](../../README.md#docker-images). 82 | -------------------------------------------------------------------------------- /packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Nest React boilerplate 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/client/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | pid /run/nginx.pid; 3 | 4 | events { 5 | worker_connections 768; 6 | multi_accept on; 7 | } 8 | 9 | http { 10 | sendfile on; 11 | tcp_nopush on; 12 | tcp_nodelay on; 13 | keepalive_timeout 65; 14 | types_hash_max_size 2048; 15 | 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | access_log /var/log/nginx/access.log; 20 | error_log /var/log/nginx/error.log; 21 | 22 | server { 23 | # server_name www.example.com; 24 | listen 80; 25 | 26 | # gzip compression 27 | gzip on; 28 | gzip_buffers 16 8k; 29 | gzip_comp_level 6; 30 | gzip_http_version 1.1; 31 | gzip_proxied any; 32 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml; 33 | gzip_vary on; 34 | 35 | # Disable gzip for certain browsers. 36 | gzip_disable “MSIE [1-6].(?!.*SV1)”; 37 | 38 | root /usr/share/nginx/html; 39 | index index.html index.htm; 40 | 41 | # Set a 1 year expiration date for CSS and JS files 42 | location ~* \.(?:css|js)$ { 43 | try_files $uri =404; 44 | expires 1y; 45 | access_log off; 46 | add_header Cache-Control "public"; 47 | } 48 | 49 | # Any route containing a file extension (e.g. /devicesfile.js) 50 | location ~ ^.+\..+$ { 51 | try_files $uri =404; 52 | } 53 | 54 | # Any route that doesn't have a file extension (e.g. /devices) 55 | location / { 56 | try_files $uri $uri/ /index.html; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-react/client-vite", 3 | "version": "1.0.0", 4 | "description": "Client boilerplate built with Vite, using React, communicating with a Nest server", 5 | "main": "src/index.tsx", 6 | "author": "Paul Landázuri", 7 | "license": "LGPL-3.0-or-later", 8 | "scripts": { 9 | "start:dev": "vite", 10 | "generate:version": "cd ../.. && yarn generate:version", 11 | "build": "vite build", 12 | "serve": "vite preview", 13 | "lint": "eslint --fix --ext .ts,.tsx,.js,.jsx src", 14 | "check-types": "tsc" 15 | }, 16 | "dependencies": { 17 | "@nest-react/domain": "^1.0.0", 18 | "@nest-react/lib": "^1.0.0", 19 | "debug": "^4.3.4", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "semver": "^7.5.2" 23 | }, 24 | "peerDependencies": { 25 | "@nest-react/server": "1.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/debug": "^4.1.7", 29 | "@types/react": "^18.0.17", 30 | "@types/react-dom": "^18.0.6", 31 | "@types/semver": "^7.3.12", 32 | "@typescript-eslint/eslint-plugin": "^5.33.1", 33 | "@typescript-eslint/parser": "^5.33.1", 34 | "@vitejs/plugin-react": "^2.0.1", 35 | "eslint": "^8.22.0", 36 | "eslint-config-prettier": "^8.5.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "eslint-plugin-react": "^7.30.1", 40 | "eslint-plugin-react-hooks": "^4.6.0", 41 | "prettier": "^2.7.1", 42 | "typescript": "^4.7.4", 43 | "vite": "^4.5.6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LandazuriPaul/nest-react/098d988a0d8c72e0a5983a31111c05ef458ecd07/packages/client/public/favicon.ico -------------------------------------------------------------------------------- /packages/client/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LandazuriPaul/nest-react/098d988a0d8c72e0a5983a31111c05ef458ecd07/packages/client/public/logo192.png -------------------------------------------------------------------------------- /packages/client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LandazuriPaul/nest-react/098d988a0d8c72e0a5983a31111c05ef458ecd07/packages/client/public/logo512.png -------------------------------------------------------------------------------- /packages/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Nest + React", 3 | "name": "Nest + React boilerplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/client/src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | 3 | import { Dictionary } from '@nest-react/domain'; 4 | 5 | import { API_URL } from '~/config'; 6 | import { Logger, checkServerVersion } from '~/utils'; 7 | 8 | export const App: FC = () => { 9 | const [response, setResponse] = useState('NO SERVER RESPONSE'); 10 | 11 | useEffect(() => { 12 | async function fetchResponse(): Promise { 13 | try { 14 | const res = await fetch(API_URL); 15 | const data = await res.text(); 16 | setResponse(data); 17 | } catch (err) { 18 | Logger.error(err); 19 | } 20 | } 21 | 22 | fetchResponse(); 23 | }, []); 24 | 25 | useEffect(() => { 26 | checkServerVersion(); 27 | }, []); 28 | 29 | const dictExample: Dictionary = { 30 | first: 1, 31 | second: 2, 32 | }; 33 | return ( 34 | <> 35 |
36 | Here we use a Dictionary<number> interface from the{' '} 37 | @nest-react/domain package: 38 |
{JSON.stringify(dictExample)}
39 |
40 |
41 | And here we get a response from the API: 42 |
43 |
44 | {response} 45 |
46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/client/src/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from './App'; 2 | -------------------------------------------------------------------------------- /packages/client/src/config.ts: -------------------------------------------------------------------------------- 1 | import { getApiUrl } from '~/utils'; 2 | 3 | export const API_URL = getApiUrl(); 4 | 5 | export const APP_ROOT = 'root'; 6 | -------------------------------------------------------------------------------- /packages/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { APP_ROOT } from '~/config'; 5 | import { App } from '~/components/App'; 6 | 7 | import './index.css'; 8 | 9 | const container = document.getElementById(APP_ROOT); 10 | const root = createRoot(container!); 11 | 12 | function ReactApp(): JSX.Element { 13 | return ( 14 | }> 15 | 16 | 17 | ); 18 | } 19 | 20 | root.render(); 21 | -------------------------------------------------------------------------------- /packages/client/src/utils/checkServerVersion.ts: -------------------------------------------------------------------------------- 1 | import satisfies from 'semver/functions/satisfies'; 2 | 3 | import { API_URL } from '~/config'; 4 | 5 | import { Logger } from './logger'; 6 | 7 | function isServerVersionSatisfying(serverVersion: string): boolean { 8 | return satisfies(serverVersion, __REQUIRED_SERVER_VERSION__); 9 | } 10 | 11 | export async function checkServerVersion(): Promise { 12 | try { 13 | const res = await fetch(`${API_URL}/version`); 14 | const { SERVER_VERSION: serverVersion } = await res.json(); 15 | if (!isServerVersionSatisfying(serverVersion)) { 16 | Logger.error( 17 | `Server version ${serverVersion} does NOT satisfy client's peer dependency: ${__REQUIRED_SERVER_VERSION__}` 18 | ); 19 | } 20 | } catch (err) { 21 | Logger.error(err); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/src/utils/getApiUrl.ts: -------------------------------------------------------------------------------- 1 | export function getApiUrl(): string { 2 | const { hostname } = window.location; 3 | if (hostname === 'localhost') { 4 | return 'http://localhost:4000'; 5 | } 6 | return `https://api.${hostname}`; 7 | } 8 | -------------------------------------------------------------------------------- /packages/client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getApiUrl'; 2 | export * from './checkServerVersion'; 3 | export * from './logger'; 4 | -------------------------------------------------------------------------------- /packages/client/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | const LOGGER_PREFIX = 'nest-react'; 4 | 5 | const debugInfo = debug(`${LOGGER_PREFIX}:info`); 6 | debugInfo.enabled = true; 7 | debugInfo.color = '#01c205'; 8 | 9 | const debugWarn = debug(`${LOGGER_PREFIX}:warn`); 10 | debugWarn.enabled = true; 11 | debugWarn.color = '#ccc310'; 12 | 13 | const debugError = debug(`${LOGGER_PREFIX}:error`); 14 | debugError.enabled = true; 15 | debugError.color = '#b01405'; 16 | 17 | export class Logger { 18 | static info(...args: unknown[]): void { 19 | return debugInfo(args); 20 | } 21 | 22 | static log(...args: unknown[]): void { 23 | return debugInfo(args); 24 | } 25 | 26 | static warn(...args: unknown[]): void { 27 | return debugWarn(args); 28 | } 29 | 30 | static error(...args: unknown[]): void { 31 | if (args && args.length > 0 && args[0] instanceof Error) { 32 | const [e, ...rest] = args; 33 | let message = e.toString(); 34 | if (e.stack) { 35 | message = `${message}\n__Stack trace__\n\n${e.stack}`; 36 | } 37 | return debugError(message, rest.length > 0 ? rest : undefined); 38 | } 39 | return debugError(args); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": ".", 7 | "esModuleInterop": false, 8 | "isolatedModules": true, 9 | "jsx": "react", 10 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 11 | "module": "ESNext", 12 | "noEmit": true, 13 | "paths": { 14 | "~/*": ["src/*"] 15 | }, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": false, 18 | "target": "ESNext", 19 | "types": ["vite/client"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { readFileSync } from 'fs'; 3 | import { ResolvedConfig, UserConfigExport, defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | 6 | import { peerDependencies } from './package.json'; 7 | 8 | const DEV_SERVER_PORT = 8000; 9 | 10 | // Commit information to identify the build 11 | function getBuildId(): string { 12 | try { 13 | const versionPath = join(__dirname, '..', '..', 'VERSION'); 14 | const versionData = readFileSync(versionPath) 15 | .toString() 16 | .split(/[\r\n]+/); 17 | const [, branch] = versionData 18 | .find(line => line.includes('GIT_BRANCH'))! 19 | .split('='); 20 | const [, shortHash] = versionData 21 | .find(line => line.includes('GIT_SHORT_HASH'))! 22 | .split('='); 23 | return `${shortHash}@${branch}`; 24 | } catch (err) { 25 | console.log(err); 26 | return 'no-git'; 27 | } 28 | } 29 | 30 | // https://vitejs.dev/config/ 31 | export default ({ 32 | command, 33 | }: Pick): UserConfigExport => { 34 | const baseConfig = defineConfig({ 35 | define: { 36 | // eslint-disable-next-line @typescript-eslint/naming-convention 37 | __REQUIRED_SERVER_VERSION__: `'${peerDependencies['@nest-react/server']}'`, 38 | }, 39 | resolve: { 40 | alias: { 41 | '~': join(__dirname, 'src'), 42 | }, 43 | }, 44 | plugins: [react()], 45 | }); 46 | 47 | if (command === 'serve') { 48 | return { 49 | ...baseConfig, 50 | server: { 51 | port: DEV_SERVER_PORT, 52 | }, 53 | }; 54 | } 55 | 56 | const assetsDir = 'assets'; 57 | 58 | return { 59 | ...baseConfig, 60 | build: { 61 | sourcemap: true, 62 | rollupOptions: { 63 | output: { 64 | entryFileNames: join(assetsDir, `[name].${getBuildId()}.[hash].js`), 65 | }, 66 | }, 67 | }, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /packages/domain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-react/domain", 3 | "version": "1.0.0", 4 | "description": "Domain of the nest-react boilerplate", 5 | "author": "Paul Landázuri", 6 | "license": "LGPL-3.0-or-later", 7 | "main": "dist/cjs/index.js", 8 | "module": "dist/mjs/index.js", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/mjs/index.js", 12 | "require": "./dist/cjs/index.js" 13 | } 14 | }, 15 | "scripts": { 16 | "prebuild": "rimraf dist", 17 | "build": "tsc -p tsconfig-mjs.json && tsc -p tsconfig-cjs.json", 18 | "postbuild": "../../scripts/fix-common-package-exports.sh", 19 | "start:dev": "tsc -p tsconfig-mjs.json --watch & tsc -p tsconfig-cjs.json --watch", 20 | "lint": "eslint \"{src,test}/**/*.ts\" --fix" 21 | }, 22 | "devDependencies": { 23 | "@typescript-eslint/eslint-plugin": "^5.33.1", 24 | "@typescript-eslint/parser": "^5.33.1", 25 | "eslint": "^8.22.0", 26 | "eslint-config-prettier": "^8.5.0", 27 | "eslint-plugin-import": "^2.26.0", 28 | "eslint-plugin-prettier": "^4.2.1", 29 | "prettier": "^2.7.1", 30 | "typescript": "^4.7.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/domain/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | -------------------------------------------------------------------------------- /packages/domain/src/interfaces/dictionary.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Dictionary { 2 | [key: string]: T; 3 | } 4 | -------------------------------------------------------------------------------- /packages/domain/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dictionary.interface'; 2 | -------------------------------------------------------------------------------- /packages/domain/tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs", 6 | "target": "es2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/domain/tsconfig-mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "dist/mjs", 6 | "target": "esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/domain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "src", 6 | "inlineSourceMap": false, 7 | "lib": ["esnext"], 8 | "listEmittedFiles": false, 9 | "listFiles": false, 10 | "noFallthroughCasesInSwitch": true, 11 | "outDir": "dist", 12 | "traceResolution": false, 13 | }, 14 | "compileOnSave": false, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-react/lib", 3 | "version": "1.0.0", 4 | "description": "Library of the nest-react boilerplate", 5 | "author": "Paul Landázuri", 6 | "license": "LGPL-3.0-or-later", 7 | "main": "dist/index.js", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "tsc", 11 | "start:dev": "tsc --watch", 12 | "lint": "eslint \"{src,test}/**/*.ts\" --fix" 13 | }, 14 | "devDependencies": { 15 | "@typescript-eslint/eslint-plugin": "^5.33.1", 16 | "@typescript-eslint/parser": "^5.33.1", 17 | "eslint": "^8.22.0", 18 | "eslint-config-prettier": "^8.5.0", 19 | "eslint-plugin-import": "^2.26.0", 20 | "eslint-plugin-prettier": "^4.2.1", 21 | "prettier": "^2.7.1", 22 | "typescript": "^4.7.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/lib/src/index.ts: -------------------------------------------------------------------------------- 1 | console.log('empty'); 2 | -------------------------------------------------------------------------------- /packages/lib/tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs", 6 | "target": "es2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/lib/tsconfig-mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "dist/mjs", 6 | "target": "esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "src", 6 | "inlineSourceMap": false, 7 | "lib": ["esnext", "DOM"], 8 | "listEmittedFiles": false, 9 | "listFiles": false, 10 | "noFallthroughCasesInSwitch": true, 11 | "outDir": "dist", 12 | "traceResolution": false 13 | }, 14 | "compileOnSave": false, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder 2 | FROM node:16-alpine as builder 3 | 4 | # Copy server and domain + lib packages 5 | WORKDIR /usr/src/nest-react/ 6 | COPY .eslintrc . 7 | COPY .eslintignore . 8 | COPY package.json . 9 | COPY tsconfig.json . 10 | COPY yarn.lock . 11 | COPY scripts/fix-common-package-exports.sh scripts/fix-common-package-exports.sh 12 | 13 | COPY packages/server packages/server 14 | COPY packages/domain packages/domain 15 | COPY packages/lib packages/lib 16 | 17 | # Install domain + lib + server dependencies 18 | RUN yarn install --pure-lockfile --non-interactive 19 | 20 | # Build common packages 21 | RUN yarn build:common 22 | 23 | # Build server then 24 | WORKDIR /usr/src/nest-react/packages/server 25 | RUN yarn build 26 | 27 | # Runner 28 | FROM node:16-alpine AS runner 29 | 30 | WORKDIR /usr/src/nest-react 31 | COPY VERSION . 32 | 33 | # Copy the dist builds from builder 34 | COPY --from=builder /usr/src/nest-react/package.json . 35 | COPY --from=builder /usr/src/nest-react/yarn.lock . 36 | COPY --from=builder /usr/src/nest-react/tsconfig.json . 37 | 38 | COPY --from=builder /usr/src/nest-react/packages/domain/package.json packages/domain/package.json 39 | COPY --from=builder /usr/src/nest-react/packages/domain/dist packages/domain/dist 40 | 41 | COPY --from=builder /usr/src/nest-react/packages/lib/package.json packages/lib/package.json 42 | COPY --from=builder /usr/src/nest-react/packages/lib/dist packages/lib/dist 43 | 44 | COPY --from=builder /usr/src/nest-react/packages/server/package.json packages/server/package.json 45 | COPY --from=builder /usr/src/nest-react/packages/server/dist packages/server/dist 46 | 47 | # Install production dependencies 48 | RUN yarn install --pure-lockfile --non-interactive --production 49 | 50 | # Move to the server app 51 | WORKDIR /usr/src/nest-react/packages/server 52 | 53 | # Set the correct ownership for the app folder 54 | RUN chown -R node:node /usr/src/nest-react/packages/server/ 55 | 56 | # Launch the server with container 57 | ARG NODE_ENV=production 58 | CMD ["yarn", "start:prod"] 59 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # Nest React Server 2 | 3 | ## Configuration module 4 | 5 | Following the [NestJS recommendation](https://docs.nestjs.com/techniques/configuration) for the server configuration, the server package comes with a convenient `ConfigModule` declared as a `@Global()` module so any other module can access it without needing to include it in its dependencies. 6 | 7 | ### Initialisation 8 | 9 | This module adapts to both local `dotenv` files and container orchestration config volumes — e.g. [Kubernetes `config-volume`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/). It follows the below logic to define the configuration variables, in order of priority: 10 | 11 | 1. If both `CONFIG_PATH` and `SECRETS_PATH` environment variables are defined, it will look for their corresponding values as folders and hydrate the configuration with their file content. 12 | 13 | 2. If the `NODE_ENV` environment variable is set, it will read both the usual configurations variables and the secrets from the corresponding `env/${NODE_ENV}.env` file. All the secrets need to be prefixed with the `SECRET_` string as per the `configSchema`. 14 | 15 | 3. If none of the above is provided, it will try to read the configuration and secret variables from the `env/local.env` file. 16 | 17 | 4. If none of the above is satisfied, the application will throw an uncaught error and crash. 18 | 19 | ### Validation 20 | 21 | In order to be make sure that the application won't crash because of a misconfiguration once started, the configuration is entirely validated against the schema provided in the `ConfigService`'s `configSchema` private property thanks to the [@hapi/joi](https://hapi.dev/module/joi/) package. 22 | 23 | ### Usage in the app 24 | 25 | As the `ConfigModule` is declared as `@Global()`, you don't need to include in the module's `imports` array to use it. Nevertheless, you'll need to include the `ConfigService` in the constructor arguments of any class using it. You can see an example in the [HelloService](./src/modules/hello/hello.service.ts). 26 | 27 | In order to enforce type safety when using the configuration variables across the application source, you should always access them via getters. You can find examples of such typed getters at the end of the [`ConfigService`](./src/config/config.service.ts). 28 | 29 | The only exception for this is in the [main's `bootstrap`](./src/main.ts) function which needs to get the configuration variables without having access to the `ConfigService` instance and which gets over the TS `private` restriction by calling the `envConfig` as a plain object. 30 | 31 | ## Running the app 32 | 33 | By default, the server is listening to the [http://localhost:4000](http://localhost:4000) port. This behaviour can be configured via the [local.env](./env/local.env) file. 34 | 35 | ### In development 36 | 37 | To run the development server locally, you can use the bellow commands: 38 | 39 | ```sh 40 | # Runs the current version of the server 41 | yarn start 42 | 43 | # Runs the current version of the server 44 | # + watches for file changes 45 | yarn start:dev 46 | 47 | # Runs the current version of the server 48 | # + watches for file changes 49 | # + opens a debugger connection (default port 9229) 50 | yarn start:debug 51 | ``` 52 | 53 | #### Debug 54 | 55 | The powerful debug feature allows any Node.js debugger tool to connect to the running process in order to define breakpoints, log points, and more. Any Chromium based browser comes with the Node inspector feature built-in. To learn more about the Node.js debugging tools, the [Node.js documentation](https://nodejs.org/de/docs/guides/debugging-getting-started/) is a nice starting point. 56 | 57 | If you use the VS Code IDE, this repository already ships a configuration to `attach` a debugger to the running application. See the [.vscode/launch.json](../../.vscode/launch.json) file for more information. 58 | 59 | ### In production 60 | 61 | Once you are happy with your code, you can run a production version following these steps: 62 | 63 | 1. Build a production bundle into a brand new `dist` folder: 64 | 65 | ```sh 66 | yarn build 67 | ``` 68 | 69 | 2. Run the production bundle: 70 | 71 | ```sh 72 | yarn start:prod 73 | ``` 74 | 75 | ## Test 76 | 77 | Nest comes with `Jest` and `Supertest` testing frameworks to ease the testing process. Here are the different test scripts which you can run: 78 | 79 | ```sh 80 | # Runs the unit tests 81 | yarn test 82 | 83 | # Runs the unit test 84 | # + watches for file changes 85 | yarn test:watch 86 | 87 | # Runs the end-to-end tests 88 | yarn test:e2e 89 | 90 | # Describes the test coverage 91 | yarn test:cov 92 | ``` 93 | 94 | ## Docker image 95 | 96 | The server package has a [Dockerfile](./Dockerfile) which builds a lightweight (based on the [alpine](https://alpinelinux.org/) project) production ready Docker image. 97 | 98 | For more information about the Docker images, see the [main README.md](../../README.md#docker-images). 99 | -------------------------------------------------------------------------------- /packages/server/env/example.env: -------------------------------------------------------------------------------- 1 | # Configuration 2 | CORS_WHITELIST=http://localhost:8000 3 | HOST=localhost 4 | PORT=4000 5 | 6 | # Secrets 7 | SECRET_JWT_KEY=AVeryPrivateJWTKey 8 | -------------------------------------------------------------------------------- /packages/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-react/server", 3 | "version": "1.0.0", 4 | "description": "Backend of the Nest + React boilerplate", 5 | "author": "Paul Landázuri", 6 | "license": "LGPL-3.0-or-later", 7 | "main": "src/main.ts", 8 | "scripts": { 9 | "generate:version": "cd ../.. && yarn generate:version", 10 | "prebuild": "rimraf dist", 11 | "build": "nest build", 12 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 13 | "prestart": "yarn generate:version", 14 | "start": "nest start", 15 | "prestart:dev": "yarn generate:version", 16 | "start:dev": "nest start --watch", 17 | "prestart:debug": "yarn generate:version", 18 | "start:debug": "nest start --debug --watch", 19 | "start:prod": "node dist/main", 20 | "lint": "eslint \"{src,test}/**/*.ts\" --fix", 21 | "test": "jest", 22 | "test:watch": "jest --watch", 23 | "test:cov": "jest --coverage", 24 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 25 | "test:e2e": "jest --config ./test/jest-e2e.json" 26 | }, 27 | "dependencies": { 28 | "@nest-react/domain": "^1.0.0", 29 | "@nest-react/lib": "^1.0.0", 30 | "@nestjs/common": "^9.0.11", 31 | "@nestjs/core": "^9.0.11", 32 | "@nestjs/platform-express": "^9.0.11", 33 | "dotenv": "^16.0.1", 34 | "joi": "^17.6.0", 35 | "reflect-metadata": "^0.1.13", 36 | "rimraf": "^3.0.2", 37 | "rxjs": "^7.5.6" 38 | }, 39 | "devDependencies": { 40 | "@nestjs/cli": "^9.0.0", 41 | "@nestjs/schematics": "^9.0.1", 42 | "@nestjs/testing": "^9.0.11", 43 | "@types/express": "^4.17.13", 44 | "@types/jest": "^28.1.7", 45 | "@types/node": "^16.11.0", 46 | "@types/supertest": "^2.0.12", 47 | "@typescript-eslint/eslint-plugin": "^5.33.1", 48 | "@typescript-eslint/parser": "^5.33.1", 49 | "eslint": "^8.22.0", 50 | "eslint-config-prettier": "^8.5.0", 51 | "eslint-plugin-import": "^2.26.0", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "jest": "^28.1.3", 54 | "prettier": "^2.7.1", 55 | "supertest": "^6.2.4", 56 | "ts-jest": "^28.0.8", 57 | "ts-loader": "^9.3.1", 58 | "ts-node": "^10.9.1", 59 | "tsconfig-paths": "^4.1.0", 60 | "typescript": "^4.7.4" 61 | }, 62 | "jest": { 63 | "moduleFileExtensions": [ 64 | "js", 65 | "json", 66 | "ts" 67 | ], 68 | "rootDir": "src", 69 | "testRegex": ".test.ts$", 70 | "transform": { 71 | "^.+\\.(t|j)s$": "ts-jest" 72 | }, 73 | "coverageDirectory": "../coverage", 74 | "testEnvironment": "node", 75 | "moduleNameMapper": { 76 | "^~/(.*)$": "/$1" 77 | }, 78 | "setupFiles": [ 79 | "/jest-setup.ts" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/server/src/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | process.env.NODE_ENV = 'local'; 4 | -------------------------------------------------------------------------------- /packages/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | 3 | import { APIModule } from './modules/api.module'; 4 | import { ConfigService } from './modules/config/config.service'; 5 | 6 | async function bootstrap(): Promise { 7 | const app = await NestFactory.create(APIModule); 8 | const configService = app.get(ConfigService); 9 | app.enableCors({ origin: configService.corsWhiteList }); 10 | await app.listen(configService.port); 11 | } 12 | 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /packages/server/src/modules/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ConfigModule } from './config/config.module'; 4 | import { StatusModule } from './status/status.module'; 5 | 6 | @Module({ 7 | imports: [ConfigModule, StatusModule], 8 | }) 9 | export class APIModule {} 10 | -------------------------------------------------------------------------------- /packages/server/src/modules/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { ConfigService } from './config.service'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [ConfigService], 8 | exports: [ConfigService], 9 | }) 10 | export class ConfigModule {} 11 | -------------------------------------------------------------------------------- /packages/server/src/modules/config/config.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { join } from 'path'; 3 | import { readFileSync, readdirSync } from 'fs'; 4 | 5 | import { DotenvParseOutput, parse } from 'dotenv'; 6 | import joi from 'joi'; 7 | import { InternalServerErrorException, Logger } from '@nestjs/common'; 8 | 9 | const { number, object, string } = joi.types(); 10 | 11 | export class ConfigService { 12 | public rootDir: string; 13 | public runningDir: string; 14 | 15 | private readonly configSchema = object.keys({ 16 | CORS_WHITELIST: string.required(), 17 | HOST: string.required(), 18 | PORT: number.default(4000), 19 | SECRET_JWT_KEY: string.required(), 20 | }); 21 | private envConfig: DotenvParseOutput; 22 | private logger = new Logger(ConfigService.name); 23 | 24 | constructor() { 25 | this.rootDir = `${join(process.cwd())}`; 26 | this.runningDir = `${join(this.rootDir, process.env.baseUrl || '')}`; 27 | 28 | // extract and validate config from ${NODE_ENV}.env file or CONFIG_PATH & SECRETS_PATH 29 | let config: DotenvParseOutput; 30 | const { CONFIG_PATH: configPath, SECRETS_PATH: secretsPath } = process.env; 31 | if (configPath && secretsPath) { 32 | config = this.getConfigFromVolumes(configPath, secretsPath); 33 | } else { 34 | config = this.getConfigFromEnvFile(process.env.NODE_ENV); 35 | } 36 | this.envConfig = this.validateInput(config); 37 | } 38 | 39 | /** 40 | * Extract the configuration from a `dotenv` file 41 | * @param env The environment name. Corresponding `name.env` file will be used. Default to `local` 42 | */ 43 | private getConfigFromEnvFile(env = 'local'): DotenvParseOutput { 44 | const envFilePath = join('env', `${env}.env`); 45 | try { 46 | const config = parse(readFileSync(envFilePath)); 47 | return config; 48 | } catch (err) { 49 | const msg = `Configuration error, see below: 50 | 51 | /!\\ No environment definition found at ${envFilePath}. Please choose one of the following options (in preference order): 52 | 1. Set both the CONFIG_PATH and the SECRETS_PATH environment variables and fill their respective folders with corresponding environment values. 53 | 2. Set the NODE_ENV environment variable and attach the corresponding "dotenv" file to the server. 54 | 55 | `; 56 | this.logger.error(msg); 57 | throw new InternalServerErrorException(); 58 | } 59 | } 60 | 61 | /** 62 | * Extract the configuration from both the `config` and `secrets` folders. 63 | * @param configPath Path to the open `configurations` directory 64 | * @param secretsPath Path to the `secrets` directory 65 | */ 66 | private getConfigFromVolumes( 67 | configPath: string, 68 | secretsPath: string 69 | ): DotenvParseOutput { 70 | const configFiles = readdirSync(configPath).filter( 71 | file => !file.includes('..') 72 | ); 73 | const secretsFiles = readdirSync(secretsPath).filter( 74 | file => !file.includes('..') 75 | ); 76 | const config: DotenvParseOutput = {}; 77 | configFiles.reduce((partialConfig, file) => { 78 | partialConfig[file] = this.extractConfigFromFile(join(configPath, file)); 79 | return partialConfig; 80 | }, config); 81 | secretsFiles.reduce((partialConfig, file) => { 82 | partialConfig[file] = this.extractConfigFromFile(join(secretsPath, file)); 83 | return partialConfig; 84 | }, config); 85 | return config; 86 | } 87 | 88 | /** 89 | * Extract the configuration string from a file 90 | * @param filePath File path to read the config from 91 | */ 92 | private extractConfigFromFile(filePath: string): string { 93 | const fileContent = readFileSync(filePath).toString().trim(); 94 | return fileContent; 95 | } 96 | 97 | /** 98 | * Validate the configuration object against the required configuration schema 99 | * using the Joi library. 100 | * @param envConfig The config object 101 | */ 102 | private validateInput(envConfig: DotenvParseOutput): DotenvParseOutput { 103 | const { error, value: validatedEnvConfig } = 104 | this.configSchema.validate(envConfig); 105 | if (error) { 106 | throw new Error(`Config validation error: ${error.message}`); 107 | } 108 | this.printConfig(validatedEnvConfig); 109 | return validatedEnvConfig; 110 | } 111 | 112 | /** 113 | * Safely prints the server configuration. All secret values will be hidden. 114 | * @param envConfig 115 | */ 116 | private printConfig(envConfig: DotenvParseOutput): void { 117 | const config = Object.keys(envConfig) 118 | .filter(key => !key.includes('SECRET')) 119 | .reduce((obj, key) => { 120 | obj[key] = envConfig[key]; 121 | return obj; 122 | }, {} as DotenvParseOutput); 123 | const secrets = Object.keys(envConfig).filter(key => 124 | key.includes('SECRET') 125 | ); 126 | this.logger.log( 127 | `Server configuration:\n${JSON.stringify(config, null, 2)}` 128 | ); 129 | this.logger.log(`Server secrets:\n${JSON.stringify(secrets, null, 2)}`); 130 | } 131 | 132 | /** 133 | * Config getters 134 | */ 135 | 136 | get corsWhiteList(): string[] { 137 | return this.envConfig.CORS_WHITELIST.split(','); 138 | } 139 | 140 | get host(): string { 141 | return String(this.envConfig.HOST); 142 | } 143 | 144 | get port(): number { 145 | return parseInt(this.envConfig.PORT, 10); 146 | } 147 | 148 | /** 149 | * Secret getters 150 | */ 151 | 152 | get secretJwtKey(): string { 153 | return String(this.envConfig.SECRET_JWT_KEY); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /packages/server/src/modules/status/status.controller.test.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { ConfigService } from '~/modules/config/config.service'; 4 | import { StatusController } from './status.controller'; 5 | import { StatusService } from './status.service'; 6 | 7 | describe('AppController', () => { 8 | let statusController: StatusController; 9 | 10 | beforeEach(async () => { 11 | const app: TestingModule = await Test.createTestingModule({ 12 | controllers: [StatusController], 13 | providers: [ConfigService, StatusService], 14 | }).compile(); 15 | 16 | statusController = app.get(StatusController); 17 | }); 18 | 19 | describe('root', () => { 20 | it('should return a nice status world', () => { 21 | expect(statusController.getStatus()).toBe( 22 | 'Hello world from Nest running on localhost:4000!' 23 | ); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/server/src/modules/status/status.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | import { Dictionary } from '@nest-react/domain'; 4 | 5 | import { StatusService } from './status.service'; 6 | 7 | @Controller() 8 | export class StatusController { 9 | constructor(private readonly statusService: StatusService) {} 10 | 11 | @Get() 12 | getStatus(): string { 13 | return this.statusService.getStatus(); 14 | } 15 | 16 | @Get('version') 17 | getVersion(): Dictionary { 18 | return this.statusService.getVersion(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/src/modules/status/status.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { StatusController } from './status.controller'; 4 | import { StatusService } from './status.service'; 5 | 6 | @Module({ 7 | controllers: [StatusController], 8 | providers: [StatusService], 9 | }) 10 | export class StatusModule {} 11 | -------------------------------------------------------------------------------- /packages/server/src/modules/status/status.service.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { readFileSync } from 'fs'; 3 | import { Injectable, Logger } from '@nestjs/common'; 4 | 5 | import { Dictionary } from '@nest-react/domain'; 6 | 7 | import { ConfigService } from '~/modules/config/config.service'; 8 | 9 | @Injectable() 10 | export class StatusService { 11 | private logger = new Logger(StatusService.name); 12 | private version: Dictionary; 13 | 14 | constructor(private readonly configService: ConfigService) { 15 | this.version = readFileSync( 16 | join(configService.rootDir, '..', '..', 'VERSION') 17 | ) 18 | .toString() 19 | .split(/[\r\n]+/) 20 | .reduce((agg, line) => { 21 | const [key, value] = line.split('='); 22 | 23 | // The client is served from another Docker image 24 | // thus, this one isn't necessarily correct 25 | if (key !== 'CLIENT_VERSION') { 26 | agg[key] = value; 27 | } 28 | return agg; 29 | }, {} as Dictionary); 30 | } 31 | 32 | getStatus(): string { 33 | this.logger.log('log from statusService.getStatus()'); 34 | return `Hello world from Nest running on ${this.configService.host}:${this.configService.port}!`; 35 | } 36 | 37 | getVersion(): Dictionary { 38 | return this.version; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/server/test/app.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import request from 'supertest'; 4 | 5 | import { ConfigModule } from '~/modules/config/config.module'; 6 | import { StatusModule } from '~/modules/status/status.module'; 7 | 8 | describe('AppController (e2e)', () => { 9 | let app: INestApplication; 10 | 11 | beforeEach(async () => { 12 | const moduleFixture: TestingModule = await Test.createTestingModule({ 13 | imports: [ConfigModule, StatusModule], 14 | }).compile(); 15 | 16 | app = moduleFixture.createNestApplication(); 17 | await app.init(); 18 | }); 19 | 20 | it('/ (GET)', async () => { 21 | return request(app.getHttpServer()) 22 | .get('/') 23 | .expect(200) 24 | .expect('Hello world from Nest running on localhost:4000!'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-test.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "moduleNameMapper": { 10 | "^~/(.*)$": "/../src/$1" 11 | }, 12 | "setupFiles": ["/../src/jest-setup.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "emitDecoratorMetadata": true, 6 | "module": "CommonJS", 7 | "outDir": "dist", 8 | "paths": { 9 | "~/*": ["src/*"] 10 | }, 11 | "strictPropertyInitialization": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /scripts/build_and_push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | # Exit on fail or ctrl-c 6 | set -e 7 | trap "exit" INT 8 | 9 | if [[ -z "${PACKAGE}" ]]; then 10 | echo "You must specify a PACKAGE name (server or client) as an environment, e.g.: PACKAGE=server" 11 | exit 1 12 | fi 13 | 14 | # Generate VERSION file 15 | source $DIR/generate_version.sh 16 | 17 | LOCAL_VERSION="${VERSION:-latest}" 18 | 19 | DOCKER_IMAGE="docker.pkg.github.com/landazuripaul/nest-react/nest-react-$PACKAGE:$LOCAL_VERSION" 20 | 21 | printf "> Building the Docker image: $DOCKER_IMAGE ...\n" 22 | 23 | DOCKER_BUILDKIT=1 docker build -f ./packages/$PACKAGE/Dockerfile -t $DOCKER_IMAGE . 24 | 25 | printf "\n\n> Sending built image to the registry...\n" 26 | 27 | docker push $DOCKER_IMAGE 28 | -------------------------------------------------------------------------------- /scripts/fix-common-package-exports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat >dist/cjs/package.json <dist/mjs/package.json < /dev/null && pwd )" 6 | 7 | # Git info 8 | GIT_SHORT_HASH=$(git rev-parse --short HEAD) 9 | GIT_BRANCH=$(git branch --show-current) 10 | 11 | # Packages info 12 | REPO_VERSION=$(node $DIR/getPackageVersion.js) 13 | CLIENT_VERSION=$(node $DIR/getPackageVersion.js client) 14 | DOMAIN_VERSION=$(node $DIR/getPackageVersion.js domain) 15 | LIB_VERSION=$(node $DIR/getPackageVersion.js lib) 16 | SERVER_VERSION=$(node $DIR/getPackageVersion.js server) 17 | 18 | cat > $VERSION_FILENAME <<- EOF 19 | GIT_SHORT_HASH=$GIT_SHORT_HASH 20 | GIT_BRANCH=$GIT_BRANCH 21 | REPO_VERSION=$REPO_VERSION 22 | CLIENT_VERSION=$CLIENT_VERSION 23 | DOMAIN_VERSION=$DOMAIN_VERSION 24 | LIB_VERSION=$LIB_VERSION 25 | SERVER_VERSION=$SERVER_VERSION 26 | EOF 27 | 28 | echo "> $VERSION_FILENAME file generated" 29 | -------------------------------------------------------------------------------- /scripts/getPackageVersion.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const { readFileSync } = require('fs'); 3 | const { join } = require('path'); 4 | 5 | let packagePath; 6 | 7 | switch (process.argv.slice(2)[0]) { 8 | case 'client': 9 | packagePath = join(__dirname, '..', 'packages', 'client'); 10 | break; 11 | case 'domain': 12 | packagePath = join(__dirname, '..', 'packages', 'domain'); 13 | break; 14 | case 'lib': 15 | packagePath = join(__dirname, '..', 'packages', 'lib'); 16 | break; 17 | case 'server': 18 | packagePath = join(__dirname, '..', 'packages', 'server'); 19 | break; 20 | case 'repo': 21 | default: 22 | packagePath = join(__dirname, '..'); 23 | break; 24 | } 25 | 26 | const { version } = JSON.parse(readFileSync(join(packagePath, 'package.json'))); 27 | 28 | console.log(version); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "incremental": true, 10 | "keyofStringsOnly": true, 11 | "module": "ES2020", 12 | "moduleResolution": "node", 13 | "removeComments": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2019" 19 | }, 20 | "exclude": ["**/node_modules/*", "**/dist/*", "scripts/*"] 21 | } 22 | --------------------------------------------------------------------------------