Add basic structure

This commit is contained in:
Timothy Warren 2016-01-25 09:19:28 -05:00
parent f00de10dfb
commit 5fecd50e7c
25 changed files with 1076 additions and 0 deletions

35
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Created by https://www.gitignore.io/api/node,osx,webstorm,eclipse
### Node ###
# Logs
logs
*.log
@ -24,6 +27,7 @@ coverage
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# Optional npm cache directory
@ -31,3 +35,34 @@ node_modules
# Optional REPL history
.node_repl_history
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Don't commit generated docs
public/*

111
.jscsrc Normal file
View File

@ -0,0 +1,111 @@
{
"disallowEmptyBlocks": true,
"disallowKeywords": [
"with",
"eval"
],
"disallowKeywordsOnNewLine": [
"else"
],
"disallowMixedSpacesAndTabs": true,
"disallowMultipleLineBreaks": true,
"disallowMultipleLineStrings": true,
"disallowQuotedKeysInObjects": true,
"disallowSpaceAfterObjectKeys": true,
"disallowSpaceAfterPrefixUnaryOperators": false,
"disallowSpaceBeforeBinaryOperators": [
","
],
"disallowSpaceBeforeComma": {
"allExcept": [
"sparseArrays"
]
},
"disallowSpaceBeforePostfixUnaryOperators": [
"++",
"--"
],
"disallowSpaceBeforeSemicolon": true,
"disallowSpacesInCallExpression": true,
"disallowSpacesInFunctionDeclaration": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInNamedFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideBrackets": true,
"disallowSpacesInsideParentheses": {
"only": [
"!",
"{",
"}"
]
},
"disallowTrailingWhitespace": true,
"disallowYodaConditions": true,
"esnext": true,
"requireBlocksOnNewline": 1,
"requireCamelCaseOrUpperCaseIdentifiers": true,
"requireCapitalizedConstructors": true,
"requireCommaBeforeLineBreak": true,
"requireCurlyBraces": [
"if",
"else",
"for",
"while",
"do",
"try",
"catch"
],
"requireDotNotation": {
"allExcept": [
"keywords"
]
},
"requireLineFeedAtFileEnd": false,
"requirePaddingNewLinesAfterBlocks": true,
"requirePaddingNewLinesBeforeLineComments": {
"allExcept": "firstAfterCurly"
},
"requireParenthesesAroundIIFE": true,
"requireSemicolons": true,
"requireSpaceAfterBinaryOperators": true,
"requireSpaceAfterComma": true,
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"case",
"return",
"try",
"catch",
"typeof"
],
"requireSpaceBeforeBinaryOperators": true,
"requireSpaceBeforeBlockStatements": true,
"requireSpaceBetweenArguments": true,
"requireSpacesInAnonymousFunctionExpression": {
"beforeOpeningRoundBrace": true,
"allExcept": [
"shorthand"
]
},
"requireSpacesInConditionalExpression": true,
"requireSpacesInForStatement": true,
"requireSpacesInsideObjectBrackets": "all",
"requireTrailingComma": {
"ignoreSingleLine": true
},
"safeContextKeyword": "_this",
"validateIndentation": "\t",
"validateLineBreaks": "LF",
"validateQuoteMarks": {
"mark": "'",
"escape": true,
"ignoreJSX": true
}
}

219
gulpfile.js Normal file
View File

@ -0,0 +1,219 @@
'use strict';
const apidoc = require('gulp-apidoc'),
documentation = require('gulp-documentation'),
eslint = require('gulp-eslint'),
gulp = require('gulp'),
istanbul = require('gulp-istanbul'),
jscs = require('gulp-jscs'),
mocha = require('gulp-mocha'),
pipe = require('gulp-pipe'),
nsp = require('gulp-nsp');
/*
* Path(s) to all source files
*/
const SRC_FILES = ['lib/**/*.js'];
/*
* Path to unit test files
*/
const UNIT_TEST_FILES = ['test/unit/**/*_test.js'];
/*
* Path to integration test files
*/
const INTEGRATION_TEST_FILES = ['test/end-to-end/**/*_test.js'];
/*
* Path(s) to all test files
*/
const TEST_FILES = ['test/**/*_test.js'];
/*
* Configuration values for eslint
*/
const ESLINT_SETTINGS = {
env: {
node: true,
es6: true,
},
// Each rule has an error level (0-2), and some have extra parameters
// 0 turns a rule off
// 1 makes a rule give a warning
// 2 makes a rule fail linting
rules: {
'linebreak-style': [2, 'unix'], // Only unix line endings
'arrow-parens': [2, 'always'], // No parens on arrow functions with one param
'no-console': [1], // Avoid using console methods
'no-constant-condition': [1],
'no-extra-semi': [1], // Enliminate extra semicolons
'no-func-assign': [1],
'no-obj-calls': [2],
'no-unexpected-multiline': [2], // Catch syntax errors due to automatic semicolon insertion
'no-unneeded-ternary': [2], // Avoid redundant ternary expressions
radix: [2], // Require radix parameter on parseInt
'no-with': [2], // No use of with construct
'no-eval': [2], // No use of eval
'no-unreachable': [1], // Avoid code that is not reachable
'no-irregular-whitespace': [1], // Highlight whitespace that isn't a tab or space
'no-new-wrappers': [2], // Avoid using primitive constructors
'no-new-func': [2], // Avoid Function constructor eval
curly: [2, 'multi-line'], // Require braces for if,for,while,do contructs that are not on the same line
'no-implied-eval': [2], // Avoid camoflauged eval
'no-invalid-this': [2],
'constructor-super': [2],
'no-dupe-args': [2], // Disallow functions to have more than one parameter with the same name
'no-dupe-keys': [2], // Disaalow objects to have more than one property with the same name
'no-dupe-class-members': [2], // Disallow classes to have more than one method/memeber with the same name
'no-this-before-super': [2],
'prefer-arrow-callback': [1], // Prefer arrow functions for callbacks
'no-var': [2], // Use let or const instead of var
'valid-jsdoc': [1],
semi: [2, 'always'], // Require use of semicolons
strict: [2, 'global'], // have a global 'use strict' in every code file
'callback-return': [1], // return when invoking a callback
'object-shorthand': [1, 'methods'], // Prefer shorthand for functions in object literals/classes, but avoid property shorthand
'prefer-template': [1], // Prefer template strings eg. `Hello ${name}`, to string concatenation
},
};
/*
* Check syntax and style of test/miscellaneous files
*/
gulp.task('lint-tests', () => {
const LINT_TESTS_FILES = TEST_FILES.concat([
'gulpfile.js',
'server.js',
]);
// eslint
pipe(gulp.src(LINT_TESTS_FILES), [
eslint(ESLINT_SETTINGS),
eslint.format(),
eslint.failAfterError(),
]);
// JSCS rules are defined in /.jscsrc
pipe(gulp.src(['test/**/*.js', 'gulpfile.js']), [
jscs(),
jscs.reporter(),
]);
});
/*
* Check syntax and style of source files
*/
gulp.task('lint-src', () => {
// eslint
pipe(gulp.src(SRC_FILES), [
eslint(ESLINT_SETTINGS),
eslint.format(),
eslint.failAfterError(),
]);
// JSCS
// JSCS rules are defined in /.jscsrc
pipe(gulp.src(SRC_FILES), [
jscs(),
jscs.reporter(),
]);
});
/*
* Run all lint tasks
*/
gulp.task('lint', ['lint-src', 'lint-tests']);
/*
* Create internal method documentation of source files
*/
gulp.task('src-docs', () => {
pipe(gulp.src(SRC_FILES), [
documentation({
format: 'html',
}),
gulp.dest('public/docs'),
]);
});
/*
* Create api documentation from source files
*/
gulp.task('api-docs', (done) => {
apidoc({
src: 'lib/',
dest: 'public/api-docs/',
}, done);
});
/*
* Run all documentation generation tasks
*/
gulp.task('docs', ['src-docs', 'api-docs']);
/*
* Run all tests
*/
gulp.task('test', ['lint'], () => {
return pipe(gulp.src(TEST_FILES), [
mocha({
ui: 'tdd',
bail: true,
slow: 1000,
timeout: 5000,
}),
]);
});
/*
* Run hooks for istanbul coverage generation
*/
gulp.task('pre-coverage', () => {
return pipe(gulp.src(SRC_FILES), [
istanbul(),
istanbul.hookRequire(),
]);
});
/*
* Run unit tests and generate code coverage
*
* Does not run end-to-end tests
*/
gulp.task('coverage', ['lint', 'pre-coverage'], () => {
return pipe(gulp.src(UNIT_TEST_FILES), [
mocha({
ui: 'tdd',
bail: true,
slow: 1000,
timeout: 5000,
}),
istanbul.writeReports({
dir: 'public/coverage',
reporters:['lcov', 'lcovonly', 'html', 'text'],
}),
]);
});
/*
* Check dependencies for known security vulnerabilites
*/
gulp.task('audit', (cb) => {
nsp({
package: `${__dirname}/package.json`,
}, cb);
});
/*
* Run all tasks
*/
gulp.task('default', [
'audit',
'lint',
'docs',
'coverage',
]);
// End of gulpfile.js

