Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
57 changes: 41 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![cf](https://i.imgur.com/7v5ASc8.png) Lab 09: Vanilla REST API w/ Persistence
![cf](https://i.imgur.com/7v5ASc8.png) Lab 08: Vanilla REST API
======

## Submission Instructions
Expand All @@ -10,23 +10,48 @@
* write a question and observation on canvas

## Learning Objectives
* students will learn how to save resource data to the file system for a layer of data persistence
* students will learn how to refactor commonly used coding constructs into custom helper modules
* students will learn to use promise constructs to manage asynchronous code
* students will learn to create a vanilla RESTful API with in-memory persistence

## Requirements

#### Configuration
* `package.json`
* `.eslintrc`
* `.gitignore`
* `README.md`
* your `README.md` should include detailed instructions on how to use your API
* this should include documentation on how to access your API endpoints
* `.gitignore`
* `.eslintrc`
* `package.json`
* `README.md`

#### Feature Tasks
* continue working on your vanilla REST API
* refactor your routes to be contained in a separate module (ex: `route/resource-route.js`)
* refactor your `res` messages & status codes to be contained in a separate module (ex: `response.js`)
* refactor the `storage.js` module to use file system persistence
* use the `fs` module to create and read the associated data files
* the name of the file should contain the related resource id
* create the following directories to organize your code:
* `lib`
* `model`
* `test`
* create an HTTP server using the native NodeJS `http` module
* create an object constructor that creates a _simple resource_ with at least 3 properties
* include an `id` property that is set to a unique id (**hint:** you'll need to use `node-uuid`)
* include two additional properties of your choice (ex: name, content, etc.)
* create a custom body parser module that uses promises to parse the JSON body of `POST` and `PUT` requests
* create a custom url parser module that returns a promise and uses the NodeJS `url` and `querystring` modules to parse the request url
* create a router constructor that handles requests to `GET`, `POST`, `PUT`, and `DELETE` requests
* create a storage module that will store resources by their schema type (ex: note) and id

## Server Endpoints
### `/api/simple-resource-name`
* `POST` request
* pass data as stringifed JSON in the body of a **POST** request to create a new resource
* `GET` request
* pass `?id=<uuid>` as a query string parameter to retrieve a specific resource (as JSON)
* `DELETE` request
* pass `?id=<uuid>` in the query string to **DELETE** a specific resource
* this should return a 204 status code with no content in the body

## Tests
* write a test to ensure that your api returns a status code of 404 for routes that have not been registered
* write tests to ensure the `/api/simple-resource-name` endpoint responds as described for each condition below:
* `GET`: test 404, it should respond with 'not found' for valid requests made with an id that was not found
* `GET`: test 400, it should respond with 'bad request' if no id was provided in the request
* `GET`: test 200, it should contain a response body for a request made with a valid id
* `POST`: test 400, it should respond with 'bad request' if no request body was provided or the body was invalid
* `POST`: test 200, it should respond with the body content for a post request with a valid body

## Bonus
* **2pts:** a `GET` request to `/api/simple-resource-name` with no **?id=** should return an array of all of the ids for that resource
1 change: 1 addition & 0 deletions data/bake/a325357c-0dbb-4ae8-b3bd-7286de9bba24.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"a325357c-0dbb-4ae8-b3bd-7286de9bba24","bakedGood":"muffin","description":"naked cupcake","calories":255}
1 change: 1 addition & 0 deletions data/bake/b935f3eb-179d-47c2-9e9a-a484b20be924.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"b935f3eb-179d-47c2-9e9a-a484b20be924","bakedGood":"muffin","description":"naked cupcake","calories":255}
30 changes: 30 additions & 0 deletions lib/parse-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

module.exports = function(req) {
return new Promise((resolve, reject) => {
if (req.method === 'POST' || req.method === 'PUT') {
var body = '';

req.on('data', data => {
body += data.toString();
});

req.on('end', () => {
try {
req.body = JSON.parse(body);
resolve(req);
} catch (err) {
reject(err);
}
});

req.on('error', err => {
reject(err);
});

return;
}

resolve();
});
};
11 changes: 11 additions & 0 deletions lib/parse-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

const parseUrl = require('url').parse;
const parseQuery = require('querystring').parse;

module.exports = function(req) {
req.url = parseUrl(req.url);
req.url.query = parseQuery(req.url.query);

return Promise.resolve(req);
};
15 changes: 15 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

module.exports = exports = {};

exports.sendJSON = function(res, status, data) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.write(JSON.stringify(data));
res.end();
};

