├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── node
├── config
│ └── tsconfig.json
├── src
│ ├── controllers
│ │ └── users.controller.ts
│ ├── models
│ │ └── user.model.ts
│ ├── routes
│ │ └── api
│ │ │ └── users.routes.ts
│ └── server.ts
└── test
│ ├── dummy.test.js
│ └── dummyRoute.test.ts
├── package-lock.json
├── package.json
├── react
├── config
│ ├── env.js
│ ├── paths.js
│ ├── polyfills.js
│ ├── scripts
│ │ ├── build.js
│ │ └── start.js
│ ├── tsconfig.json
│ ├── typings
│ │ └── custom
│ │ │ └── react-redux.d.ts
│ ├── webpack.config.dev.js
│ ├── webpack.config.prod.js
│ └── webpackDevServer.config.js
├── public
│ ├── images
│ │ ├── favicon.ico
│ │ └── logo.svg
│ └── index.html
├── src
│ ├── App.tsx
│ ├── actions
│ │ └── users.ts
│ ├── constants
│ │ └── actions.ts
│ ├── containers
│ │ ├── Home
│ │ │ └── index.tsx
│ │ └── Login
│ │ │ └── index.tsx
│ ├── index.tsx
│ ├── models
│ │ └── User.ts
│ ├── reducers
│ │ ├── index.ts
│ │ └── users.ts
│ ├── store
│ │ └── index.ts
│ ├── styles
│ │ ├── App.scss
│ │ └── Base.scss
│ └── utils
│ │ └── registerServiceWorker.ts
└── test
│ └── dummy.test.js
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Build
4 | dist-node
5 | dist-react
6 |
7 | # API keys and secrets
8 | .env
9 |
10 | # Dependency directory
11 | node_modules
12 | bower_components
13 |
14 | # Editors
15 | .idea
16 | *.iml
17 |
18 | # OS metadata
19 | .DS_Store
20 | Thumbs.db
21 |
22 | # testing
23 | /coverage
24 |
25 | # Misc
26 | .DS_Store
27 | .env
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | npm-debug.log*
34 | yarn-debug.log*
35 | yarn-error.log*
36 |
37 | lib-cov
38 | *.seed
39 | *.log
40 | *.csv
41 | *.dat
42 | *.out
43 | *.pid
44 | *.gz
45 | *.swp
46 |
47 | pids
48 | logs
49 | results
50 | tmp
51 |
52 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "editor.insertSpaces": false,
4 | "editor.renderWhitespace": "boundary",
5 | "editor.fontSize": 13,
6 | "editor.tabSize": 4,
7 | "editor.minimap.enabled": true,
8 | "editor.minimap.renderCharacters": false,
9 | "vsicons.dontShowNewVersionMessage": true,
10 | "editor.detectIndentation": true,
11 | "files.exclude": {
12 | "node_modules/": true,
13 | "dist-node/": true,
14 | "dist-react/": true,
15 | ".vscode/": true,
16 | ".gitignore": true,
17 | "tslint.json": true,
18 | "README.md": true,
19 | "package-lock.json": true
20 | },
21 | "typescript.tsdk": "./node_modules/typescript/lib"
22 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Typescript Boilerplate
2 |
3 | **Why Full Stack?**
4 |
5 | - **Working with only one repository**
6 |
7 |
8 | - **Only one server needed to deploy the application** - even if you were to deploy it on the same server, having it all in one place reduces the effort in finding your way around the project
9 |
10 |
11 | - **Separate consoles in development** - more often than not, it's a good idea to run React application and Node application in separate consoles. Both work differently and it's ok to accept them as such. But in reality, we only need them separate in development.
12 |
13 |
14 | - **Multiple build locations** - It's way simpler to add more react projects on top of it, just duplicate the react folder and adjust redirects from the server
15 | - **Only one package.json** - This is especially important. One package.json means one node_modules. And that means no duplicate code, dependencies or packages killing themselves and causing you headaches
16 |
17 |
18 |
19 | **Why Typescript?**
20 |
21 | Two words: type safety.
22 |
23 |
24 |
25 | **Technology stack**
26 |
27 | - Node 8
28 | - Express 4
29 | - React 15.6
30 | - Typescript 2.3
31 | - Redux 3.7
32 | - Webpack 2.6
33 | - Jest
34 | - Scss
35 |
36 |
37 |
38 | ### Getting started
39 |
40 | In order to start the project, you will need Node 8 installed! Please visit nodejs.org and fetch the latest version.
41 |
42 |
43 |
44 | Install all required dependecies with
45 |
46 | ```
47 | yarn install
48 | ```
49 |
50 | or
51 |
52 | ```
53 | npm install
54 | ```
55 |
56 |
57 |
58 |
59 |
60 | Create a .env file in the project root where you will keep the environment variables. Using the environment variables is based on the dotenv package, whose details can be found here: https://github.com/motdotla/dotenv
61 |
62 | Example `.env`
63 |
64 | ```
65 | NODE_ENV=development
66 | ```
67 |
68 |
69 |
70 | To start a development node server, simply type
71 |
72 | ```javascript
73 | yarn nstart
74 | ```
75 |
76 | or
77 |
78 | ```javascript
79 | npm run nstart
80 | ```
81 |
82 |
83 |
84 | As for React in development mode (server), start by typing
85 |
86 | ```
87 | yarn rstart
88 | ```
89 |
90 | or
91 |
92 | ```
93 | npm run rstart
94 | ```
95 |
96 |
97 |
98 | If you would like to build node, react or both, use one of the following commands respectively
99 |
100 | ```
101 | yarn node-build
102 | yarn react-build
103 | yarn build
104 | ```
105 |
106 | or
107 |
108 | ```
109 | npm run node-build
110 | npm run react-build
111 | npm run build
112 | ```
113 |
114 |
115 |
116 | To keep everything simple, a simple `npm start` script will have everything prepared for production. No additional configuration needed.
117 |
118 |
119 |
120 | ### Where to from here?
121 |
122 | Well, the usual stops are setting up a process manager, adding a database (SQL/NoSQL) and everything else you might need on your project. Since these additions are quite simple to add and specific choise of one is quite dependant on the project, I decided to leave it out from the boilerplate.
123 |
124 |
125 |
126 | Should there be a sufficient amount of request for additional features within the boilerplate, I'll give it an update! In the meantime - **happy coding!**
127 |
128 |
--------------------------------------------------------------------------------
/node/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "moduleResolution": "node",
6 | "sourceMap": true,
7 | "outDir": "../../dist-node",
8 | "baseUrl": "../../node",
9 | "paths": {
10 | "*": [
11 | "../../node_modules/*"
12 | ]
13 | },
14 | "typeRoots" : ["./typings"],
15 | "noImplicitAny": false,
16 | "experimentalDecorators": true,
17 | "emitDecoratorMetadata": true
18 | },
19 | "include": [
20 | "../src/**/*",
21 | "typings/**/*"
22 | ]
23 | }
--------------------------------------------------------------------------------
/node/src/controllers/users.controller.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response} from 'express';
2 |
3 | export const getAllUsers = (req: Request, res: Response) => {
4 | res.send('allUsers API route');
5 | };
--------------------------------------------------------------------------------
/node/src/models/user.model.ts:
--------------------------------------------------------------------------------
1 | const User = {};
2 |
3 | export default User;
--------------------------------------------------------------------------------
/node/src/routes/api/users.routes.ts:
--------------------------------------------------------------------------------
1 | import {Express} from 'express';
2 | import * as UsersController from '../../controllers/users.controller';
3 |
4 | const UsersRoutes = (app: Express) => {
5 |
6 | app.get('/api/1.0/allUsers', UsersController.getAllUsers);
7 |
8 | };
9 |
10 | export default UsersRoutes;
11 |
--------------------------------------------------------------------------------
/node/src/server.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import * as compression from 'compression';
3 | import * as session from 'express-session';
4 | import * as bodyParser from 'body-parser';
5 | import * as logger from 'morgan';
6 | import * as errorHandler from 'errorhandler';
7 | import * as lusca from 'lusca';
8 | import * as dotenv from 'dotenv';
9 | import * as flash from 'express-flash';
10 | import * as path from 'path';
11 | import * as clear from 'clear-console';
12 | import * as chalk from 'chalk';
13 | import expressValidator = require('express-validator');
14 |
15 | if (process.env.NODE_ENV !== 'production') {
16 | dotenv.config({ path: '.env' });
17 | clear({toStart: true});
18 | clear({toStart: true});
19 | }
20 |
21 | const app = express();
22 |
23 | app.set('port', process.env.PORT || 3000);
24 |
25 | app.use(compression());
26 | app.use(logger('dev'));
27 | app.use(bodyParser.json());
28 | app.use(bodyParser.urlencoded({ extended: true }));
29 | app.use(expressValidator());
30 | app.use(session({
31 | resave: true,
32 | saveUninitialized: true,
33 | secret: process.env.SESSION_SECRET
34 | }));
35 | app.use(flash());
36 | app.use(lusca.xframe('SAMEORIGIN'));
37 | app.use(lusca.xssProtection(true));
38 |
39 | // API Routes imports
40 | import UsersAPIRoutes from './routes/api/users.routes';
41 | // Initialization
42 | UsersAPIRoutes(app);
43 |
44 | if (process.env.NODE_ENV === 'production') {
45 | app.use('/images', express.static(path.join(__dirname, '..', 'dist-react', 'images'), { maxAge: 31557600000 }));
46 | app.use('/libs', express.static(path.join(__dirname, '..', 'dist-react', 'libs'), { maxAge: 31557600000 }));
47 | app.use('/static', express.static(path.join(__dirname, '..', 'dist-react', 'static'), { maxAge: 31557600000 }));
48 | app.get('*', (req, res) => res.sendFile(path.join(__dirname, '..', 'dist-react', 'index.html')));
49 | } else {
50 | app.get('/:url', (req, res) => (res.redirect('http://localhost:3001/' + req.params.url)));
51 | }
52 |
53 | app.use(errorHandler());
54 |
55 | app.listen(app.get('port'), () => {
56 | console.info(chalk.green('Node server compiled succesfully!'));
57 | console.info('App is running at ' + chalk.bold('http://localhost:' + app.get('port')) + ' in ' + chalk.bold(app.get('env').toUpperCase()) + ' mode');
58 | });
59 |
60 | module.exports = app;
--------------------------------------------------------------------------------
/node/test/dummy.test.js:
--------------------------------------------------------------------------------
1 | test('adds 2 + 3 to equal 5', () => {
2 | const sum = (a, b) => (a + b);
3 | expect(2 + 3).toBe(5);
4 | });
--------------------------------------------------------------------------------
/node/test/dummyRoute.test.ts:
--------------------------------------------------------------------------------
1 | import * as supertest from 'supertest';
2 | const request = supertest('http://localhost:3000');
3 |
4 | describe('GET /', () => {
5 | it('should return 200 OK', (done) => {
6 | request.get('/').expect(200, done);
7 | });
8 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript-fullstack-boilerplate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "Node + React + Redux with Typescript",
6 | "author": "Matko Raguz, Axilis",
7 | "proxy": "http://localhost:3000",
8 | "dependencies": {
9 | "@types/body-parser": "^1.16.2",
10 | "@types/compression": "0.0.33",
11 | "@types/dotenv": "^2.0.20",
12 | "@types/errorhandler": "0.0.30",
13 | "@types/express": "^4.0.35",
14 | "@types/express-session": "0.0.32",
15 | "@types/morgan": "^1.7.32",
16 | "@types/node": "^8.0.6",
17 | "@types/react": "^15.0.32",
18 | "@types/react-dom": "^15.5.1",
19 | "@types/react-redux": "^4.4.45",
20 | "@types/react-router-dom": "^4.0.8",
21 | "@types/redux": "^3.6.0",
22 | "@types/redux-actions": "^1.2.6",
23 | "autoprefixer": "7.1.0",
24 | "body-parser": "^1.15.2",
25 | "case-sensitive-paths-webpack-plugin": "2.0.0",
26 | "chalk": "^1.1.3",
27 | "clear-console": "^1.1.0",
28 | "compression": "^1.6.2",
29 | "css-loader": "0.28.1",
30 | "errorhandler": "^1.4.3",
31 | "express": "^4.14.0",
32 | "express-flash": "^0.0.2",
33 | "express-session": "^1.14.2",
34 | "express-validator": "^3.1.3",
35 | "lusca": "^1.4.1",
36 | "morgan": "^1.7.0",
37 | "node-sass": "^4.5.3",
38 | "react": "^15.6.1",
39 | "react-dom": "^15.6.1",
40 | "react-redux": "^5.0.5",
41 | "react-router-dom": "^4.1.1",
42 | "redux": "^3.7.1",
43 | "redux-actions": "^2.0.3",
44 | "sass-loader": "^6.0.6",
45 | "typescript": "^2.3.3"
46 | },
47 | "devDependencies": {
48 | "@types/jest": "^20.0.0",
49 | "@types/supertest": "^2.0.0",
50 | "concurrently": "^3.4.0",
51 | "dotenv": "4.0.0",
52 | "extract-text-webpack-plugin": "2.1.0",
53 | "file-loader": "0.11.1",
54 | "fs-extra": "3.0.1",
55 | "html-webpack-plugin": "2.28.0",
56 | "jest": "20.0.3",
57 | "nodemon": "1.11.0",
58 | "object-assign": "4.1.1",
59 | "postcss-flexbugs-fixes": "3.0.0",
60 | "postcss-loader": "2.0.5",
61 | "promise": "7.1.1",
62 | "react-dev-utils": "^2.0.1",
63 | "react-error-overlay": "^1.0.6",
64 | "rimraf": "^2.6.2",
65 | "source-map-loader": "^0.2.1",
66 | "style-loader": "0.17.0",
67 | "supertest": "^2.0.1",
68 | "sw-precache-webpack-plugin": "0.9.1",
69 | "ts-jest": "^19.0.8",
70 | "ts-loader": "^2.0.3",
71 | "tslint": "^5.2.0",
72 | "tslint-loader": "^3.5.3",
73 | "tslint-react": "^3.0.0",
74 | "url-loader": "0.5.8",
75 | "webpack": "2.6.0",
76 | "webpack-dev-server": "2.4.5",
77 | "webpack-manifest-plugin": "1.1.0",
78 | "whatwg-fetch": "2.0.3"
79 | },
80 | "scripts": {
81 | "start": "npm run build && node dist-node/server.js",
82 | "build": "npm run react-build && npm run node-build",
83 | "rstart": "node react/config/scripts/start.js -s",
84 | "nstart": "npm run node-start -s",
85 | "test": "jest",
86 | "react-clean": "rimraf dist-react",
87 | "react-build": "npm run react-clean && node react/config/scripts/build.js",
88 | "react-test": "jest react",
89 | "node-start": "npm run node-build && npm run node-watch",
90 | "node-clean": "rimraf dist-node",
91 | "node-build": "npm run node-clean && npm run node-build-ts",
92 | "node-build-ts": "tsc --p node/config/tsconfig.json",
93 | "node-lint": "tslint -c tslint.json -p node/config/tsconfig.json",
94 | "node-watch": "concurrently -k -p \"[{name}]\" -n \"Type,Node\" -c \"cyan.bold,green.bold\" \"npm run node-watch-ts\" \"nodemon dist-node/server.js -q\" --kill-others",
95 | "node-watch-ts": "tsc -w --p node/config/tsconfig.json",
96 | "node-test": "jest node"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/react/config/env.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const paths = require('./paths');
6 |
7 | // Make sure that including paths.js after env.js will read .env variables.
8 | delete require.cache[require.resolve('./paths')];
9 |
10 | const NODE_ENV = process.env.NODE_ENV;
11 | if (!NODE_ENV) {
12 | throw new Error(
13 | 'The NODE_ENV environment variable is required but was not specified.'
14 | );
15 | }
16 |
17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
18 | var dotenvFiles = [
19 | `${paths.dotenv}.${NODE_ENV}.local`,
20 | `${paths.dotenv}.${NODE_ENV}`,
21 | // Don't include `.env.local` for `test` environment
22 | // since normally you expect tests to produce the same
23 | // results for everyone
24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`,
25 | paths.dotenv,
26 | ].filter(Boolean);
27 |
28 | // Load environment variables from .env* files. Suppress warnings using silent
29 | // if this file is missing. dotenv will never modify any environment variables
30 | // that have already been set.
31 | // https://github.com/motdotla/dotenv
32 | dotenvFiles.forEach(dotenvFile => {
33 | if (fs.existsSync(dotenvFile)) {
34 | require('dotenv').config({
35 | path: dotenvFile,
36 | });
37 | }
38 | });
39 |
40 | // We support resolving modules according to `NODE_PATH`.
41 | // This lets you use absolute paths in imports inside large monorepos:
42 | // https://github.com/facebookincubator/create-react-app/issues/253.
43 | // It works similar to `NODE_PATH` in Node itself:
44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
48 | // We also resolve them to make sure all tools using them work consistently.
49 | const appDirectory = fs.realpathSync(process.cwd());
50 | process.env.NODE_PATH = (process.env.NODE_PATH || '')
51 | .split(path.delimiter)
52 | .filter(folder => folder && !path.isAbsolute(folder))
53 | .map(folder => path.resolve(appDirectory, folder))
54 | .join(path.delimiter);
55 |
56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
57 | // injected into the application via DefinePlugin in Webpack configuration.
58 | const REACT_APP = /^REACT_APP_/i;
59 |
60 | function getClientEnvironment(publicUrl) {
61 | const raw = Object.keys(process.env)
62 | .filter(key => REACT_APP.test(key))
63 | .reduce(
64 | (env, key) => {
65 | env[key] = process.env[key];
66 | return env;
67 | },
68 | {
69 | // Useful for determining whether we’re running in production mode.
70 | // Most importantly, it switches React into the correct mode.
71 | NODE_ENV: process.env.NODE_ENV || 'development',
72 | // Useful for resolving the correct path to static assets in `public`.
73 | // For example,
.
74 | // This should only be used as an escape hatch. Normally you would put
75 | // images into the `src` and `import` them in code to get their paths.
76 | PUBLIC_URL: publicUrl,
77 | }
78 | );
79 | // Stringify all values so we can feed into Webpack DefinePlugin
80 | const stringified = {
81 | 'process.env': Object.keys(raw).reduce(
82 | (env, key) => {
83 | env[key] = JSON.stringify(raw[key]);
84 | return env;
85 | },
86 | {}
87 | ),
88 | };
89 |
90 | return { raw, stringified };
91 | }
92 |
93 | module.exports = getClientEnvironment;
94 |
--------------------------------------------------------------------------------
/react/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebookincubator/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(path, needsSlash) {
15 | const hasSlash = path.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return path.substr(path, path.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${path}/`;
20 | } else {
21 | return path;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right