96
lib/Container.js Normal file
View File

@ -0,0 +1,96 @@
'use strict';
const express = require('express'),
path = require('path');
/**
* Container for keeping track of dependencies
*/
class Container {
/**
* Create the object container
*
* @constructor
*/
constructor() {
const app = express();
this._container = {
app: app,
};
}
/**
* Determine if an item exists in the container
*
* @param {String} name - name of the item
* @return {Boolean} - whether the item exists in the container
*/
has(name) {
return (name in this._container);
}
/**
* Return an existing object instance
*
* @param {String} name - name of the item
* @return {Object|Null} - the item, or null if it doesn't exist
*/
get(name) {
if (! this.has(name)) {
return null;
}
return this._container[name];
}
/**
* Set an object in the container
*
* @param {String} name - name to associate with object
* @param {Object} object - item to keep track of
* @return {Container} - the container instance
*/
set(name, object) {
this._container[name] = object;
return this;
}
/**
* Does a native require, relative to the lib folder,
* and returns the value
*
* @param {String} modulePath - name of the module to require
* @return {mixed} - the value returned from require
*/
require(modulePath) {
// If the value is already saved, just return it
if (this.has(modulePath)) {
return this.get(modulePath);
}
let includeName = modulePath;
// Allow referencing local modules without using a ./
// eg. util/route-loader instead of ./util/route-loader
if (
modulePath.includes('/') &&
! (modulePath.startsWith('./') || modulePath.includes(__dirname))
) {
includeName = path.join(__dirname, modulePath);
}
return require(includeName);
}
}
let instance = null;
module.exports = (function () {
if (instance === null) {
instance = new Container();
}
return instance;
}());

