├── api ├── data │ ├── db-config.js │ ├── seeds │ │ └── 01-cleanup.js │ └── migrations │ │ └── 20210124181032_first-migration.js ├── __tests__ │ └── server.test.js └── server.js ├── index.js ├── .eslintrc.json ├── package.json ├── knexfile.js ├── .gitignore ├── README.md └── jest.config.js /api/data/db-config.js: -------------------------------------------------------------------------------- 1 | const knex = require('knex') 2 | const configs = require('../../knexfile') 3 | 4 | module.exports = knex(configs[process.env.NODE_ENV]) 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const server = require('./api/server') 4 | 5 | const port = process.env.PORT 6 | 7 | server.listen(port, () => { 8 | console.log('listening on ' + port) 9 | }) 10 | -------------------------------------------------------------------------------- /api/data/seeds/01-cleanup.js: -------------------------------------------------------------------------------- 1 | const { clean } = require('knex-cleaner') 2 | 3 | exports.seed = function (knex) { 4 | return clean(knex, { 5 | mode: 'truncate', 6 | ignoreTables: ['knex_migrations', 'knex_migrations_lock'], 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": "latest" 11 | }, 12 | "rules": {} 13 | } 14 | -------------------------------------------------------------------------------- /api/data/migrations/20210124181032_first-migration.js: -------------------------------------------------------------------------------- 1 | exports.up = async (knex) => { 2 | await knex.schema 3 | .createTable('users', (users) => { 4 | users.increments('user_id') 5 | users.string('username', 200).notNullable() 6 | users.string('password', 200).notNullable() 7 | users.timestamps(false, true) 8 | }) 9 | } 10 | 11 | exports.down = async (knex) => { 12 | await knex.schema.dropTableIfExists('users') 13 | } 14 | -------------------------------------------------------------------------------- /api/__tests__/server.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const server = require('../server') 3 | const db = require('../data/db-config') 4 | 5 | beforeAll(async () => { 6 | await db.migrate.rollback() 7 | await db.migrate.latest() 8 | }) 9 | beforeEach(async () => { 10 | await db.seed.run() 11 | }) 12 | afterAll(async () => { 13 | await db.destroy() 14 | }) 15 | 16 | it('sanity check', () => { 17 | expect(true).not.toBe(false) 18 | }) 19 | 20 | describe('server.js', () => { 21 | it('is the correct testing environment', async () => { 22 | expect(process.env.NODE_ENV).toBe('testing') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const helmet = require('helmet') 3 | const cors = require('cors') 4 | const db = require('./data/db-config') 5 | 6 | function getAllUsers() { return db('users') } 7 | 8 | async function insertUser(user) { 9 | // WITH POSTGRES WE CAN PASS A "RETURNING ARRAY" AS 2ND ARGUMENT TO knex.insert/update 10 | // AND OBTAIN WHATEVER COLUMNS WE NEED FROM THE NEWLY CREATED/UPDATED RECORD 11 | const [newUserObject] = await db('users').insert(user, ['user_id', 'username', 'password']) 12 | return newUserObject // { user_id: 7, username: 'foo', password: 'xxxxxxx' } 13 | } 14 | 15 | const server = express() 16 | server.use(express.json()) 17 | server.use(helmet()) 18 | server.use(cors()) 19 | 20 | server.get('/api/users', async (req, res) => { 21 | res.json(await getAllUsers()) 22 | }) 23 | 24 | server.post('/api/users', async (req, res) => { 25 | res.status(201).json(await insertUser(req.body)) 26 | }) 27 | 28 | module.exports = server 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-week-scaffolding-node", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "node index.js", 6 | "server": "nodemon index.js", 7 | "migrate:dev": "knex migrate:latest", 8 | "rollback:dev": "knex migrate:rollback", 9 | "seed:dev": "knex seed:run", 10 | "migrate:prod": "heroku run knex migrate:latest -a YOUR_HEROKU_APP_NAME", 11 | "rollback:prod": "heroku run knex migrate:rollback -a YOUR_HEROKU_APP_NAME", 12 | "database:prod": "heroku pg:psql -a YOUR_HEROKU_APP_NAME", 13 | "seed:prod": "heroku run knex seed:run -a YOUR_HEROKU_APP_NAME", 14 | "test": "cross-env NODE_ENV=testing jest --verbose --runInBand", 15 | "deploy": "git push heroku main" 16 | }, 17 | "engines": { 18 | "node": "16.13.2" 19 | }, 20 | "license": "ISC", 21 | "dependencies": { 22 | "cors": "2.8.5", 23 | "dotenv": "14.3.0", 24 | "express": "4.17.2", 25 | "helmet": "5.0.2", 26 | "knex": "1.0.1", 27 | "knex-cleaner": "1.3.1", 28 | "pg": "8.7.1" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "27.4.0", 32 | "cross-env": "7.0.3", 33 | "eslint": "8.7.0", 34 | "jest": "27.4.7", 35 | "nodemon": "2.0.15", 36 | "supertest": "6.2.2" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/bloominstituteoftechnology/build-week-scaffolding-node.git" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | /* 3 | 4 | PORT=9000 5 | NODE_ENV=development 6 | DEV_DATABASE_URL=postgresql://postgres:password@localhost:5432/database_name 7 | TESTING_DATABASE_URL=postgresql://postgres:password@localhost:5432/testing_database_name 8 | 9 | Put the above in your .env file. Some adjustments in the connection URLs will be needed: 10 | 11 | - 5432 (this is the default TCP port for PostgreSQL, should work as is) 12 | - postgres (in postgres:password, this is the default superadmin user, might work as is) 13 | - password (in postgres:password, replace with the actual password of the postgres user) 14 | - database_name (use the real name of the development database you created in pgAdmin 4) 15 | - testing_database_name (use the real name of the testing database you created in pgAdmin 4) 16 | 17 | */ 18 | const pg = require('pg') 19 | 20 | if (process.env.DATABASE_URL) { 21 | pg.defaults.ssl = { rejectUnauthorized: false } 22 | } 23 | 24 | const sharedConfig = { 25 | client: 'pg', 26 | migrations: { directory: './api/data/migrations' }, 27 | seeds: { directory: './api/data/seeds' }, 28 | } 29 | 30 | module.exports = { 31 | development: { 32 | ...sharedConfig, 33 | connection: process.env.DEV_DATABASE_URL, 34 | }, 35 | testing: { 36 | ...sharedConfig, 37 | connection: process.env.TESTING_DATABASE_URL, 38 | }, 39 | production: { 40 | ...sharedConfig, 41 | connection: process.env.DATABASE_URL, 42 | pool: { min: 2, max: 10 }, 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* 116 | 117 | .vscode 118 | 119 | .DS_Store 120 | *.db3 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Week Scaffolding 2 | 3 | First READ these instructions to get an overview of what's involved in scaffolding an Express + PostgreSQL app that deploys to Heroku. 4 | 5 | Then watch [this video tutorial](https://bloomtech-1.wistia.com/medias/2625bl7sei) for a detailed demonstration of setting up a project, using a Windows dev machine. Other operating systems will require some adjustments. 6 | 7 | **There will have been updates to this repo since the video tutorial was created, so make sure to read these instructions before watching.** 8 | 9 | ## The Stack and Tools 10 | 11 | 1. Web server: [Node & Express](https://expressjs.com/) 12 | 2. Development database: [PostgreSQL 14](https://www.postgresql.org/download/) 13 | 3. Dev database Graphical-User Interface tool: [pgAdmin 4](https://www.pgadmin.org/download/) 14 | 4. Dev database Command-Line Interface tool: [psql](https://www.postgresql.org/docs/14/app-psql.html) 15 | 16 | **Note:** **pgAdmin 4** and **psql** should be bundled with the PostgreSQL installer, but they might not be the latest versions. 17 | 18 | 5. Production cloud service: [Heroku](https://id.heroku.com/login) 19 | 6. Prod database: [Heroku Postgres Addon](https://devcenter.heroku.com/articles/heroku-postgresql) 20 | 7. Prod Command-Line Interface tool: [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) 21 | 22 | ## Important Differences between SQLite and Postgres 23 | 24 | The SQLite database is a file embedded inside the project. PostgreSQL on the other hand is a full-blown server, separate from the Express server. 25 | 26 | This means Postgres and its tooling must be installed on the development machine prior to scaffolding an Express + Postgres app. 27 | 28 | Another difference is that executing migrations for the first time will not make the database pop into existance as was the case with SQLite. You must use the pgAdmin 4 GUI to create the development database by hand. Once the database exists and shows up in pgAdmin 4 you can connect to it using Knex and migrate it. 29 | 30 | In production, we create the database by installing the Postgres Addon from the dashboard of our app on the Heroku website. You can connect pgAdmin 4 to the production db following [these instructions](https://stackoverflow.com/a/63046594/3895791). 31 | 32 | ## Installation of PostgreSQL on the Development Machine 33 | 34 | Install [Postgres](https://www.postgresql.org/download/) on your computer, taking into account that getting psql and pgAdmin 4 up and running might require a bit of research and effort. 35 | 36 | 1. Leave the default options during the Postgres installation wizard (components, location, port number). 37 | 2. You will be asked to create a password for the superadmin "postgres" db user. Enter a simple string using only letters (e.g. "password"). 38 | 3. No need to execute the "Stack Builder" at the end of the installation. You can safely uncheck that and exit the wizard. 39 | 4. The first time you open pgAdmin 4 you will be asked to create another password, this time a master password to be able to use pgAdmin. 40 | 41 | ## Starting a New Project 42 | 43 | - Create a new repository using this template, and clone it to your local. 44 | - Create a `.env` file and follow the instructions inside `knexfile.js`. 45 | - Fix the scripts inside `package.json` to use your Heroku app. 46 | 47 | ## Scripts 48 | 49 | - **start** Runs the app with Node. 50 | - **server** Runs the app with Nodemon. 51 | - **migrate:dev** Migrates the local development db to the latest. 52 | - **rollback:dev** Rolls back migrations in the local dev db. 53 | - **seed:dev** Truncates all tables in the local dev db. 54 | - **deploy** Deploys the main branch to Heroku. Must login to the Heroku CLI and add Heroku as a remote. 55 | - **test** Runs tests. 56 | 57 | **The following scripts NEED TO BE EDITED before using: replace `YOUR_HEROKU_APP_NAME`** 58 | 59 | - **migrate:prod** Migrates the Heroku database to the latest. 60 | - **rollback:prod** Rolls back migrations in the Heroku database. 61 | - **databaseh** Interacts with the Heroku database from the command line using psql. 62 | - **seed:prod** Runs all seeds in the Heroku database. 63 | 64 | ## Tips 65 | 66 | - Figure out deployment before writing any additional code. 67 | 68 | - If you need to make changes to a migration file that has already been released to Heroku, follow this sequence: 69 | 70 | 1. Roll back migrations in the Heroku database 71 | 2. Deploy the latest code to Heroku 72 | 3. Migrate the Heroku database to the latest 73 | 74 | - If your frontend devs are clear on the shape of the data they need, you can quickly build provisional endpoints that return mock data. They shouldn't have to wait for you to build the entire backend. 75 | 76 | - Keep your endpoints super lean: the bulk of the code belongs inside models and other middlewares. 77 | 78 | - Validating and sanitizing client data using a library is much less work than doing it manually. 79 | 80 | - Revealing crash messages to clients is a security risk, but during development it's helpful if your frontend devs are able to tell you what crashed exactly. 81 | 82 | - PostgreSQL comes with [fantastic built-in functions](https://hashrocket.com/blog/posts/faster-json-generation-with-postgresql) for hammering rows into whatever JSON shape. 83 | 84 | - If you want to edit a migration that has already been released but don't want to lose all the data, make a new migration instead. This is a more realistic flow for production apps: prod databases are never migrated down. We can migrate Heroku down freely only because there's no valuable data from customers in it. In this sense, Heroku is acting more like a staging environment than production. 85 | 86 | - If your fronted devs are interested in running the API locally, help them set up PostgreSQL & pgAdmin on their machines, and teach them how to run migrations in their local. This empowers them to (1) help you troubleshoot bugs, (2) obtain the latest code by simply doing a `git pull` and (3) work with their own data, without it being wiped every time you roll back the Heroku db. Collaboration is more fun and direct, and you don't need to deploy as often. 87 | 88 | ## Video Demonstration 89 | 90 | The following demo explains how to set up a project using PostgreSQL and Heroku. 91 | 92 | [![Setting up PostgreSQL for Build Week](https://tk-assets.lambdaschool.com/e43c6d1e-5ae8-4142-937b-b865d71925fb_unit-4-build-week-project-scaffolding.png)](https://bloomtech-1.wistia.com/medias/2625bl7sei) 93 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/78/wsw4bc497l1b0dlb3f77xh380000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jest-circus/runner", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | --------------------------------------------------------------------------------