├── .env.example ├── .env.test ├── .gitignore ├── README.md ├── config ├── db.js ├── env.js ├── getHttpsConfig.js ├── jest │ ├── cssTransform.js │ ├── fileTransform.js │ └── server.json ├── modules.js ├── paths.js ├── secrets.js ├── webpack.config.js └── webpackDevServer.config.js ├── migrations ├── 1530234244089_initial-migration.js ├── 1555584527837_add-users.js └── 1564491788184_posts-add-user-relation.js ├── package-lock.json ├── package.json ├── scripts ├── build.js ├── db-migrate.js ├── db-seed.js ├── start-client-dev.js ├── start-server-dev.js ├── start-server-prod.js └── test.js ├── src-client ├── App.css ├── App.jsx ├── App.test.jsx ├── api │ └── api.jsx ├── components │ ├── auth │ │ ├── sign-in.jsx │ │ └── sign-up.jsx │ ├── header.jsx │ └── posts │ │ ├── posts-add.jsx │ │ ├── posts-container.jsx │ │ └── posts-table.jsx ├── index.css ├── index.jsx ├── logo.svg ├── private-route.jsx ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── store.jsx └── src-server ├── app.js ├── app.test.js ├── components ├── auth │ ├── helpers.js │ ├── jwt.js │ └── local.js ├── posts │ └── index.js └── users │ └── index.js ├── db.js └── routes ├── api └── posts.js └── auth └── index.js /.env.example: -------------------------------------------------------------------------------- 1 | DB_NAME=starter 2 | DB_USER=starter 3 | DB_PASSWORD=password 4 | DB_HOST=localhost 5 | DB_PORT=5432 6 | DB_ENABLE_SSL=1 7 | JWT_SECRET=starter 8 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DB_NAME=starter_test 2 | DB_USER=starter 3 | DB_PASSWORD=password 4 | DB_HOST=localhost 5 | DB_PORT=5432 6 | DB_ENABLE_SSL=0 7 | JWT_SECRET=starter-test-secret 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # public 10 | /public 11 | 12 | # build (output folder) 13 | /build 14 | 15 | .eslintcache 16 | 17 | # misc 18 | .idea 19 | .DS_Store 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starter Full-Stack JS Project: Postgres + Express + React + Node (PERN) 2 | 3 | ## Overview 4 | 5 | The frontend was added from a bootstrapped React project [Create React App](https://github.com/facebookincubator/create-react-app), then ejected and customized. 6 | The backend was added from a bootstrapped Express project [Express Generator](https://expressjs.com/en/starter/generator.html) 7 | 8 | ## Folder Structure 9 | 10 | After creation, your project should look like this: 11 | 12 | ``` 13 | app/ 14 | config/ 15 | migrations/ 16 | scripts/ 17 | src-client/ 18 | src-server/ 19 | package.json 20 | README.md 21 | .env.example 22 | ``` 23 | 24 | 25 | 26 | ## Prerequisites 27 | Before installing, please make sure to have global installations of 28 | * [node v8 or higher](https://nodejs.org/en/download/) 29 | * npm v5 or higher 30 | * [PostgreSQL](https://www.postgresql.org/download/) (if running a local DB instance) 31 | 32 | ## Installation 33 | 1. Execute `npm install` to configure the local environment. 34 | 2. Create `.env` file and define environmental variables (see `.env.example` for example) 35 | 3. Perform DB initialization/migration and seeding `npm run seed` 36 | 4. Start the development server `npm run dev` 37 | 5. Build the production version `npm run build` 38 | 39 | 40 | ## Usage 41 | This application uses npm scripts for testing, development, and deployment. 42 | Note that the pre-commit hook runs the build script which compiles FE and lints BE code. 43 | 44 | ### Primary 45 | * `$ npm run start`: run the production version of the app 46 | * `$ npm run build`: build the production bundle of the FE app (linting is automatically executed), and perform linting of the BE code 47 | * `$ npm run lint`: perform linting of the BE code 48 | * `$ npm run seed`: perform DB initialization/migration and seeding 49 | * `$ npm run dev`: run the development version of the app 50 | * `$ npm run test:client`: run FE tests using Jest 51 | * `$ npm run test:server`: run BE tests using Jest 52 | 53 | ### Secondary 54 | * `$ npm run client:dev`: run Webpack dev server for FE development 55 | * `$ npm run server:dev`: run the development version of BE 56 | * `$ npm run server:prod`: alias of `start` 57 | * `$ npm run pg-migrate`: alias of `node-pg-migrate` module 58 | * `$ npm run db:migrate`: run DB migration scripts 59 | * `$ npm run db:seed`: alias of `seed` 60 | 61 | ## Authentication Endpoints (/auth/*) 62 | This project uses JWT for authentication. 63 | 64 | ### `POST /auth/login`: Authenticate User 65 | This endpoint authenticates a user. An example of the payload (input data) is provided below: 66 | ``` 67 | body: { 68 | email : String, /* required */ 69 | password: String, /* required */ 70 | } 71 | ``` 72 | The output returns JWT token and user object: 73 | ``` 74 | let response = { 75 | statusCode: 200, 76 | body: { 77 | token : String, 78 | user : Object, 79 | } 80 | } 81 | ``` 82 | 83 | ### `POST /auth/register`: Register New User 84 | This endpoint registers a new user. An example of the payload (input data) is provided below: 85 | ``` 86 | body: { 87 | email : String, /* required */ 88 | firstName: String, /* required */ 89 | lastName : String, /* required */ 90 | password : String, /* required */ 91 | } 92 | ``` 93 | The output is the same as from `POST /auth/login` 94 | 95 | ### `GET /auth/me`: Get Current User 96 | This endpoint returns the User object associated with the currently authenticated user. No input data is required 97 | The output is provided is an object with the following structure: 98 | ``` 99 | let response = { 100 | statusCode: 200, 101 | body: { 102 | id : Number, 103 | email : String, 104 | firstName: String, 105 | lastName : String, 106 | createdAt: Date, 107 | } 108 | } 109 | ``` 110 | 111 | ### Seed data (sample user) 112 | ``` 113 | Email: user@test.com 114 | Password: password 115 | ``` 116 | ## API Endpoints (/api/*) 117 | 118 | ### `POST /api/posts`: Create a New Post 119 | This endpoint creates a new Post with current user as author. An example of the payload (input data) is provided below: 120 | ``` 121 | body: { 122 | content: Text, /* required */ 123 | title : String /* required */ 124 | } 125 | ``` 126 | The output echos back the provided data with the system-generated record ID: 127 | ``` 128 | let response = { 129 | statusCode: 200, 130 | body: { 131 | id : Number, 132 | content: Text, 133 | title : String, 134 | user_id: Number, 135 | } 136 | } 137 | ``` 138 | 139 | ### `GET /api/posts`: Get all Posts 140 | This endpoint returns the complete set of available Posts. No input data is required 141 | The output is provided in array with each object having the structure described above: 142 | ``` 143 | let response = { 144 | statusCode: 200, 145 | body: [ 146 | Post1, 147 | Post2, 148 | ... 149 | PostN 150 | ] 151 | } 152 | ``` 153 | 154 | ### `GET /api/posts/:id`: Get a Post by ID 155 | This endpoint returns an individual Post by ID. The ID is provided as a URI parameter. 156 | The output is the same as from `POST /api/posts` 157 | 158 | ### `PUT /api/posts/:id`: Update a Post by ID 159 | This endpoint updates an existing Post by ID. The input/output formats are the same as in `POST /api/posts` 160 | 161 | ### `DELETE /api/posts/:id`: Delete a Post by ID 162 | This endpoint deletes an individual Post by ID. The ID is provided as a URI parameter. 163 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | const hostWithPort = `${process.env.DB_HOST}${process.env.DB_PORT ? `:${process.env.DB_PORT}` : ''}`; 2 | let connectionStr = `postgres://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${hostWithPort}/${process.env.DB_NAME}`; 3 | if (process.env.DB_ENABLE_SSL) connectionStr += '?ssl=true'; 4 | 5 | const schema = 'public'; 6 | 7 | module.exports = { 8 | connectionStr, 9 | schema 10 | }; 11 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const paths = require('./paths'); 4 | 5 | // Make sure that including paths.js after env.js will read .env variables. 6 | delete require.cache[require.resolve('./paths')]; 7 | 8 | const NODE_ENV = process.env.NODE_ENV; 9 | if (!NODE_ENV) { 10 | throw new Error( 11 | 'The NODE_ENV environment variable is required but was not specified.' 12 | ); 13 | } 14 | 15 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 16 | const dotenvFiles = [ 17 | `${paths.dotenv}.${NODE_ENV}.local`, 18 | // Don't include `.env.local` for `test` environment 19 | // since normally you expect tests to produce the same 20 | // results for everyone 21 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 22 | `${paths.dotenv}.${NODE_ENV}`, 23 | paths.dotenv, 24 | ].filter(Boolean); 25 | 26 | // Load environment variables from .env* files. Suppress warnings using silent 27 | // if this file is missing. dotenv will never modify any environment variables 28 | // that have already been set. Variable expansion is supported in .env files. 29 | // https://github.com/motdotla/dotenv 30 | // https://github.com/motdotla/dotenv-expand 31 | dotenvFiles.forEach((dotenvFile) => { 32 | if (fs.existsSync(dotenvFile)) { 33 | require('dotenv-expand')( 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }) 37 | ); 38 | } 39 | }); 40 | 41 | // We support resolving modules according to `NODE_PATH`. 42 | // This lets you use absolute paths in imports inside large monorepos: 43 | // https://github.com/facebook/create-react-app/issues/253. 44 | // It works similar to `NODE_PATH` in Node itself: 45 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 46 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 47 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 48 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 49 | // We also resolve them to make sure all tools using them work consistently. 50 | const appDirectory = fs.realpathSync(process.cwd()); 51 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 52 | .split(path.delimiter) 53 | .filter(folder => folder && !path.isAbsolute(folder)) 54 | .map(folder => path.resolve(appDirectory, folder)) 55 | .join(path.delimiter); 56 | 57 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 58 | // injected into the application via DefinePlugin in webpack configuration. 59 | const REACT_APP = /^REACT_APP_/i; 60 | 61 | function getClientEnvironment(publicUrl) { 62 | const raw = Object.keys(process.env) 63 | .filter(key => REACT_APP.test(key)) 64 | .reduce( 65 | (env, key) => { 66 | env[key] = process.env[key]; 67 | return env; 68 | }, 69 | { 70 | // Useful for determining whether we’re running in production mode. 71 | // Most importantly, it switches React into the correct mode. 72 | NODE_ENV: process.env.NODE_ENV || 'development', 73 | // Useful for resolving the correct path to static assets in `public`. 74 | // For example, . 75 | // This should only be used as an escape hatch. Normally you would put 76 | // images into the `src` and `import` them in code to get their paths. 77 | PUBLIC_URL: publicUrl, 78 | // We support configuring the sockjs pathname during development. 79 | // These settings let a developer run multiple simultaneous projects. 80 | // They are used as the connection `hostname`, `pathname` and `port` 81 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` 82 | // and `sockPort` options in webpack-dev-server. 83 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 84 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 85 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 86 | // Whether or not react-refresh is enabled. 87 | // react-refresh is not 100% stable at this time, 88 | // which is why it's disabled by default. 89 | // It is defined here so it is available in the webpackHotDevClient. 90 | FAST_REFRESH: process.env.FAST_REFRESH !== 'false', 91 | } 92 | ); 93 | // Stringify all values so we can feed into webpack DefinePlugin 94 | const stringified = { 95 | 'process.env': Object.keys(raw).reduce((env, key) => { 96 | env[key] = JSON.stringify(raw[key]); 97 | return env; 98 | }, {}), 99 | }; 100 | 101 | return { raw, stringified }; 102 | } 103 | 104 | module.exports = getClientEnvironment; 105 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/jest/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": "../../src-server", 3 | "collectCoverageFrom": [ 4 | "/**/*.{js,jsx,mjs}" 5 | ], 6 | "testMatch": [ 7 | "/**/__tests__/**/*.js", 8 | "/**/?(*.)(spec|test).js" 9 | ], 10 | "testEnvironment": "node", 11 | "moduleFileExtensions": [ 12 | "js", 13 | "json" 14 | ] 15 | } -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | if (!baseUrl) { 18 | return ''; 19 | } 20 | 21 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 22 | 23 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 24 | // the default behavior. 25 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 26 | return null; 27 | } 28 | 29 | // Allow the user set the `baseUrl` to `appSrc`. 30 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 31 | return [paths.appSrc]; 32 | } 33 | 34 | // If the path is equal to the root directory we ignore it here. 35 | // We don't want to allow importing from the root directly as source files are 36 | // not transpiled outside of `src`. We do allow importing them with the 37 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 38 | // an alias. 39 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 40 | return null; 41 | } 42 | 43 | // Otherwise, throw an error. 44 | throw new Error( 45 | chalk.red.bold( 46 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 47 | ' Create React App does not support other values at this time.' 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 54 | * 55 | * @param {*} options 56 | */ 57 | function getWebpackAliases(options = {}) { 58 | const baseUrl = options.baseUrl; 59 | 60 | if (!baseUrl) { 61 | return {}; 62 | } 63 | 64 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 65 | 66 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 67 | return { 68 | src: paths.appSrc, 69 | }; 70 | } 71 | } 72 | 73 | /** 74 | * Get jest aliases based on the baseUrl of a compilerOptions object. 75 | * 76 | * @param {*} options 77 | */ 78 | function getJestAliases(options = {}) { 79 | const baseUrl = options.baseUrl; 80 | 81 | if (!baseUrl) { 82 | return {}; 83 | } 84 | 85 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 86 | 87 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 88 | return { 89 | '^src/(.*)$': '/src/$1', 90 | }; 91 | } 92 | } 93 | 94 | function getModules() { 95 | // Check if TypeScript is setup 96 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 97 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 98 | 99 | if (hasTsConfig && hasJsConfig) { 100 | throw new Error( 101 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 102 | ); 103 | } 104 | 105 | let config; 106 | 107 | // If there's a tsconfig.json we assume it's a 108 | // TypeScript project and set up the config 109 | // based on tsconfig.json 110 | if (hasTsConfig) { 111 | const ts = require(resolve.sync('typescript', { 112 | basedir: paths.appNodeModules, 113 | })); 114 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 115 | // Otherwise we'll check if there is jsconfig.json 116 | // for non TS projects. 117 | } else if (hasJsConfig) { 118 | config = require(paths.appJsConfig); 119 | } 120 | 121 | config = config || {}; 122 | const options = config.compilerOptions || {}; 123 | 124 | const additionalModulePaths = getAdditionalModulePaths(options); 125 | 126 | return { 127 | additionalModulePaths: additionalModulePaths, 128 | webpackAliases: getWebpackAliases(options), 129 | jestAliases: getJestAliases(options), 130 | hasTsConfig, 131 | }; 132 | } 133 | 134 | module.exports = getModules(); 135 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 4 | 5 | // Make sure any symlinks in the project folder are resolved: 6 | // https://github.com/facebook/create-react-app/issues/637 7 | const appDirectory = fs.realpathSync(process.cwd()); 8 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 9 | 10 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 11 | // "public path" at which the app is served. 12 | // webpack needs to know it to put the right