23
lib/app.js Normal file
View File

@ -0,0 +1,23 @@
'use strict';
const container = require('./Container');
const loadRoutes = container.require('util/route-loader'),
path = container.require('path');
const app = container.get('app');
const middleware = container.require('config/middleware');
const routes = loadRoutes(path.join(__dirname, 'routes'));
const errorHandlers = container.require('config/error-handlers');
// load middleware
middleware.forEach((mw) => app.use(mw));
// automatically set up routing by folder structure
Object.keys(routes).reverse().forEach((path) => {
app.use(path, require(routes[path]));
});
// load error handlers
errorHandlers.forEach((handler) => app.use(handler));
module.exports = app;

10
lib/base/ApiModel.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
const container = require('../Container');
const Model = container.require('base/Model');
class ApiModel extends Model {
}
module.exports = ApiModel;

32
lib/base/Config.js Normal file
View File

@ -0,0 +1,32 @@
'use strict';
let nconf = require('nconf');
/**
* Config management class
*/
class Config {
constructor() {
// Use, in order of preference:
// - command line arguments
// - environment variables
// - config files
nconf.argv()
.env()
.file({
});
}
/**
* Retreive a config value
*
* @param {String} key - the name of the config value
* @return {mixed} - the configuration value
*/
get(key) {
}
}
module.exports = Config;