exports.sendText = function(res, status, msg) {
res.writeHead(status, { 'Content-Type': 'text/plain' });
res.write(msg);
res.end();
};
63 changes: 63 additions & 0 deletions lib/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const parseUrl = require('./parse-url.js');
const parseJSON = require('./parse-json.js');

const Router = module.exports = function() {
this.routes = {
GET: {},
POST: {},
PUT: {},
DELETE: {}
};
};

Router.prototype.get = function(endpoint, callback) {
this.routes.GET[endpoint] = callback;
};

Router.prototype.post = function(endpoint, callback) {
this.routes.POST[endpoint] = callback;
};

Router.prototype.put = function(endpoint, callback) {
this.routes.PUT[endpoint] = callback;
};

Router.prototype.delete = function(endpoint, callback) {
this.routes.DELETE[endpoint] = callback;
};

Router.prototype.route = function() {
return (req, res) => {
Promise.all([
parseUrl(req),
parseJSON(req)
])
.then ( () => {
if (typeof this.routes[req.method][req.url.pathname] === 'function') {
this.routes[req.method][req.url.pathname](req,res);
return;
}

res.writeHead(404, { 'Content-Type': 'text/plain' });
res.write('route not found');
res.end();
})
.catch( err => {
// console.error(err);
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.write('bad request');
res.end();
});
};
};









41 changes: 41 additions & 0 deletions lib/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'), { suffix: 'Prom' });

module.exports = exports = {};

exports.createItem = function(schemaName, item) {
if (!schemaName) return Promise.reject(new Error('expected schema name'));
if (!item) return Promise.reject(new Error('expected item'));

let json = JSON.stringify(item);
return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`, json)
.then( () => item)
.catch( err => Promise.reject(err));
};

exports.fetchItem = function(schemaName, id) {
if (!schemaName) return Promise.reject(new Error('expected schema name'));
if (!id) return Promise.reject(new Error('expected id'));

return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`)
.then( data => {
try {
let item = JSON.parse(data.toString());
return item;
} catch (err) {
return Promise.reject(err);
}
})
.catch( err => Promise.reject(err));
};

