├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── config └── dbConfig.js ├── database ├── migrations │ └── 20191226143741_users.js └── seeds │ └── 001_users.js ├── index.js ├── knexfile.js ├── models └── users-model.js ├── package.json ├── routes └── users-router.js ├── server.js └── tests ├── server.test.js └── users.test.js /.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 | # TypeScript v1 declaration files 45 | typings/ 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 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roenz Aberin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-knex-postgres-boilerplate 2 | 3 | Boilerplate code for quick setup for CRUD applications using express/knex/postgres/jest/supertest 4 | 5 | ##Setup - Detailed Instructions Below 6 | 7 | 1. Git clone the repo ```git clone [url]``` and remove origin ```git remote remove origin``` 8 | 2. npm install 9 | 3. setup postgres backend 10 | 4. Modify .env file to suit your backend and migrate/seed db 11 | 1. migrate tables ```npx knex migrate:latest``` 12 | 2. run seeds ```npx knex seed:run``` 13 | 5. npm run server 14 | 6. npm run test 15 | 7. modify code to suit your needs 16 | 17 | ## Setup PostgreSQL 18 | 19 | ### Homebrew (for macOS users) 20 | 21 | If you dont have postgres follow this link (Follow directions until you're able to get into psql utility): https://www.codementor.io/engineerapart/getting-started-with-postgresql-on-mac-osx-are8jcopb 22 | 23 | #### Create dev and test database (Mac) 24 | 25 | In terminal run the following commands: 26 | 27 | 1. ```psql``` -- To get into postgreSQL utility 28 | 2. ```CREATE DATABASE db-name;``` -- Creates development server 29 | 3. ```CREATE DATABASE db-name-test;``` -- Creates testing server 30 | 4. ```\q``` 31 | 5. CD into your repo 32 | 33 | ### Windows 34 | 35 | If you dont have postgres follow this link: https://www.2ndquadrant.com/en/blog/pginstaller-install-postgresql/ 36 | 37 | #### Create dev and test databases (Windows) 38 | 39 | Set up Postgres and create databases for both the development server (db-name) and testing server (db-name-test) 40 | 41 | 1. Open pgAdmin, sign in with your master password created during the set up of postgres. 42 | 2. Create a server if needed, if already created, turn server on by right clicking and pressing "Connect Server" 43 | 3. Once connected, look for the drop down for databases and right click to Create a database 44 | 4. Create a database called 'db-name' for the development connection & (db-name-test) for the testing connection 45 | 46 | ## Environmental Variables at Runtime 47 | 48 | Create a ".env" file at the root of your project and add the following for both DEV and TEST databases 49 | 50 | ``` 51 | POSTGRES_DEV_HOST=localhost 52 | POSTGRES_DEV_PORT=5432 53 | POSTGRES_DEV_USER=postgres 54 | POSTGRES_DEV_PASSWORD= \_Insert your postgres password here* 55 | POSTGRES_DEV_DATABASE=db-name 56 | ``` 57 | 58 | ``` 59 | POSTGRES_TEST_HOST=localhost 60 | POSTGRES_TEST_PORT=5432 61 | POSTGRES_TEST_USER=postgres 62 | POSTGRES_TEST_PASSWORD= \_Insert your postgres password here* 63 | POSTGRES_TEST_DATABASE=db-name-test 64 | ``` 65 | -------------------------------------------------------------------------------- /config/dbConfig.js: -------------------------------------------------------------------------------- 1 | const knex = require("knex"); 2 | const knexConfig = require("../knexfile"); 3 | 4 | const environment = process.env.DB_ENV || "development"; 5 | 6 | module.exports = knex(knexConfig[environment]); 7 | -------------------------------------------------------------------------------- /database/migrations/20191226143741_users.js: -------------------------------------------------------------------------------- 1 | exports.up = function(knex, Promise) { 2 | return knex.schema.createTable("users", tbl => { 3 | tbl.increments(); 4 | 5 | tbl.string("name"); 6 | }); 7 | }; 8 | 9 | exports.down = function(knex, Promise) { 10 | return knex.schema.dropTableIfExists("users"); 11 | }; 12 | -------------------------------------------------------------------------------- /database/seeds/001_users.js: -------------------------------------------------------------------------------- 1 | exports.seed = function(knex) { 2 | // Deletes ALL existing entries 3 | return knex("users") 4 | .del() 5 | .then(function() { 6 | // Inserts seed entries 7 | return knex("users").insert([ 8 | { name: "Bob" }, 9 | { name: "Roenz" }, 10 | { name: "Joe" } 11 | ]); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const server = require("./server"); 3 | 4 | const port = process.env.PORT || 5000; 5 | 6 | server.listen(port, () => console.log(`API running on port ${port}`)); 7 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const pg = require("pg"); 3 | // pg.defaults.ssl = true; 4 | 5 | module.exports = { 6 | development: { 7 | client: "pg", 8 | useNullAsDefault: true, 9 | connection: { 10 | host: process.env.POSTGRES_DEV_HOST, 11 | port: process.env.POSTGRES_DEV_PORT, 12 | user: process.env.POSTGRES_DEV_USER, 13 | password: process.env.POSTGRES_DEV_PASSWORD, 14 | database: process.env.POSTGRES_DEV_DATABASE 15 | }, 16 | migrations: { 17 | directory: "./database/migrations" 18 | }, 19 | seeds: { 20 | directory: "./database/seeds" 21 | } 22 | }, 23 | 24 | testing: { 25 | client: "pg", 26 | connection: { 27 | host: process.env.POSTGRES_TEST_HOST, 28 | port: process.env.POSTGRES_TEST_PORT, 29 | user: process.env.POSTGRES_TEST_USER, 30 | password: process.env.POSTGRES_TEST_PASSWORD, 31 | database: process.env.POSTGRES_TEST_DATABASE 32 | }, 33 | useNullAsDefault: true, 34 | migrations: { 35 | directory: "./database/migrations" 36 | }, 37 | seeds: { 38 | directory: "./database/seeds" 39 | } 40 | }, 41 | 42 | production: { 43 | client: "pg", 44 | useNullAsDefault: true, 45 | 46 | connection: process.env.DATABASE_URL, 47 | 48 | migrations: { 49 | directory: "./database/migrations" 50 | }, 51 | seeds: { 52 | directory: "./database/seeds" 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /models/users-model.js: -------------------------------------------------------------------------------- 1 | const db = require("../config/dbConfig.js"); 2 | 3 | // GET ALL USERS 4 | const find = () => { 5 | return db("users"); 6 | }; 7 | 8 | // GET SPECIFIC USER BY ID 9 | const findById = id => { 10 | return db("users").where("id", id); 11 | 12 | //SQL RAW METHOD 13 | // return db.raw(`SELECT * FROM users 14 | // WHERE id = ${id}`); 15 | }; 16 | 17 | // ADD A USER 18 | const addUser = user => { 19 | return db("users").insert(user, "id"); 20 | }; 21 | 22 | // UPDATE USER 23 | const updateUser = (id, post) => { 24 | return db("users") 25 | .where("id", id) 26 | .update(post); 27 | }; 28 | 29 | // REMOVE USER 30 | const removeUser = id => { 31 | return db("users") 32 | .where("id", id) 33 | .del(); 34 | }; 35 | 36 | module.exports = { 37 | find, 38 | findById, 39 | addUser, 40 | updateUser, 41 | removeUser 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-knex-postgres-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Boilerplate code for quick setup for CRUD applications using express/knex/postgres", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "DB_ENV=testing jest --watch --verbose", 8 | "server": "nodemon index.js", 9 | "start": "node index.js" 10 | }, 11 | "jest": { 12 | "testEnvironment": "node" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/raberin/express-knex-postgres-boilerplate.git" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/raberin/express-knex-postgres-boilerplate/issues" 23 | }, 24 | "homepage": "https://github.com/raberin/express-knex-postgres-boilerplate#readme", 25 | "dependencies": { 26 | "cors": "^2.8.5", 27 | "dotenv": "^8.2.0", 28 | "express": "^4.17.1", 29 | "knex": "^0.20.4", 30 | "morgan": "^1.9.1", 31 | "pg": "^7.15.2" 32 | }, 33 | "devDependencies": { 34 | "cross-env": "^6.0.3", 35 | "jest": "^24.9.0", 36 | "nodemon": "^2.0.2", 37 | "supertest": "^4.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /routes/users-router.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | 3 | const usersDB = require("../models/users-model.js"); 4 | 5 | // GET ALL USERS 6 | router.get("/", async (req, res) => { 7 | try { 8 | const users = await usersDB.find(); 9 | res.status(200).json(users); 10 | } catch (err) { 11 | res.status(500).json({ err: err }); 12 | } 13 | }); 14 | 15 | // GET USER BY ID 16 | router.get("/:id", async (req, res) => { 17 | const userId = req.params.id; 18 | try { 19 | const user = await usersDB.findById(userId); 20 | if (!user) { 21 | res 22 | .status(404) 23 | .json({ err: "The user with the specified id does not exist" }); 24 | } else { 25 | res.status(200).json(user); 26 | } 27 | } catch (err) { 28 | res.status(500).json({ err: "The user information could not be retrieved" }); 29 | } 30 | }); 31 | 32 | // INSERT USER INTO DB 33 | router.post("/", async (req, res) => { 34 | const newUser = req.body; 35 | if (!newUser.name) { 36 | res.status(404).json({ err: "Please provide the name" }); 37 | } else { 38 | try { 39 | const user = await usersDB.addUser(newUser); 40 | res.status(201).json(user); 41 | } catch (err) { 42 | res.status(500).json({ err: "Error in adding user" }); 43 | } 44 | } 45 | }); 46 | 47 | router.put("/:id", async (req, res) => { 48 | const userId = req.params.id; 49 | const newChanges = req.body; 50 | if (!newChanges.name) { 51 | res.status(404).json({ err: "You are missing information" }); 52 | } else { 53 | try { 54 | const addChanges = await usersDB.updateUser(userId, newChanges); 55 | res.status(200).json(addChanges); 56 | } catch (err) { 57 | res.status(500).json({ err: "Error in updating user" }); 58 | } 59 | } 60 | }); 61 | 62 | router.delete("/:id", async (req, res) => { 63 | const userId = req.params.id; 64 | try { 65 | const deleting = await usersDB.removeUser(userId); 66 | res.status(204).json(deleting); 67 | } catch (err) { 68 | res.status(500).json({ err: "Error in deleting user" }); 69 | } 70 | }); 71 | 72 | module.exports = router; 73 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const server = express(); 3 | 4 | const morgan = require("morgan"); 5 | const cors = require("cors"); 6 | 7 | const usersRouter = require("./routes/users-router.js"); 8 | 9 | // Middleware 10 | server.use(cors()); 11 | server.use(morgan("dev")); 12 | server.use(express.json()); 13 | 14 | // Routers 15 | server.use("/api/users", usersRouter); 16 | 17 | //Routes 18 | server.get("/", (req, res) => { 19 | res.status(200).json({ hello: "World!" }); 20 | }); 21 | 22 | module.exports = server; 23 | -------------------------------------------------------------------------------- /tests/server.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const server = require("../server.js"); 3 | 4 | describe("server.js", () => { 5 | describe("index route", () => { 6 | it("should return status 200", async () => { 7 | const user = await request(server) 8 | .get("/") 9 | .expect(200); 10 | }); 11 | 12 | it("should return a JSON object fron the index route", async () => { 13 | const response = await request(server).get("/"); 14 | 15 | expect(response.type).toEqual("application/json"); 16 | }); 17 | 18 | it("should return a hello World! JSON", async () => { 19 | const expectedBody = { hello: "World!" }; 20 | const response = await request(server).get("/"); 21 | expect(response.body).toEqual(expectedBody); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/users.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const server = require("../server"); 3 | 4 | const db = require("../config/dbConfig.js"); 5 | 6 | beforeAll(async () => { 7 | await db("users").insert([ 8 | { name: "Roenz" }, 9 | { name: "Joe" }, 10 | { name: "Bob" } 11 | ]); 12 | }); 13 | 14 | afterAll(async () => { 15 | await db.raw(`TRUNCATE TABLE users RESTART IDENTITY CASCADE`); 16 | }); 17 | 18 | describe("users endpoints", () => { 19 | describe("GET /", () => { 20 | it("should return 200", async () => { 21 | const response = await request(server) 22 | .get("/api/users") 23 | .expect(200); 24 | }); 25 | it("should be an object/array", async () => { 26 | const response = await request(server) 27 | .get("/api/users") 28 | .expect(200); 29 | expect(typeof response.body).toBe("object"); 30 | }); 31 | it("should return a length of 3", async () => { 32 | const response = await request(server) 33 | .get("/api/users") 34 | .expect(200); 35 | expect(response.body.length).toBe(3); 36 | }); 37 | }); 38 | describe("GET /:id", () => { 39 | it("should return 200", async () => { 40 | const response = await request(server) 41 | .get("/api/users/1") 42 | .expect(200); 43 | }); 44 | it("should be an object/array", async () => { 45 | const response = await request(server) 46 | .get("/api/users/1") 47 | .expect(200); 48 | expect(typeof response.body).toBe("object"); 49 | }); 50 | it("should return the right user", async () => { 51 | const expected = { id: 1, name: "Roenz" }; 52 | const response = await request(server) 53 | .get("/api/users/1") 54 | .expect(200); 55 | expect(response.body[0].name).toBe(expected.name); 56 | }); 57 | }); 58 | describe("POST /", () => { 59 | it("adds a user into db", async () => { 60 | const user = { name: "Test" }; 61 | const posting = await request(server) 62 | .post("/api/users/") 63 | .send(user) 64 | .set("Accept", "application/json") 65 | .expect("Content-Type", /json/) 66 | .expect(201); 67 | 68 | expect(posting.body).toEqual([4]); 69 | }); 70 | it("its the right user", async () => { 71 | const getUser = await request(server).get("/api/users/4"); 72 | expect(getUser.body[0].name).toEqual("Test"); 73 | }); 74 | }); 75 | 76 | describe("PUT /", () => { 77 | it("changes name of user", async () => { 78 | const user = { name: "updatedTest" }; 79 | const updating = await request(server) 80 | .put("/api/users/4") 81 | .send(user) 82 | .set("Accept", "application/json") 83 | .expect("Content-Type", /json/) 84 | .expect(200); 85 | 86 | const getUser = await request(server).get("/api/users/4"); 87 | console.log(getUser.body); 88 | }); 89 | it("its the right user", async () => { 90 | const getUser = await request(server).get("/api/users/4"); 91 | expect(getUser.body[0].name).toEqual("updatedTest"); 92 | }); 93 | }); 94 | 95 | describe("DELETE /", () => { 96 | it("should return 204", async () => { 97 | const deleting = await request(server) 98 | .delete("/api/users/4") 99 | .expect(204); 100 | }); 101 | it("should have a length of 3", async () => { 102 | const getUsers = await request(server).get("/api/users/"); 103 | expect(getUsers.body.length).toEqual(3); 104 | }); 105 | }); 106 | }); 107 | --------------------------------------------------------------------------------