69
lib/base/HttpServer.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const http = require('http'),
logger = require('winston');
/**
* Class for creating an http server
*
* @param {Express} app - current express instance
* @param {Number} port - the port to listen on
*/
class HttpServer {
/**
* Creates an HTTP Server
*
* @constructor
* @param {Express} app - current express instance
* @param {Number} port - the port to listen on
*/
constructor(app, port) {
let server = http.createServer(app);
server.listen(port);
server.on('error', this.onError);
server.on('listening', () => {
let addr = server.address();
let bind = typeof addr === 'string'
? `pipe ${addr}`
: `port ${addr.port}`;
logger.info(`Listening on ${bind}`);
});
this.server = server;
}
/**
* Event listener for HTTP server "error" event.
*
* @param {Error} error - the error object
* @return {Null} - Does not return a value
* @throws {Error}
*/
onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
let port = this.server.address().port;
let bind = typeof port === 'string'
? `Pipe ${port}`
: `Port ${port}`;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
logger.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
logger.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
}
}
module.exports = HttpServer;

7
lib/base/Model.js Normal file
View File

@ -0,0 +1,7 @@
'use strict';
class Model {
}
module.exports = Model;

View File

@ -0,0 +1,37 @@
'use strict';
// -----------------------------------------------------------------------------
// Error handlers
// -----------------------------------------------------------------------------
const container = require('../Container');
const app = container.get('app');
let errorHandlers = [
// catch 404 and forward to error handler
(req, res, next) => {
let err = new Error('Not Found');
err.status = 404;
next(err);
},
// general error handler
(err, req, res, next) => {
res.status(err.status || 500);
let output = {
status: err.status,
message: err.message,
};
// Show stack trace in development environment
if (app.get('env') === 'development') {
output.error = err;
}
res.json(output);
},
];
module.exports = errorHandlers;

31
lib/config/middleware.js Normal file
View File

@ -0,0 +1,31 @@
'use strict';
// -----------------------------------------------------------------------------
// Middleware
// -----------------------------------------------------------------------------
const bodyParser = require('body-parser'),
cookieParser = require('cookie-parser'),
express = require('express'),
helmet = require('helmet'),
requestLogger = require('morgan'),
path = require('path');
let middleware = [
// some security settings controlled by helmet
helmet.frameguard(),
helmet.hidePoweredBy(),
helmet.ieNoOpen(),
helmet.noSniff(),
helmet.xssFilter(),
// basic express middleware
requestLogger('combined'),
bodyParser.json(),
bodyParser.urlencoded({ extended: false }),
cookieParser(),
express.static(path.join(__dirname, '../../public')),
];
module.exports = middleware;

16
lib/routes/index.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
const express = require('express');
let router = express.Router();
/* GET home page. */
router.get('/', (req, res, next) => {
res.json({
status: 200,
data: {
index: { title: 'Express' },
},
});
});
module.exports = router;

11
lib/routes/users.js Normal file
View File

@ -0,0 +1,11 @@
'use strict';
const express = require('express');
let router = express.Router();
/* GET users listing. */
router.get('/', (req, res, next) => {
res.send('respond with a resource');
});
module.exports = router;

29
lib/util/route-loader.js Normal file
View File

@ -0,0 +1,29 @@
'use strict';
const glob = require('glob');
/**
* Map Routes to route files
*
* @param {String} path - folder with Routes
* @return {Object} - Object mapping routes to their files
*/
module.exports = function routeLoader(path) {
const basePath = path;
let paths = glob.sync(`${path}/**/*.js`);
paths = paths.sort();
let routes = {};
paths.forEach((path) => {
let routePath = path.replace(basePath, '')
.replace('.js', '')
.replace('index', '');
routes[routePath] = path;
});
return routes;
};

