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/hike/09b66e38-156c-41a7-906a-05675fb638c7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"09b66e38-156c-41a7-906a-05675fb638c7","name":"some cool hike","distance":"3.4 miles","difficulty":"medium","description":"a cool hike"}
1 change: 1 addition & 0 deletions data/hike/09c55d68-ec12-4b57-90c1-3735ef74ac92.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"09c55d68-ec12-4b57-90c1-3735ef74ac92","name":"some cool hike","distance":"3.4 miles","difficulty":"medium","description":"a cool hike"}
1 change: 1 addition & 0 deletions data/hike/106404cb-9a5c-42cd-87d1-447dcb1198d0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"106404cb-9a5c-42cd-87d1-447dcb1198d0","name":"some cool hike","distance":"3.4 miles","difficulty":"medium","description":"a cool hike"}
1 change: 1 addition & 0 deletions data/hike/78b02d3c-99e5-450a-9a57-97df43d0f88a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"78b02d3c-99e5-450a-9a57-97df43d0f88a","name":"some cool hike","distance":"3.4 miles","difficulty":"medium","description":"a cool hike"}
1 change: 1 addition & 0 deletions data/hike/976954a0-a510-426c-b0f8-e6269bef045e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"976954a0-a510-426c-b0f8-e6269bef045e","name":"some cool hike","distance":"3.4 miles","difficulty":"medium","description":"a cool hike"}
31 changes: 31 additions & 0 deletions lib/parse-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'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) {
console.error(err);
reject(err);
}
});

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

return;
}

resolve();
});
};
10 changes: 10 additions & 0 deletions lib/parse-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'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);
};
21 changes: 21 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'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();
};
62 changes: 62 additions & 0 deletions lib/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'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;
}
console.error('route not found');

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 result');
res.end();
});
};
};
50 changes: 50 additions & 0 deletions lib/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'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 schemaName'));
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());
};


// didn't know if i was supposed to rafactor this, was short on time so didn't, but kept it around just in case...
// exports.deleteItem = function(schemaName, id){
// return new Promise((resolve, reject) => {
// if(!schemaName) return reject(new Error('expected schemaName'));
// if(!id) return reject(new Error('expected id'));
//
// var schema = storage[schemaName];
// if(!schema) return reject(new Error('schema not found'));
//
// var item = schema[id];
// if(!item) return reject(new Error('item not found'));
//
// delete storage[schemaName][id];
// resolve();
// });
// };
16 changes: 16 additions & 0 deletions model/hike.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

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

module.exports = function(name, distance, difficulty, description){
if(!name)throw new Error('expected name');
if(!distance)throw new Error('expected distance');
if(!difficulty)throw new Error('expected difficulty');
if(!description)throw new Error('expected description');

this.id = uuidv4();
this.name = name;
this.distance = distance;
this.difficulty = difficulty;
this.description = description;
};
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"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": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/codefellows-javascript-401d17/08-vanilla_rest_api.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/codefellows-javascript-401d17/08-vanilla_rest_api/issues"
},
"homepage": "https://github.com/codefellows-javascript-401d17/08-vanilla_rest_api#readme",
"dependencies": {
"bluebird": "^3.5.0",
"uuid": "^3.1.0"
},
"devDependencies": {
"chai": "^4.1.0",
"mocha": "^3.4.2",
"superagent": "^3.5.2"
}
}
33 changes: 33 additions & 0 deletions route/hike-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const storage = require('../lib/storage.js');
const response = require('../lib/response.js');
const Hike = require('../model/hike.js');

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

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

router.post('/api/hike', function(req, res){
try {
var hike = new Hike(req.body.name, req.body.distance, req.body.difficulty, req.body.description);
storage.createItem('hike', hike);
response.sendJSON(res, 200, hike);
} catch (err) {
console.error(err);
response.sendText(res, 400, 'bad request');
}
});
};
14 changes: 14 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

const http =require('http');
const Router = require('./lib/router.js');
const PORT = process.env.PORT || 3000;
const router = new Router();

require('./route/hike-route.js')(router);

const server = http.createServer(router.route());

server.listen(PORT, () => {
console.log('listening on PORT:', PORT);
});
Loading