├── .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 |
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 |
--------------------------------------------------------------------------------