63
package.json Normal file
View File

@ -0,0 +1,63 @@
{
"author": "Timothy J. Warren",
"dependencies": {
"bluebird": "^3.1.1",
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
"debug": "~2.2.0",
"express": "4.*",
"glob": "^6.0.4",
"helmet": "^1.1.0",
"morgan": "~1.6.1",
"nconf": "^0.8.2",
"node-dev": "^2.7.1",
"winston": "^2.1.1"
},
"description": "An Opinionated Take on express with use of ES6 features",
"devDependencies": {
"chai": "3.4.*",
"eslint": "1.10.*",
"gulp": "3.9.*",
"gulp-apidoc": "0.2.*",
"gulp-documentation": "2.1.*",
"gulp-eslint": "1.1.*",
"gulp-istanbul": "^0.10.3",
"gulp-jscs": "3.0.*",
"gulp-mocha": "2.2.*",
"gulp-nsp": "^2.3.0",
"gulp-pipe": "1.0.*",
"istanbul": "0.4.*",
"mocha": "2.3.*",
"pre-commit": "^1.1.2",
"supertest": "^1.1.0"
},
"directories": {
"lib": "./lib"
},
"engines": {
"node": ">4.0.0"
},
"files": [
"lib/",
"server.js"
],
"license": "MIT",
"main": "./server.js",
"name": "crispy-train",
"pre-commit": {
"silent": false,
"run": [
"gulp"
]
},
"repository": {
"type": "git",
"url": "https://github.com/timw4mail/crispy-train.git"
},
"scripts": {
"start": "node-dev server.js",
"gulp": "gulp default",
"test": "gulp test"
},
"version": "0.0.1"
}

37
server.js Normal file
View File