exports.deleteItem = function(schemaName, id) {
if (!schemaName) return Promise.reject(new Error('expected schema name'));
if (!id) return Promise.reject(new Error('expected id'));

return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`)
.then( () => console.log(`${id} deleted`))
.catch( err => Promise.reject(err));
};
14 changes: 14 additions & 0 deletions model/bake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

const uuidv4 = require('uuid/v4');

module.exports = function(bakedGood, description, calories) {
if (!bakedGood) throw new Error('expected baked good');
if (!description) throw new Error('expected description');
if (!calories) throw new Error('expected calories');

this.id = uuidv4();
this.bakedGood = bakedGood;
this.description = description;
this.calories = calories;
};
48 changes: 48 additions & 0 deletions npm-debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
0 info it worked if it ends with ok
1 verbose cli [ '/Users/devjonah/.nvm/versions/node/v6.11.1/bin/node',
1 verbose cli '/Users/devjonah/.nvm/versions/node/v6.11.1/bin/npm',
1 verbose cli 'run',
1 verbose cli 'test' ]
2 info using npm@3.10.10
3 info using node@v6.11.1
4 verbose run-script [ 'pretest', 'test', 'posttest' ]
5 info lifecycle 08-vanilla_rest_api@1.0.0~pretest: 08-vanilla_rest_api@1.0.0
6 silly lifecycle 08-vanilla_rest_api@1.0.0~pretest: no script for pretest, continuing
7 info lifecycle 08-vanilla_rest_api@1.0.0~test: 08-vanilla_rest_api@1.0.0
8 verbose lifecycle 08-vanilla_rest_api@1.0.0~test: unsafe-perm in lifecycle true
9 verbose lifecycle 08-vanilla_rest_api@1.0.0~test: PATH: /Users/devjonah/.nvm/versions/node/v6.11.1/lib/node_modules/npm/bin/node-gyp-bin:/Users/devjonah/codefellows/401/labs/09-vanilla_api_persistence/node_modules/.bin:/Users/devjonah/.nvm/versions/node/v6.11.1/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/git/bin
10 verbose lifecycle 08-vanilla_rest_api@1.0.0~test: CWD: /Users/devjonah/codefellows/401/labs/09-vanilla_api_persistence
11 silly lifecycle 08-vanilla_rest_api@1.0.0~test: Args: [ '-c', 'mocha' ]
12 silly lifecycle 08-vanilla_rest_api@1.0.0~test: Returned: code: 1 signal: null
13 info lifecycle 08-vanilla_rest_api@1.0.0~test: Failed to exec test script
14 verbose stack Error: 08-vanilla_rest_api@1.0.0 test: `mocha`
14 verbose stack Exit status 1
14 verbose stack at EventEmitter.<anonymous> (/Users/devjonah/.nvm/versions/node/v6.11.1/lib/node_modules/npm/lib/utils/lifecycle.js:255:16)
14 verbose stack at emitTwo (events.js:106:13)
14 verbose stack at EventEmitter.emit (events.js:191:7)
14 verbose stack at ChildProcess.<anonymous> (/Users/devjonah/.nvm/versions/node/v6.11.1/lib/node_modules/npm/lib/utils/spawn.js:40:14)
14 verbose stack at emitTwo (events.js:106:13)
14 verbose stack at ChildProcess.emit (events.js:191:7)
14 verbose stack at maybeClose (internal/child_process.js:891:16)
14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:226:5)
15 verbose pkgid 08-vanilla_rest_api@1.0.0
16 verbose cwd /Users/devjonah/codefellows/401/labs/09-vanilla_api_persistence
17 error Darwin 16.6.0
18 error argv "/Users/devjonah/.nvm/versions/node/v6.11.1/bin/node" "/Users/devjonah/.nvm/versions/node/v6.11.1/bin/npm" "run" "test"
19 error node v6.11.1
20 error npm v3.10.10
21 error code ELIFECYCLE
22 error 08-vanilla_rest_api@1.0.0 test: `mocha`
22 error Exit status 1
23 error Failed at the 08-vanilla_rest_api@1.0.0 test script 'mocha'.
23 error Make sure you have the latest version of node.js and npm installed.
23 error If you do, this is most likely a problem with the 08-vanilla_rest_api package,
23 error not with npm itself.
23 error Tell the author that this fails on your system:
23 error mocha
23 error You can get information on how to open an issue for this project with:
23 error npm bugs 08-vanilla_rest_api
23 error Or if that isn't available, you can get their info via:
23 error npm owner ls 08-vanilla_rest_api
23 error There is likely additional logging output above.
24 verbose exit [ 1, true ]
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "08-vanilla_rest_api",
"version": "1.0.0",
"description": "![cf](https://i.imgur.com/7v5ASc8.png) Lab 08: Vanilla REST API ======",
"main": "server.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "mocha",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ohjonah/08-vanilla_rest_api.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/ohjonah/08-vanilla_rest_api/issues"
},
"homepage": "https://github.com/ohjonah/08-vanilla_rest_api#readme",
"devDependencies": {
"cha": "^0.2.1",
"chai": "^4.1.0",
"mocha": "^3.4.2",
"superagent": "^3.5.2"
},
"dependencies": {
"bluebird": "^3.5.0",
"uuid": "^3.1.0"
}
}
52 changes: 52 additions & 0 deletions route/bake-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';

const storage = require('../lib/storage.js');
const response = require('../lib/response.js');
const Note = require('../model/bake.js');

module.exports = function(router) {
router.get('/api/bake', function(req, res) {
if (req.url.query.id) {
storage.fetchItem('bake', req.url.query.id)
.then( bake => {
response.sendJSON(res, 200, bake);
})
// error block
.catch( () => {
response.sendText(res, 404, 'not found');
});

return;
}
response.sendText(res, 400, 'bad request');
});

router.post('/api/bake', function(req, res) {
try {
var note = new Note(req.body.bakedGood, req.body.description, req.body.calories);

storage.createItem('bake', note);

response.sendJSON(res, 200, note);
} catch (err) {
response.sendText(res, 400, 'bad request');
}
});

router.delete('/api/bake', function(req, res) {
if (req.url.query.id) {
storage.deleteItem('bake', req.url.query.id)
.then( () => {
response.sendText(res, 204, 'deleted');
})
.catch( err => {
console.error(err);
response.sendText(res, 400, 'bad request');
});

return;
}

response.sendText(res, 400, 'bad request');
});
};
Loading