├── seeds └── 01-product.js ├── knexfile.js ├── db ├── connection.js └── queries.js ├── migrations └── 20180219203613_product.js ├── package.json ├── .gitignore ├── README.md ├── products.js ├── app.js ├── bin └── www └── api └── product.js /seeds/01-product.js: -------------------------------------------------------------------------------- 1 | const products = require('../products'); 2 | 3 | exports.seed = function(knex, Promise) { 4 | return knex('product').del() 5 | .then(function () { 6 | return knex('product').insert(products); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | development: { 4 | client: 'pg', 5 | connection: 'postgres://localhost/cjs_store' 6 | }, 7 | production: { 8 | client: 'pg', 9 | connection: process.env.DATABASE_URL 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /db/connection.js: -------------------------------------------------------------------------------- 1 | const environment = process.env.NODE_ENV || 'development'; 2 | const config = require('../knexfile'); 3 | const environmentConfig = config[environment]; 4 | const knex = require('knex'); 5 | const connection = knex(environmentConfig); 6 | 7 | module.exports = connection; 8 | -------------------------------------------------------------------------------- /migrations/20180219203613_product.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return knex.schema.createTable('product', (table) => { 4 | table.increments(); 5 | table.text('title').notNullable(); 6 | table.text('description'); 7 | table.decimal('price').notNullable(); 8 | table.integer('quantity').unsigned().notNullable(); 9 | table.text('image'); 10 | }); 11 | }; 12 | 13 | exports.down = function(knex, Promise) { 14 | return knex.schema.dropTableIfExists('product'); 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "dev": "nodemon ./bin/www" 8 | }, 9 | "dependencies": { 10 | "body-parser": "~1.17.1", 11 | "cookie-parser": "~1.4.3", 12 | "cors": "^2.8.4", 13 | "debug": "~2.6.3", 14 | "express": "~4.15.2", 15 | "jade": "~1.11.0", 16 | "knex": "^0.14.4", 17 | "morgan": "~1.8.1", 18 | "nodemon": "^1.15.1", 19 | "pg": "^7.4.1", 20 | "serve-favicon": "~2.4.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /db/queries.js: -------------------------------------------------------------------------------- 1 | const connection = require('./connection'); 2 | 3 | module.exports = { 4 | getAll() { 5 | return connection('product'); 6 | }, 7 | getOne(id) { 8 | return connection('product').where('id', id).first(); 9 | }, 10 | create(product) { 11 | return connection('product').insert(product, 'id').then(ids => { 12 | return ids[0]; 13 | }); 14 | }, 15 | update(id, product) { 16 | return connection('product').where('id', id).update(product); 17 | }, 18 | delete(id) { 19 | return connection('product').where('id', id).del(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/cli/shrinkwrap#caveats 27 | node_modules 28 | 29 | # Debug log from npm 30 | npm-debug.log 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Product API Server 2 | 3 | # NOTE: The server is now running on PORT 5000! http://localhost:5000/api/v1/products 4 | 5 | ## Pre-requisites 6 | 7 | * postgres installed locally 8 | * OSX - brew install postgres 9 | * npm install -g knex 10 | 11 | ## Setup 12 | 13 | ```sh 14 | npm install # install dependencies 15 | createdb cjs_store # create postgres db 16 | knex migrate:latest # create tables in db 17 | knex seed:run # add sample data to db 18 | ``` 19 | 20 | ## Development Start 21 | 22 | ```sh 23 | npm run dev 24 | ``` 25 | 26 | ## Production Start 27 | 28 | ```sh 29 | npm start 30 | ``` 31 | 32 | ## Explore API 33 | 34 | * Server is listening on PORT 5000. 35 | * http://localhost:5000/api/v1/products -------------------------------------------------------------------------------- /products.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | title: 'Macbook Pro', 3 | description: 'A laptop for cool people.', 4 | price: 999.99, 5 | quantity: 10, 6 | image: 'https://i.imgur.com/hVOxDYK.jpg' 7 | }, { 8 | title: 'Skateboard', 9 | description: 'Full setup! Get skateboarding today!', 10 | price: 99.99, 11 | quantity: 5, 12 | image: 'https://i.imgur.com/46g9Bha.jpg' 13 | }, { 14 | title: 'Speaker System', 15 | description: 'Listen to music and things real loud!', 16 | price: 199.99, 17 | quantity: 2, 18 | image: 'https://i.imgur.com/yzVv6kW.jpg' 19 | }, { 20 | title: 'T-Shirt', 21 | description: 'Plane, white, T.', 22 | price: 9.99, 23 | quantity: 20, 24 | image: 'https://i.imgur.com/gHRIpPB.jpg' 25 | }, { 26 | title: 'Shoes', 27 | description: 'Vans shoes for skateboarding, or not.', 28 | price: 49.99, 29 | quantity: 5, 30 | image: 'https://i.imgur.com/vhvmkAM.jpg' 31 | }]; 32 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('morgan'); 3 | const cookieParser = require('cookie-parser'); 4 | const bodyParser = require('body-parser'); 5 | const cors = require('cors'); 6 | 7 | const product = require('./api/product'); 8 | 9 | const app = express(); 10 | 11 | // uncomment after placing your favicon in /public 12 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 13 | app.use(logger('dev')); 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(cookieParser()); 17 | app.use(cors()); 18 | 19 | app.get('/', (req, res) => { 20 | res.json({ 21 | message: 'Hello World!' 22 | }); 23 | }); 24 | 25 | app.use('/api/v1/products', product); 26 | 27 | // catch 404 and forward to error handler 28 | app.use(function(req, res, next) { 29 | const err = new Error('Not Found'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | // error handler 35 | app.use(function(err, req, res, next) { 36 | res.status(err.status || 500); 37 | res.json({ 38 | message: err.message, 39 | error: req.app.get('env') === 'development' ? err.stack : {} 40 | }); 41 | }); 42 | 43 | module.exports = app; 44 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '5000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /api/product.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | 5 | const queries = require('../db/queries'); 6 | 7 | function validProduct(product) { 8 | return typeof product.title == 'string' && 9 | product.title.trim() != '' && 10 | !isNaN(product.price) && 11 | product.price > 0 && 12 | Number.isInteger(product.quantity) && 13 | product.quantity >= 0; 14 | } 15 | 16 | function validId(req, res, next) { 17 | if(!isNaN(req.params.id)) { 18 | next(); 19 | } else { 20 | const error = new Error('Invalid id'); 21 | next(error); 22 | } 23 | } 24 | 25 | function validProductMiddleware(req, res, next) { 26 | if(validProduct(req.body)) { 27 | next(); 28 | } else { 29 | const error = new Error('Invalid product'); 30 | next(error); 31 | } 32 | } 33 | 34 | function getProductFromBody(body) { 35 | const { title, description, price, quantity, image } = body; 36 | // insert into the DB 37 | const product = { 38 | title, 39 | description, 40 | price, 41 | quantity, 42 | image 43 | }; 44 | 45 | return product; 46 | } 47 | 48 | // /api/v1/products 49 | router.get('/', (req, res) => { 50 | queries 51 | .getAll() 52 | .then(products => { 53 | res.json(products); 54 | }); 55 | }); 56 | 57 | router.get('/:id', validId, (req, res, next) => { 58 | queries 59 | .getOne(req.params.id) 60 | .then(product => { 61 | if(product) { 62 | res.json(product); 63 | } else { 64 | next(); 65 | } 66 | }); 67 | }); 68 | 69 | router.post('/', validProductMiddleware, (req, res) => { 70 | const product = getProductFromBody(req.body); 71 | 72 | queries 73 | .create(product) 74 | .then(id => { 75 | res.json({ 76 | id 77 | }); 78 | }); 79 | }); 80 | 81 | router.put('/:id', validId, validProductMiddleware, (req, res) => { 82 | const product = getProductFromBody(req.body); 83 | 84 | queries 85 | .update(req.params.id, product) 86 | .then(() =>{ 87 | res.json({ 88 | message: 'Updated!' 89 | }); 90 | }); 91 | }); 92 | 93 | router.delete('/:id', validId, (req, res) => { 94 | queries 95 | .delete(req.params.id) 96 | .then(() => { 97 | res.json({ 98 | message: 'Deleted!' 99 | }); 100 | }); 101 | }); 102 | 103 | module.exports = router; 104 | --------------------------------------------------------------------------------