@ -0,0 +1,37 @@
'use strict';
const container = require('./lib/Container');
const app = container.require('./app'),
logger = require('winston'),
HttpServer = container.require('base/HttpServer');
/**
* Normalize a port into a number, string, or false.
*
* @private
* @param {mixed} val - port value
* @return {mixed} - normalized value
*/
function normalizePort(val) {
let port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
// Get port from environment and store in Express.
let port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
// Create HTTP Server
let server = new HttpServer(app, port);

View File

@ -0,0 +1,51 @@
'use strict';
const supertest = require('supertest'),
expect = require('chai').expect;
let server = supertest.agent('http://localhost:3000');
suite('Example tests', () => {
test('Index page works as expected', (done) => {
server.get('/')
.expect('Content-type', /json/)
.expect(200)
.end((err, res) => {
expect(res.status).to.equal(200);
expect(res.body.error).to.be.undefined;
expect(res.body).to.deep.equal({
status: 200,
data: {
index: { title: 'Express' },
},
});
done();
});
});
test('404 page works as expected', (done) => {
server.get('/foo')
.expect('Content-type', /json/)
.expect(404)
.end((err, res) => {
expect(res.status).to.equal(404);
expect(res.body.status).to.equal(res.status);
expect(res.body.message).to.be.ok;
let expected = {
status: 404,
message: 'Not Found',
};
if (process.env.NODE_ENV === 'development') {
expected.error = {
status: 404,
};
}
expect(res.body).to.deep.equal(expected);
done();
});
});
});

5
test/mocha.opts Normal file
View File

@ -0,0 +1,5 @@
test/**/*.js
--ui tdd
--reporter spec
--slow 1000
--timeout 5000

60
test/test-base.js Normal file
View File

@ -0,0 +1,60 @@
'use strict';
const path = require('path');
/**
* Base Object for unit test utilities
*/
const testBase = {
/**
* Chai expect assertion library
*/
expect: require('chai').expect,
/**
* Determine the appropriate path to a module relative to the root folder
*
* @param {String} modulePath - the raw path to the module
* @return {String} - the normalized path to the module
*/
_normalizeIncludePath(modulePath) {
const basePath = path.resolve(path.join(__dirname, '../'));
let includePath = modulePath;
// Allow referencing local modules without using a ./
// eg. util/route-loader instead of ./util/route-loader
if (modulePath.includes('/') && ! modulePath.startsWith('./')) {
includePath = path.join(basePath, modulePath);
}
return includePath;
},
/**
* Load a module relative to the root folder
*
* @param {String} modulePath - path to the module, relative to the tests/ folder
* @return {mixed} - whatever the module returns
*/
testRequire(modulePath) {
const includePath = testBase._normalizeIncludePath(modulePath);
return require(includePath);
},
/**
* Load a module relative to the root folder, but first delete
* the module from the require cache
*
* @param {String} modulePath - path to the module, relative to the tests/ folder
* @return {mixed} - whatever the module returns
*/
testRequireNoCache(modulePath) {
const includePath = testBase._normalizeIncludePath(modulePath);
delete require.cache[includePath];
return require(includePath);
},
};
module.exports = testBase;

0
test/unit/.gitkeep Normal file
View File

View File

@ -0,0 +1,77 @@
'use strict';
const testBase = require('../../test-base');
const expect = testBase.expect;
suite('Dependency Container tests', () => {
let container = null;
setup(() => {
// Delete cached version of container class that may have been required
// at an earlier time. The container module has a saved state to keep track
// of modules during normal use. Removing the cached version ensures that
// a new instance of the module is used.
container = testBase.testRequireNoCache('lib/Container');
});
test('Multiple requires return the same instance', () => {
let container2 = testBase.testRequire('lib/Container');
expect(container2).to.be.equal(container);
});
suite('has method', () => {
setup(() => {
container.set('foobar', {
foo: {
bar: [1, 2, 3],
},
});
});
test('Item "foobar" exists', () => {
expect(container.has('foobar')).to.be.true;
});
test('Item "abc" does not exist', () => {
expect(container.has('abc')).to.be.false;
});
});
suite('Get/set functionality', () => {
let obj = {
foo: {
bar: [1, 2, 3],
},
};
test('Set method returns Container', () => {
let actual = container.set('foobar', obj);
expect(actual).to.be.equal(container);
});
test('Get method returns set object', () => {
let actual = container.get('foobar');
expect(actual).to.be.equal(obj);
});
test('Attempt to get non-existent item returns null', () => {
expect(container.get('aseiutj')).to.be.null;
});
});
suite('require method', () => {
test('Returns same object as testInclude', () => {
let containerFile = container.require('app');
let testFile = testBase.testRequire('lib/app');
expect(containerFile).to.be.equal(testFile);
});
test('Returns same object as native require', () => {
let containerFile = container.require('express');
let nativeRequire = require('express');
expect(containerFile).to.be.equal(nativeRequire);
});
});
});

View File

@ -0,0 +1,18 @@
'use strict';
const expect = require('chai').expect,
path = require('path');
let routeLoader = require(path.join(__dirname, '/../../../lib/util/route-loader'));
suite('Util - Route Loader', () => {
test('routeLoader creates accurate route mapping', () => {
let actual = routeLoader(path.join(__dirname, 'test-routes'));
let expected = {
'/api/foo/bar': path.join(__dirname, 'test-routes/api/foo/bar.js'),
'/api/foo': path.join(__dirname, 'test-routes/api/foo.js'),
'/': path.join(__dirname, 'test-routes/index.js'),
};
expect(expected).to.be.deep.equal(actual);
});
});

View File

@ -0,0 +1,13 @@
'use strict';
const express = require('express');
let router = express.Router();
/* GET home page. */
router.get('/', (req, res, next) => {
res.json({
index: { title: 'Express' },
});
});
module.exports = router;

View File

@ -0,0 +1,13 @@
'use strict';
const express = require('express');
let router = express.Router();
/* GET home page. */
router.get('/', (req, res, next) => {
res.json({
index: { title: 'Express' },
});
});
module.exports = router;

View File

@ -0,0 +1,13 @@
'use strict';
const express = require('express');
let router = express.Router();
/* GET home page. */
router.get('/', (req, res, next) => {
res.json({
index: { title: 'Express' },
});
});
module.exports = router;