Repository: WP-API/node-wpapi
Pull request #406 (2018-12-20 00:26:34)

Clean up and modernize codebase
It feels premature to migrate to a typed system, but given the prevalence of modern Node environments there's some other improvements that will help improve the maintainability of the codebase, and we have sufficient unit testing to refactor with confidence.

Contained changes:

- Use `let` and `const` over `var`
- Use arrow functions where appropriate, to better distinguish functions requiring `this` access
- Use spread operator instead of `Object.assign` and `node.extend`
- Drop support for node v6 (**Breaking**)
- Update routes configuration for WP v5.0.2
- Upgrade Webpack to v4 (remove json-loader, add babel-loader)


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index bd44976..fe28b0d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -34,6 +34,7 @@ module.exports = {
 		'no-multiple-empty-lines': [ 'error', {
 			'max': 1,
 		} ],
+		'no-var': [ 'error' ],
 		'object-curly-newline': [ 'error', {
 			'ObjectExpression': {
 				'consistent': true,
@@ -56,6 +57,8 @@ module.exports = {
 		} ],
 		'object-curly-spacing': [ 'error', 'always' ],
 		'object-property-newline': [ 'error' ],
+		'prefer-arrow-callback': [ 'error' ],
+		'prefer-const': [ 'error' ],
 		'quotes': [ 'error', 'single' ],
 		'semi': [ 'error', 'always' ],
 		'semi-spacing': [ 'error', {
diff --git a/.travis.yml b/.travis.yml
index 0e3eff7..eec9a38 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,5 @@ node_js:
   - "lts/*"
   - "10"
   - "8"
-  - "6"
 # Opt-in to travis container infrastructure
 sudo: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 04514bd..ba4dedb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -73,39 +73,9 @@ We rebase feature branches onto master when merging in order to maintain a linea
 
 ## Code Syntax & Style
 
-We use [JSCS](https://www.npmjs.org/package/jscs) to enforce a basic set of code style guidelines, and [JSHint](http://jshint.com/) to guard against syntax errors. To run them both execute `npm run lint`; they will also be run every time you execute `npm test`.
+We use [ESLint](https://eslint.org/) to enforce a basic set of code style guidelines and syntax warnings. To run ESLint use the command `npm run lint`; this command will also be run every time you execute `npm test`.
 
-JSCS is a useful tool for enforcing a code style, but isn't flexible enough to cover all guidelines. Note our standard for spacing within function parentheses, which is not enforced mechanically but will be evaluated manually when reviewing pull requests:
-
-```javascript
-// Function params and args should be spaced out from the parentheses:
-someMethodCall( param1, param2 );
-function newFunction( arg1, arg2 ) {};
-```
-
-"When in doubt, space it out," with the following exceptions.
-
-```javascript
-// The space can be omitted when passing function expressions, object literals
-// or array literals as arguments:
-someMethodCall(function() {
-    // do stuff
-});
-someOtherMethod({
-    object: 'no space before an object literal'
-}, 'but this String argument still has a space after it' );
-someMethodThatTakesAnArray([
-    'no',
-    'leading or trailing',
-    'spaces needed'
-]);
-```
-
-We prefer `camelCase` variable and function names, and `UpperCamelCase` constructors. When using the `underscore_case` parameter names that are required by the WordPress API, the following JSHint directive can be used to disable the case enforcement for that particular file:
-
-```javascript
-/*jshint -W106 */// Disable underscore_case warnings in this file
-```
+We prefer `camelCase` variable and function names, and `UpperCamelCase` constructors. `underscore_case` parameter names may be necessary when working with values returned from or intended to be sent to the WordPress REST API.
 
 ## Documentation
 
diff --git a/README.md b/README.md
index 7d4d796..349408c 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,9 @@ To get started, `npm install wpapi` or [download the browser build](https://wp-a
 
 ## Installation
 
-`node-wpapi` works both on the server or in the browser. Node.js version 4.0 or higher is required.
+`node-wpapi` works both on the server or in the browser. Node.js version 8 or higher is required, and the latest LTS release is recommended.
+
+In the browser `node-wpapi` officially supports the latest two versions of all evergreen browsers, and Internet Explorer 11.
 
 ### Install with NPM
 
diff --git a/build/grunt/generate-docs.js b/build/grunt/generate-docs.js
index 2cc34dd..87cf287 100644
--- a/build/grunt/generate-docs.js
+++ b/build/grunt/generate-docs.js
@@ -6,12 +6,12 @@ module.exports = function( grunt ) {
 		'can be rendered with Jekyll.',
 	].join( ' ' ), function() {
 		// Force task into async mode and grab a handle to the "done" function.
-		var done = this.async();
+		const done = this.async();
 
 		grunt.log.writeln( 'Extracting page content from README.md...' );
 
 		// Kick off generation
-		require( '../scripts/generate-docs-markdown' ).then( function() {
+		require( '../scripts/generate-docs-markdown' ).then( () => {
 			grunt.log.writeln( 'Pages generated successfully' );
 			done();
 		} );
diff --git a/build/scripts/generate-docs-markdown.js b/build/scripts/generate-docs-markdown.js
index dfe9ff1..8dfa808 100644
--- a/build/scripts/generate-docs-markdown.js
+++ b/build/scripts/generate-docs-markdown.js
@@ -125,7 +125,7 @@ const readmeOutput = readFile( readmePath ).then( ( contents ) => {
 	let entry = null;
 
 	for ( let i = 0; i < tokens.length; i++ ) {
-		let token = tokens[ i ];
+		const token = tokens[ i ];
 
 		if ( ! isTitle( token ) ) {
 			if ( entry && entry.tokens ) {
diff --git a/build/scripts/simplify-object.js b/build/scripts/simplify-object.js
index d5bd292..d9a99a5 100644
--- a/build/scripts/simplify-object.js
+++ b/build/scripts/simplify-object.js
@@ -3,7 +3,7 @@
  */
 'use strict';
 
-var objectReduce = require( '../../lib/util/object-reduce' );
+const objectReduce = require( '../../lib/util/object-reduce' );
 
 /**
  * Walk through the keys and values of a provided object, removing any properties
@@ -17,12 +17,20 @@ var objectReduce = require( '../../lib/util/object-reduce' );
  * @returns {*} The passed-in value, with non-essential args properties and all
  * _links properties removes.
  */
-function simplifyObject( obj ) {
+const simplifyObject = ( obj ) => {
 	// Pass through falsy values, Dates and RegExp values without modification
 	if ( ! obj || obj instanceof Date || obj instanceof RegExp ) {
 		return obj;
 	}
 
+	if ( obj.methods && obj.args ) {
+		// If the key is an object with "methods" and "args" properties, only
+		// include the full "args" object if "methods" contains GET.
+		if ( ! obj.methods.map( str => str.toLowerCase() ).includes( 'get' ) ) {
+			obj.args = {};
+		}
+	}
+
 	// Map arrays through simplifyObject
 	if ( Array.isArray( obj ) ) {
 		return obj.map( simplifyObject );
@@ -30,29 +38,36 @@ function simplifyObject( obj ) {
 
 	// Reduce through objects to run each property through simplifyObject
 	if ( typeof obj === 'object' ) {
-		return objectReduce( obj, function( newObj, val, key ) {
-			// Omit _links objects entirely
-			if ( key === '_links' ) {
+		return objectReduce(
+			obj,
+			( newObj, val, key ) => {
+				// Omit _links objects entirely
+				if ( key === '_links' ) {
+					return newObj;
+				}
+
+				// If the key is "args", omit all keys of second-level descendants
+				if ( key === 'args' ) {
+					newObj.args = objectReduce(
+						val,
+						( slimArgs, argVal, argKey ) => {
+							slimArgs[ argKey ] = {};
+							return slimArgs;
+						},
+						{}
+					);
+				} else {
+					// Pass all other objects through simplifyObject
+					newObj[ key ] = simplifyObject( obj[ key ] );
+				}
 				return newObj;
-			}
-
-			// If the key is "args", omit all keys of second-level descendants
-			// other than "required"
-			if ( key === 'args' ) {
-				newObj.args = objectReduce( val, function( slimArgs, argVal, argKey ) {
-					slimArgs[ argKey ] = {};
-					return slimArgs;
-				}, {} );
-			} else {
-				// Pass all other objects through simplifyObject
-				newObj[ key ] = simplifyObject( obj[ key ] );
-			}
-			return newObj;
-		}, {} );
+			},
+			{}
+		);
 	}
 
 	// All other types pass through without modification
 	return obj;
-}
+};
 
 module.exports = simplifyObject;
diff --git a/build/scripts/update-default-routes-json.js b/build/scripts/update-default-routes-json.js
index 34652e6..1cbcd47 100755
--- a/build/scripts/update-default-routes-json.js
+++ b/build/scripts/update-default-routes-json.js
@@ -62,13 +62,13 @@
  */
 'use strict';
 
-var agent = require( 'superagent' );
-var fs = require( 'fs' );
-var path = require( 'path' );
-var simplifyObject = require( './simplify-object' );
+const agent = require( 'superagent' );
+const fs = require( 'fs' );
+const path = require( 'path' );
+const simplifyObject = require( './simplify-object' );
 
 // Parse the arguments object
-var argv = require( 'minimist' )( process.argv.slice( 2 ) );
+const argv = require( 'minimist' )( process.argv.slice( 2 ) );
 
 if ( argv.h || argv.help ) {
 	console.log( `
@@ -90,25 +90,25 @@ update-default-routes-json \\
 // The output directory defaults to the lib/data directory. To customize it,
 // specify your own directory with --output=your/output/directory (supports
 // both relative and absolute paths)
-var outputPath = argv.output ?
+const outputPath = argv.output ?
 	// Nested ternary, don't try this at home: this is to support absolute paths
 	argv.output[ 0 ] === '/' ? argv.output : path.join( process.cwd(), argv.output ) :
 	// Output to lib/data/ by default
 	path.resolve( process.cwd(), 'lib', 'data' );
 
 // Specify your own API endpoint with --endpoint=http://your-endpoint.com/wp-json
-var endpoint = argv.endpoint || 'http://wpapi.local/wp-json';
+const endpoint = argv.endpoint || 'http://wpapi.local/wp-json';
 
 // Specify a custom output file name with --file=custom-api-routes-filename.json
-var fileName = argv.file || 'default-routes.json';
+const fileName = argv.file || 'default-routes.json';
 
 // This directory will be called to kick off the JSON download: it uses
 // superagent internally for HTTP transport that respects HTTP redirects.
-function getJSON( cbFn ) {
+const getJSON = ( cbFn ) => {
 	agent
 		.get( endpoint )
 		.set( 'Accept', 'application/json' )
-		.end( function( err, res ) {
+		.end( ( err, res ) => {
 			// Inspect the error and then the response to infer various error states
 			if ( err ) {
 				console.error( '\nSomething went wrong! Could not download endpoint JSON.' );
@@ -129,7 +129,7 @@ function getJSON( cbFn ) {
 
 			cbFn( res );
 		} );
-}
+};
 
 // The only assumption we want to make about the URL is that it should be a web
 // URL of _some_ sort, which generally means it has "http" in it somewhere. We
@@ -142,7 +142,7 @@ if ( ! /http/i.test( endpoint ) ) {
 	process.exit( 1 );
 }
 
-fs.stat( outputPath, function( err, stats ) {
+fs.stat( outputPath, ( err, stats ) => {
 	if ( err || ! stats.isDirectory() ) {
 		console.error( '\nError: ' + outputPath );
 		console.error( 'This is not a valid directory. Please double-check the path and try again.' );
@@ -150,15 +150,15 @@ fs.stat( outputPath, function( err, stats ) {
 	}
 
 	// If we made it this far, our arguments look good! Carry on.
-	getJSON( function( response ) {
+	getJSON( ( response ) => {
 		// Extract the JSON
-		var endpointJSON = JSON.parse( JSON.stringify( response.body ) );
+		const endpointJSON = JSON.parse( JSON.stringify( response.body ) );
 		// Simplify the JSON structure and pick out the routes dictionary
-		var slimJSON = simplifyObject( endpointJSON ).routes;
+		const slimJSON = simplifyObject( endpointJSON ).routes;
 
 		// Save the file
-		var outputFilePath = path.join( outputPath, fileName );
-		fs.writeFile( outputFilePath, JSON.stringify( slimJSON ), function( err ) {
+		const outputFilePath = path.join( outputPath, fileName );
+		fs.writeFile( outputFilePath, JSON.stringify( slimJSON ), ( err ) => {
 			if ( err ) {
 				console.error( '\nSomething went wrong! Could not save ' + outputFilePath );
 				return process.exit( 1 );
diff --git a/lib/autodiscovery.js b/lib/autodiscovery.js
index f7689ec..c45c091 100644
--- a/lib/autodiscovery.js
+++ b/lib/autodiscovery.js
@@ -6,7 +6,7 @@
  */
 'use strict';
 
-var parseLinkHeader = require( 'parse-link-header' );
+const parseLinkHeader = require( 'parse-link-header' );
 
 /**
  * Attempt to locate a `rel="https://api.w.org"` link relation header
@@ -17,12 +17,12 @@ var parseLinkHeader = require( 'parse-link-header' );
  */
 function locateAPIRootHeader( response ) {
 	// Define the expected link rel value per http://v2.wp-api.org/guide/discovery/
-	var rel = 'https://api.w.org/';
+	const rel = 'https://api.w.org/';
 
 	// Extract & parse the response link headers
-	var link = response.link || ( response.headers && response.headers.link );
-	var headers = parseLinkHeader( link );
-	var apiHeader = headers && headers[ rel ];
+	const link = response.link || ( response.headers && response.headers.link );
+	const headers = parseLinkHeader( link );
+	const apiHeader = headers && headers[ rel ];
 
 	if ( apiHeader && apiHeader.url ) {
 		return apiHeader.url;
diff --git a/lib/constructors/wp-request.js b/lib/constructors/wp-request.js
index 6ce0e21..61d75eb 100644
--- a/lib/constructors/wp-request.js
+++ b/lib/constructors/wp-request.js
@@ -1,13 +1,12 @@
 'use strict';
 
-var qs = require( 'qs' );
-var _unique = require( 'lodash.uniq' );
-var extend = require( 'node.extend' );
+const qs = require( 'qs' );
+const _unique = require( 'lodash.uniq' );
 
-var alphaNumericSort = require( '../util/alphanumeric-sort' );
-var keyValToObj = require( '../util/key-val-to-obj' );
-var paramSetter = require( '../util/parameter-setter' );
-var objectReduce = require( '../util/object-reduce' );
+const alphaNumericSort = require( '../util/alphanumeric-sort' );
+const keyValToObj = require( '../util/key-val-to-obj' );
+const paramSetter = require( '../util/parameter-setter' );
+const objectReduce = require( '../util/object-reduce' );
 
 /**
  * WPRequest is the base API request object constructor
@@ -37,7 +36,7 @@ function WPRequest( options ) {
 		'username',
 		'password',
 		'nonce',
-	].reduce( function( localOptions, key ) {
+	].reduce( ( localOptions, key ) => {
 		if ( options && options[ key ] ) {
 			localOptions[ key ] = options[ key ];
 		}
@@ -94,9 +93,7 @@ function WPRequest( options ) {
  * Identity function for use within invokeAndPromisify()
  * @private
  */
-function identity( value ) {
-	return value;
-}
+const identity = value => value;
 
 /**
  * Process arrays of taxonomy terms into query parameters.
@@ -125,15 +122,19 @@ function prepareTaxonomies( taxonomyFilters ) {
 		return {};
 	}
 
-	return objectReduce( taxonomyFilters, function( result, terms, key ) {
-		// Trim whitespace and concatenate multiple terms with +
-		result[ key ] = terms.map( function( term ) {
-			// Coerce term into a string so that trim() won't fail
-			return ( term + '' ).trim().toLowerCase();
-		} ).join( '+' );
-
-		return result;
-	}, {} );
+	return objectReduce(
+		taxonomyFilters,
+		( result, terms, key ) => {
+			// Trim whitespace and concatenate multiple terms with +
+			result[ key ] = terms
+				// Coerce term into a string so that trim() won't fail
+				.map( term => ( term + '' ).trim().toLowerCase() )
+				.join( '+' );
+
+			return result;
+		},
+		{}
+	);
 }
 
 /**
@@ -152,17 +153,21 @@ function prepareTaxonomies( taxonomyFilters ) {
  * @param {Object} obj An object of key/value pairs
  * @returns {Object} That object with all empty values removed
  */
-function populated( obj ) {
+const populated = ( obj ) => {
 	if ( ! obj ) {
 		return obj;
 	}
-	return objectReduce( obj, function( values, val, key ) {
-		if ( val !== undefined && val !== null && val !== '' ) {
-			values[ key ] = val;
-		}
-		return values;
-	}, {} );
-}
+	return objectReduce(
+		obj,
+		( values, val, key ) => {
+			if ( val !== undefined && val !== null && val !== '' ) {
+				values[ key ] = val;
+			}
+			return values;
+		},
+		{}
+	);
+};
 
 /**
  * Assert whether a provided URL component is "valid" by checking it against
@@ -176,11 +181,11 @@ function populated( obj ) {
  * @returns {boolean} Whether the provided input matches any of the provided
  * level validation functions
  */
-function validatePathLevel( levelDefinitions, levelContents ) {
+const validatePathLevel = ( levelDefinitions, levelContents ) => {
 	// One "level" may have multiple options, as a route tree is a branching
 	// structure. We consider a level "valid" if the provided levelContents
 	// match any of the available validators.
-	var valid = levelDefinitions.reduce( function( anyOptionValid, levelOption ) {
+	const valid = levelDefinitions.reduce( ( anyOptionValid, levelOption ) => {
 		if ( ! levelOption.validate ) {
 			// If there is no validator function, the level is implicitly valid
 			return true;
@@ -194,12 +199,13 @@ function validatePathLevel( levelDefinitions, levelContents ) {
 			levelContents,
 			// awkward pluralization support:
 			'does not match' + ( levelDefinitions.length > 1 ? ' any of' : '' ),
-			levelDefinitions.reduce( function( components, levelOption ) {
-				return components.concat( levelOption.component );
-			}, [] ).join( ', ' ),
+			levelDefinitions.reduce(
+				( components, levelOption ) => components.concat( levelOption.component ),
+				[]
+			).join( ', ' ),
 		].join( ' ' ) );
 	}
-}
+};
 
 // (Semi-)Private Prototype Methods
 // ================================
@@ -220,22 +226,27 @@ function validatePathLevel( levelDefinitions, levelContents ) {
  */
 WPRequest.prototype._renderQuery = function() {
 	// Build the full query parameters object
-	var queryParams = extend( {}, populated( this._params ) );
+	const queryParams = {
+		...populated( this._params ),
+	};
 
 	// Prepare any taxonomies and merge with other filter values
-	var taxonomies = prepareTaxonomies( this._taxonomyFilters );
-	queryParams.filter = extend( {}, populated( this._filters ), taxonomies );
+	const taxonomies = prepareTaxonomies( this._taxonomyFilters );
+	queryParams.filter = {
+		...populated( this._filters ),
+		...taxonomies,
+	};
 
 	// Parse query parameters object into a query string, sorting the object
 	// properties by alphabetical order (consistent property ordering can make
 	// for easier caching of request URIs)
-	var queryString = qs.stringify( queryParams, { arrayFormat: 'brackets' } )
+	const queryString = qs.stringify( queryParams, { arrayFormat: 'brackets' } )
 		.split( '&' )
 		.sort()
 		.join( '&' );
 
 	// Check if the endpoint contains a previous query and set the query character accordingly.
-	var queryCharacter = /\?/.test( this._options.endpoint ) ? '&' : '?';
+	const queryCharacter = /\?/.test( this._options.endpoint ) ? '&' : '?';
 
 	// Prepend a "?" (or a "&") if a query is present, and return.
 	return ( queryString === '' ) ? '' : queryCharacter + queryString;
@@ -252,20 +263,18 @@ WPRequest.prototype._renderPath = function() {
 	// an error will be thrown
 	this.validatePath();
 
-	var pathParts = this._path;
-	var orderedPathParts = Object.keys( pathParts )
-		.sort( function( a, b ) {
-			var intA = parseInt( a, 10 );
-			var intB = parseInt( b, 10 );
+	const pathParts = this._path;
+	const orderedPathParts = Object.keys( pathParts )
+		.sort( ( a, b ) => {
+			const intA = parseInt( a, 10 );
+			const intB = parseInt( b, 10 );
 			return intA - intB;
 		} )
-		.map( function( pathPartKey ) {
-			return pathParts[ pathPartKey ];
-		} );
+		.map( pathPartKey => pathParts[ pathPartKey ] );
 
 	// Combine all parts of the path together, filtered to omit any components
 	// that are unspecified or empty strings, to create the full path template
-	var path = [
+	const path = [
 		this._namespace,
 	].concat( orderedPathParts ).filter( identity ).join( '/' );
 
@@ -283,10 +292,10 @@ WPRequest.prototype._renderPath = function() {
  */
 WPRequest.prototype.toString = function() {
 	// Render the path to a string
-	var path = this._renderPath();
+	const path = this._renderPath();
 
 	// Render the query string
-	var queryStr = this._renderQuery();
+	const queryStr = this._renderQuery();
 
 	return this._options.endpoint + path + queryStr;
 };
@@ -329,21 +338,17 @@ WPRequest.prototype.setPathPart = function( level, val ) {
  */
 WPRequest.prototype.validatePath = function() {
 	// Iterate through all _specified_ levels of this endpoint
-	var specifiedLevels = Object.keys( this._path )
-		.map( function( level ) {
-			return parseInt( level, 10 );
-		} )
-		.filter( function( pathPartKey ) {
-			return ! isNaN( pathPartKey );
-		} );
+	const specifiedLevels = Object.keys( this._path )
+		.map( level => parseInt( level, 10 ) )
+		.filter( pathPartKey => ! isNaN( pathPartKey ) );
 
-	var maxLevel = Math.max.apply( null, specifiedLevels );
+	const maxLevel = Math.max.apply( null, specifiedLevels );
 
 	// Ensure that all necessary levels are specified
-	var path = [];
-	var valid = true;
+	const path = [];
+	let valid = true;
 
-	for ( var level = 0; level <= maxLevel; level++ ) {
+	for ( let level = 0; level <= maxLevel; level++ ) {
 
 		if ( ! this._levels || ! this._levels[ level ] ) {
 			continue;
@@ -391,8 +396,8 @@ WPRequest.prototype.param = function( props, value ) {
 	}
 
 	// Iterate through the properties
-	Object.keys( props ).forEach( function( key ) {
-		var value = props[ key ];
+	Object.keys( props ).forEach( ( key ) => {
+		let value = props[ key ];
 
 		// Arrays should be de-duped and sorted
 		if ( Array.isArray( value ) ) {
@@ -401,7 +406,7 @@ WPRequest.prototype.param = function( props, value ) {
 
 		// Set the value
 		this._params[ key ] = value;
-	}.bind( this ) );
+	} );
 
 	return this;
 };
@@ -682,7 +687,10 @@ WPRequest.prototype.setHeaders = function( headers, value ) {
 		headers = keyValToObj( headers, value );
 	}
 
-	this._options.headers = Object.assign( {}, this._options.headers || {}, headers );
+	this._options.headers = {
+		...( this._options.headers || {} ),
+		...headers,
+	};
 
 	return this;
 };
diff --git a/lib/data/default-routes.json b/lib/data/default-routes.json
index f7a3c0c..f40b371 100644
--- a/lib/data/default-routes.json
+++ b/lib/data/default-routes.json
@@ -1,1 +1,1 @@
-{"/":{"namespace":"","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/oembed/1.0":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"namespace":{},"context":{}}}]},"/oembed/1.0/embed":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"url":{},"format":{},"maxwidth":{}}}]},"/wp/v2":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"namespace":{},"context":{}}}]},"/wp/v2/posts":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"slug":{},"status":{},"categories":{},"categories_exclude":{},"tags":{},"tags_exclude":{},"sticky":{}}},{"methods":["POST"],"args":{"date":{},"date_gmt":{},"slug":{},"status":{},"password":{},"title":{},"content":{},"author":{},"excerpt":{},"featured_media":{},"comment_status":{},"ping_status":{},"format":{},"meta":{},"sticky":{},"template":{},"categories":{},"tags":{}}}]},"/wp/v2/posts/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{"date":{},"date_gmt":{},"slug":{},"status":{},"password":{},"title":{},"content":{},"author":{},"excerpt":{},"featured_media":{},"comment_status":{},"ping_status":{},"format":{},"meta":{},"sticky":{},"template":{},"categories":{},"tags":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/posts/(?P<parent>[\\d]+)/revisions":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/posts/(?P<parent>[\\d]+)/revisions/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/pages":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"before":{},"exclude":{},"include":{},"menu_order":{},"offset":{},"order":{},"orderby":{},"parent":{},"parent_exclude":{},"slug":{},"status":{}}},{"methods":["POST"],"args":{"date":{},"date_gmt":{},"slug":{},"status":{},"password":{},"parent":{},"title":{},"content":{},"author":{},"excerpt":{},"featured_media":{},"comment_status":{},"ping_status":{},"menu_order":{},"meta":{},"template":{}}}]},"/wp/v2/pages/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{"date":{},"date_gmt":{},"slug":{},"status":{},"password":{},"parent":{},"title":{},"content":{},"author":{},"excerpt":{},"featured_media":{},"comment_status":{},"ping_status":{},"menu_order":{},"meta":{},"template":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/pages/(?P<parent>[\\d]+)/revisions":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/pages/(?P<parent>[\\d]+)/revisions/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/media":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"parent":{},"parent_exclude":{},"slug":{},"status":{},"media_type":{},"mime_type":{}}},{"methods":["POST"],"args":{"date":{},"date_gmt":{},"slug":{},"status":{},"title":{},"author":{},"comment_status":{},"ping_status":{},"meta":{},"template":{},"alt_text":{},"caption":{},"description":{},"post":{}}}]},"/wp/v2/media/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{"date":{},"date_gmt":{},"slug":{},"status":{},"title":{},"author":{},"comment_status":{},"ping_status":{},"meta":{},"template":{},"alt_text":{},"caption":{},"description":{},"post":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/types":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/types/(?P<type>[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/statuses":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/statuses/(?P<status>[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/taxonomies":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{},"type":{}}}]},"/wp/v2/taxonomies/(?P<taxonomy>[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/categories":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"order":{},"orderby":{},"hide_empty":{},"parent":{},"post":{},"slug":{}}},{"methods":["POST"],"args":{"description":{},"name":{},"slug":{},"parent":{},"meta":{}}}]},"/wp/v2/categories/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{"description":{},"name":{},"slug":{},"parent":{},"meta":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/tags":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"hide_empty":{},"post":{},"slug":{}}},{"methods":["POST"],"args":{"description":{},"name":{},"slug":{},"meta":{}}}]},"/wp/v2/tags/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{"description":{},"name":{},"slug":{},"meta":{}}},{"methods":["DELETE"],"args":{"force":{}}}]},"/wp/v2/users":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"slug":{},"roles":{}}},{"methods":["POST"],"args":{"username":{},"name":{},"first_name":{},"last_name":{},"email":{},"url":{},"description":{},"locale":{},"nickname":{},"slug":{},"roles":{},"password":{},"meta":{}}}]},"/wp/v2/users/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{"username":{},"name":{},"first_name":{},"last_name":{},"email":{},"url":{},"description":{},"locale":{},"nickname":{},"slug":{},"roles":{},"password":{},"meta":{}}},{"methods":["DELETE"],"args":{"force":{},"reassign":{}}}]},"/wp/v2/users/me":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{"username":{},"name":{},"first_name":{},"last_name":{},"email":{},"url":{},"description":{},"locale":{},"nickname":{},"slug":{},"roles":{},"password":{},"meta":{}}},{"methods":["DELETE"],"args":{"force":{},"reassign":{}}}]},"/wp/v2/comments":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"author_email":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"parent":{},"parent_exclude":{},"post":{},"status":{},"type":{},"password":{}}},{"methods":["POST"],"args":{"author":{},"author_email":{},"author_ip":{},"author_name":{},"author_url":{},"author_user_agent":{},"content":{},"date":{},"date_gmt":{},"parent":{},"post":{},"status":{},"meta":{}}}]},"/wp/v2/comments/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{"author":{},"author_email":{},"author_ip":{},"author_name":{},"author_url":{},"author_user_agent":{},"content":{},"date":{},"date_gmt":{},"parent":{},"post":{},"status":{},"meta":{}}},{"methods":["DELETE"],"args":{"force":{},"password":{}}}]},"/wp/v2/settings":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH"],"endpoints":[{"methods":["GET"],"args":{}},{"methods":["POST","PUT","PATCH"],"args":{"title":{},"description":{},"url":{},"email":{},"timezone":{},"date_format":{},"time_format":{},"start_of_week":{},"language":{},"use_smilies":{},"default_category":{},"default_post_format":{},"posts_per_page":{},"default_ping_status":{},"default_comment_status":{}}}]}}
\ No newline at end of file
+{"/":{"namespace":"","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/oembed/1.0":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"namespace":{},"context":{}}}]},"/oembed/1.0/embed":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"url":{},"format":{},"maxwidth":{}}}]},"/oembed/1.0/proxy":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"url":{},"format":{},"maxwidth":{},"maxheight":{},"discover":{}}}]},"/wp/v2":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"namespace":{},"context":{}}}]},"/wp/v2/posts":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"slug":{},"status":{},"categories":{},"categories_exclude":{},"tags":{},"tags_exclude":{},"sticky":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/posts/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/posts/(?P<parent>[\\d]+)/revisions":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{}}}]},"/wp/v2/posts/(?P<parent>[\\d]+)/revisions/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","DELETE"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"id":{},"context":{}}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/posts/(?P<id>[\\d]+)/autosaves":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"context":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/posts/(?P<parent>[\\d]+)/autosaves/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"id":{},"context":{}}}]},"/wp/v2/pages":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"before":{},"exclude":{},"include":{},"menu_order":{},"offset":{},"order":{},"orderby":{},"parent":{},"parent_exclude":{},"slug":{},"status":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/pages/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/pages/(?P<parent>[\\d]+)/revisions":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{}}}]},"/wp/v2/pages/(?P<parent>[\\d]+)/revisions/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","DELETE"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"id":{},"context":{}}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/pages/(?P<id>[\\d]+)/autosaves":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"context":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/pages/(?P<parent>[\\d]+)/autosaves/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"id":{},"context":{}}}]},"/wp/v2/media":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"parent":{},"parent_exclude":{},"slug":{},"status":{},"media_type":{},"mime_type":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/media/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/blocks":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"slug":{},"status":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/blocks/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/blocks/(?P<id>[\\d]+)/autosaves":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"context":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/blocks/(?P<parent>[\\d]+)/autosaves/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"parent":{},"id":{},"context":{}}}]},"/wp/v2/types":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/types/(?P<type>[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"type":{},"context":{}}}]},"/wp/v2/statuses":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{}}}]},"/wp/v2/statuses/(?P<status>[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"status":{},"context":{}}}]},"/wp/v2/taxonomies":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{},"type":{}}}]},"/wp/v2/taxonomies/(?P<taxonomy>[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"taxonomy":{},"context":{}}}]},"/wp/v2/categories":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"order":{},"orderby":{},"hide_empty":{},"parent":{},"post":{},"slug":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/categories/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/tags":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"hide_empty":{},"post":{},"slug":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/tags/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/users":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"slug":{},"roles":{},"who":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/users/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/users/me":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"context":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/comments":{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"after":{},"author":{},"author_exclude":{},"author_email":{},"before":{},"exclude":{},"include":{},"offset":{},"order":{},"orderby":{},"parent":{},"parent_exclude":{},"post":{},"status":{},"type":{},"password":{}}},{"methods":["POST"],"args":{}}]},"/wp/v2/comments/(?P<id>[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"endpoints":[{"methods":["GET"],"args":{"id":{},"context":{},"password":{}}},{"methods":["POST","PUT","PATCH"],"args":{}},{"methods":["DELETE"],"args":{}}]},"/wp/v2/search":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"type":{},"subtype":{}}}]},"/wp/v2/block-renderer/(?P<name>core/block)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"name":{},"context":{},"attributes":{},"post_id":{}}}]},"/wp/v2/block-renderer/(?P<name>core/latest-comments)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"name":{},"context":{},"attributes":{},"post_id":{}}}]},"/wp/v2/block-renderer/(?P<name>core/archives)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"name":{},"context":{},"attributes":{},"post_id":{}}}]},"/wp/v2/block-renderer/(?P<name>core/categories)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"name":{},"context":{},"attributes":{},"post_id":{}}}]},"/wp/v2/block-renderer/(?P<name>core/latest-posts)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"name":{},"context":{},"attributes":{},"post_id":{}}}]},"/wp/v2/block-renderer/(?P<name>core/shortcode)":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"name":{},"context":{},"attributes":{},"post_id":{}}}]},"/wp/v2/settings":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH"],"endpoints":[{"methods":["GET"],"args":{}},{"methods":["POST","PUT","PATCH"],"args":{}}]},"/wp/v2/themes":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{},"page":{},"per_page":{},"search":{},"status":{}}}]}}
\ No newline at end of file
diff --git a/lib/endpoint-factories.js b/lib/endpoint-factories.js
index 655a534..159b371 100644
--- a/lib/endpoint-factories.js
+++ b/lib/endpoint-factories.js
@@ -6,10 +6,9 @@
  */
 'use strict';
 
-var extend = require( 'node.extend' );
-var createResourceHandlerSpec = require( './resource-handler-spec' ).create;
-var createEndpointRequest = require( './endpoint-request' ).create;
-var objectReduce = require( './util/object-reduce' );
+const createResourceHandlerSpec = require( './resource-handler-spec' ).create;
+const createEndpointRequest = require( './endpoint-request' ).create;
+const objectReduce = require( './util/object-reduce' );
 
 /**
  * Given an array of route definitions and a specific namespace for those routes,
@@ -27,19 +26,22 @@ var objectReduce = require( './util/object-reduce' );
  */
 function generateEndpointFactories( routesByNamespace ) {
 
-	return objectReduce( routesByNamespace, function( namespaces, routeDefinitions, namespace ) {
+	return objectReduce( routesByNamespace, ( namespaces, routeDefinitions, namespace ) => {
 
 		// Create
-		namespaces[ namespace ] = objectReduce( routeDefinitions, function( handlers, routeDef, resource ) {
+		namespaces[ namespace ] = objectReduce( routeDefinitions, ( handlers, routeDef, resource ) => {
 
-			var handlerSpec = createResourceHandlerSpec( routeDef, resource );
+			const handlerSpec = createResourceHandlerSpec( routeDef, resource );
 
-			var EndpointRequest = createEndpointRequest( handlerSpec, resource, namespace );
+			const EndpointRequest = createEndpointRequest( handlerSpec, resource, namespace );
 
 			// "handler" object is now fully prepared; create the factory method that
 			// will instantiate and return a handler instance
 			handlers[ resource ] = function( options ) {
-				return new EndpointRequest( extend( {}, this._options, options ) );
+				return new EndpointRequest( {
+					...this._options,
+					...options,
+				} );
 			};
 
 			// Expose the constructor as a property on the factory function, so that
diff --git a/lib/endpoint-request.js b/lib/endpoint-request.js
index 379a62c..eb20b02 100644
--- a/lib/endpoint-request.js
+++ b/lib/endpoint-request.js
@@ -3,11 +3,11 @@
  */
 'use strict';
 
-var inherit = require( 'util' ).inherits;
-var WPRequest = require( './constructors/wp-request' );
-var mixins = require( './mixins' );
+const inherit = require( 'util' ).inherits;
+const WPRequest = require( './constructors/wp-request' );
+const mixins = require( './mixins' );
 
-var applyMixin = require( './util/apply-mixin' );
+const applyMixin = require( './util/apply-mixin' );
 
 /**
  * Create an endpoint request handler constructor for a specific resource tree
@@ -45,20 +45,20 @@ function createEndpointRequest( handlerSpec, resource, namespace ) {
 	// Mix in all available shortcut methods for GET request query parameters that
 	// are valid within this endpoint tree
 	if ( typeof handlerSpec._getArgs === 'object' ) {
-		Object.keys( handlerSpec._getArgs ).forEach( function( supportedQueryParam ) {
-			var mixinsForParam = mixins[ supportedQueryParam ];
+		Object.keys( handlerSpec._getArgs ).forEach( ( supportedQueryParam ) => {
+			const mixinsForParam = mixins[ supportedQueryParam ];
 
 			// Only proceed if there is a mixin available AND the specified mixins will
 			// not overwrite any previously-set prototype method
 			if ( typeof mixinsForParam === 'object' ) {
-				Object.keys( mixinsForParam ).forEach( function( methodName ) {
+				Object.keys( mixinsForParam ).forEach( ( methodName ) => {
 					applyMixin( EndpointRequest.prototype, methodName, mixinsForParam[ methodName ] );
 				} );
 			}
 		} );
 	}
 
-	Object.keys( handlerSpec._setters ).forEach( function( setterFnName ) {
+	Object.keys( handlerSpec._setters ).forEach( ( setterFnName ) => {
 		// Only assign setter functions if they do not overwrite preexisting methods
 		if ( ! EndpointRequest.prototype[ setterFnName ] ) {
 			EndpointRequest.prototype[ setterFnName ] = handlerSpec._setters[ setterFnName ];
diff --git a/lib/http-transport.js b/lib/http-transport.js
index 9e0ca7b..5e8bc52 100644
--- a/lib/http-transport.js
+++ b/lib/http-transport.js
@@ -3,18 +3,14 @@
  */
 'use strict';
 
-/*jshint -W079 */// Suppress warning about redefiniton of `Promise`
-var Promise = require( 'es6-promise' ).Promise;
+const agent = require( 'superagent' );
+const parseLinkHeader = require( 'li' ).parse;
+const url = require( 'url' );
 
-var agent = require( 'superagent' );
-var parseLinkHeader = require( 'li' ).parse;
-var url = require( 'url' );
-
-var WPRequest = require( './constructors/wp-request' );
-var checkMethodSupport = require( './util/check-method-support' );
-var extend = require( 'node.extend' );
-var objectReduce = require( './util/object-reduce' );
-var isEmptyObject = require( './util/is-empty-object' );
+const WPRequest = require( './constructors/wp-request' );
+const checkMethodSupport = require( './util/check-method-support' );
+const objectReduce = require( './util/object-reduce' );
+const isEmptyObject = require( './util/is-empty-object' );
 
 /**
  * Set any provided headers on the outgoing request object. Runs after _auth.
@@ -31,9 +27,11 @@ function _setHeaders( request, options ) {
 		return request;
 	}
 
-	return objectReduce( options.headers, function( request, value, key ) {
-		return request.set( key, value );
-	}, request );
+	return objectReduce(
+		options.headers,
+		( request, value, key ) => request.set( key, value ),
+		request
+	);
 }
 
 /**
@@ -59,8 +57,8 @@ function _auth( request, options, forceAuthentication ) {
 	}
 
 	// Retrieve the username & password from the request options if they weren't provided
-	var username = username || options.username;
-	var password = password || options.password;
+	const username = options.username;
+	const password = options.password;
 
 	// If no username or no password, can't authenticate
 	if ( ! username || ! password ) {
@@ -88,7 +86,7 @@ function _auth( request, options, forceAuthentication ) {
  * @returns {String} The full URL path to the provided link
  */
 function mergeUrl( endpoint, linkPath ) {
-	var request = url.parse( endpoint );
+	const request = url.parse( endpoint );
 	linkPath = url.parse( linkPath, true );
 
 	// Overwrite relevant request URL object properties with the link's values:
@@ -112,7 +110,7 @@ function mergeUrl( endpoint, linkPath ) {
  * @returns {Object} The response content as a JS object
  */
 function extractResponseBody( response ) {
-	var responseBody = response.body;
+	let responseBody = response.body;
 	if ( isEmptyObject( responseBody ) && response.type === 'text/html' ) {
 		// Response may have come back as HTML due to caching plugin; try to parse
 		// the response text into JSON
@@ -148,14 +146,14 @@ function extractResponseBody( response ) {
  * @returns {Object} The pagination metadata object for this HTTP request, or else null
  */
 function createPaginationObject( result, options, httpTransport ) {
-	var _paging = null;
+	let _paging = null;
 
 	if ( ! result.headers || ! result.headers[ 'x-wp-totalpages' ] ) {
 		// No headers: return as-is
 		return _paging;
 	}
 
-	var totalPages = result.headers[ 'x-wp-totalpages' ];
+	const totalPages = result.headers[ 'x-wp-totalpages' ];
 
 	if ( ! totalPages || totalPages === '0' ) {
 		// No paging: return as-is
@@ -163,7 +161,7 @@ function createPaginationObject( result, options, httpTransport ) {
 	}
 
 	// Decode the link header object
-	var links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};
+	const links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};
 
 	// Store pagination data from response headers on the response collection
 	_paging = {
@@ -174,22 +172,24 @@ function createPaginationObject( result, options, httpTransport ) {
 
 	// Re-use any options from the original request, updating only the endpoint
 	// (this ensures that request properties like authentication are preserved)
-	var endpoint = options.endpoint;
+	const endpoint = options.endpoint;
 
 	// Create a WPRequest instance pre-bound to the "next" page, if available
 	if ( links.next ) {
-		_paging.next = new WPRequest( extend( {}, options, {
+		_paging.next = new WPRequest( {
+			...options,
 			transport: httpTransport,
 			endpoint: mergeUrl( endpoint, links.next ),
-		} ) );
+		} );
 	}
 
 	// Create a WPRequest instance pre-bound to the "prev" page, if available
 	if ( links.prev ) {
-		_paging.prev = new WPRequest( extend( {}, options, {
+		_paging.prev = new WPRequest( {
+			...options,
 			transport: httpTransport,
 			endpoint: mergeUrl( endpoint, links.prev ),
-		} ) );
+		} );
 	}
 
 	return _paging;
@@ -209,9 +209,9 @@ function createPaginationObject( result, options, httpTransport ) {
  * @returns {Promise} A promise to the superagent request
  */
 function invokeAndPromisify( request, callback, transform ) {
-	return new Promise( function( resolve, reject ) {
+	return new Promise( ( resolve, reject ) => {
 		// Fire off the result
-		request.end( function( err, result ) {
+		request.end( ( err, result ) => {
 
 			// Return the results as a promise
 			if ( err || result.error ) {
@@ -220,14 +220,14 @@ function invokeAndPromisify( request, callback, transform ) {
 				resolve( result );
 			}
 		} );
-	} ).then( transform ).then( function( result ) {
+	} ).then( transform ).then( ( result ) => {
 		// If a node-style callback was provided, call it, but also return the
 		// result value for use via the returned Promise
 		if ( callback && typeof callback === 'function' ) {
 			callback( null, result );
 		}
 		return result;
-	}, function( err ) {
+	}, ( err ) => {
 		// If the API provided an error object, it will be available within the
 		// superagent response object as response.body (containing the response
 		// JSON). If that object exists, it will have a .code property if it is
@@ -258,8 +258,8 @@ function invokeAndPromisify( request, callback, transform ) {
  *                  pagination information if the result is a partial collection.
  */
 function returnBody( wpreq, result ) {
-	var body = extractResponseBody( result );
-	var _paging = createPaginationObject( result, wpreq._options, wpreq.transport );
+	const body = extractResponseBody( result );
+	const _paging = createPaginationObject( result, wpreq._options, wpreq.transport );
 	if ( _paging ) {
 		body._paging = _paging;
 	}
@@ -289,9 +289,9 @@ function returnHeaders( result ) {
  */
 function _httpGet( wpreq, callback ) {
 	checkMethodSupport( 'get', wpreq );
-	var url = wpreq.toString();
+	const url = wpreq.toString();
 
-	var request = _auth( agent.get( url ), wpreq._options );
+	let request = _auth( agent.get( url ), wpreq._options );
 	request = _setHeaders( request, wpreq._options );
 
 	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
@@ -308,16 +308,18 @@ function _httpGet( wpreq, callback ) {
  */
 function _httpPost( wpreq, data, callback ) {
 	checkMethodSupport( 'post', wpreq );
-	var url = wpreq.toString();
+	const url = wpreq.toString();
 	data = data || {};
-	var request = _auth( agent.post( url ), wpreq._options, true );
+	let request = _auth( agent.post( url ), wpreq._options, true );
 	request = _setHeaders( request, wpreq._options );
 
 	if ( wpreq._attachment ) {
 		// Data must be form-encoded alongside image attachment
-		request = objectReduce( data, function( req, value, key ) {
-			return req.field( key, value );
-		}, request.attach( 'file', wpreq._attachment, wpreq._attachmentName ) );
+		request = objectReduce(
+			data,
+			( req, value, key ) => req.field( key, value ),
+			request.attach( 'file', wpreq._attachment, wpreq._attachmentName )
+		);
 	} else {
 		request = request.send( data );
 	}
@@ -335,10 +337,10 @@ function _httpPost( wpreq, data, callback ) {
  */
 function _httpPut( wpreq, data, callback ) {
 	checkMethodSupport( 'put', wpreq );
-	var url = wpreq.toString();
+	const url = wpreq.toString();
 	data = data || {};
 
-	var request = _auth( agent.put( url ), wpreq._options, true ).send( data );
+	let request = _auth( agent.put( url ), wpreq._options, true ).send( data );
 	request = _setHeaders( request, wpreq._options );
 
 	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
@@ -358,8 +360,8 @@ function _httpDelete( wpreq, data, callback ) {
 		data = null;
 	}
 	checkMethodSupport( 'delete', wpreq );
-	var url = wpreq.toString();
-	var request = _auth( agent.del( url ), wpreq._options, true ).send( data );
+	const url = wpreq.toString();
+	let request = _auth( agent.del( url ), wpreq._options, true ).send( data );
 	request = _setHeaders( request, wpreq._options );
 
 	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
@@ -374,8 +376,8 @@ function _httpDelete( wpreq, data, callback ) {
  */
 function _httpHead( wpreq, callback ) {
 	checkMethodSupport( 'head', wpreq );
-	var url = wpreq.toString();
-	var request = _auth( agent.head( url ), wpreq._options );
+	const url = wpreq.toString();
+	let request = _auth( agent.head( url ), wpreq._options );
 	request = _setHeaders( request, wpreq._options );
 
 	return invokeAndPromisify( request, callback, returnHeaders );
diff --git a/lib/mixins/filters.js b/lib/mixins/filters.js
index 4fe92ba..dacc722 100644
--- a/lib/mixins/filters.js
+++ b/lib/mixins/filters.js
@@ -3,11 +3,10 @@
  */
 'use strict';
 
-var _unique = require( 'lodash.uniq' );
-var extend = require( 'node.extend' );
+const _unique = require( 'lodash.uniq' );
 
-var alphaNumericSort = require( '../util/alphanumeric-sort' );
-var keyValToObj = require( '../util/key-val-to-obj' );
+const alphaNumericSort = require( '../util/alphanumeric-sort' );
+const keyValToObj = require( '../util/key-val-to-obj' );
 
 /**
  * Filter methods that can be mixed in to a request constructor's prototype to
@@ -16,7 +15,7 @@ var keyValToObj = require( '../util/key-val-to-obj' );
  *
  * @mixin filters
  */
-var filterMixins = {};
+const filterMixins = {};
 
 // Filter Methods
 // ==============
@@ -47,8 +46,6 @@ var filterMixins = {};
  * @returns The request instance (for chaining)
  */
 filterMixins.filter = function( props, value ) {
-	/* jshint validthis:true */
-
 	if ( ! props || typeof props === 'string' && value === undefined ) {
 		// We have no filter to set, or no value to set for that filter
 		return this;
@@ -59,7 +56,10 @@ filterMixins.filter = function( props, value ) {
 		props = keyValToObj( props, value );
 	}
 
-	this._filters = extend( this._filters, props );
+	this._filters = {
+		...this._filters,
+		...props,
+	};
 
 	return this;
 };
@@ -74,19 +74,21 @@ filterMixins.filter = function( props, value ) {
  * @returns The request instance (for chaining)
  */
 filterMixins.taxonomy = function( taxonomy, term ) {
-	/* jshint validthis:true */
-	var termIsArray = Array.isArray( term );
-	var termIsNumber = termIsArray ?
-		term.reduce( function( allAreNumbers, term ) {
-			return allAreNumbers && typeof term === 'number';
-		}, true ) :
+	const termIsArray = Array.isArray( term );
+
+	const termIsNumber = termIsArray ?
+		term.reduce(
+			( allAreNumbers, term ) => allAreNumbers && typeof term === 'number',
+			true
+		) :
 		typeof term === 'number';
-	var termIsString = termIsArray ?
-		term.reduce( function( allAreStrings, term ) {
-			return allAreStrings && typeof term === 'string';
-		}, true ) :
+
+	const termIsString = termIsArray ?
+		term.reduce(
+			( allAreStrings, term ) => allAreStrings && typeof term === 'string',
+			true
+		) :
 		typeof term === 'string';
-	var taxonomyTerms;
 
 	if ( ! termIsString && ! termIsNumber ) {
 		throw new Error( 'term must be a number, string, or array of numbers or strings' );
@@ -111,7 +113,7 @@ filterMixins.taxonomy = function( taxonomy, term ) {
 	this._taxonomyFilters = this._taxonomyFilters || {};
 
 	// Ensure there's an array of terms available for this taxonomy
-	taxonomyTerms = ( this._taxonomyFilters[ taxonomy ] || [] )
+	const taxonomyTerms = ( this._taxonomyFilters[ taxonomy ] || [] )
 		// Insert the provided terms into the specified taxonomy's terms array
 		.concat( term )
 		// Sort array
@@ -132,7 +134,6 @@ filterMixins.taxonomy = function( taxonomy, term ) {
  * @returns The request instance (for chaining)
  */
 filterMixins.year = function( year ) {
-	/* jshint validthis:true */
 	return filterMixins.filter.call( this, 'year', year );
 };
 
@@ -146,8 +147,7 @@ filterMixins.year = function( year ) {
  * @returns The request instance (for chaining)
  */
 filterMixins.month = function( month ) {
-	/* jshint validthis:true */
-	var monthDate;
+	let monthDate;
 	if ( typeof month === 'string' ) {
 		// Append a arbitrary day and year to the month to parse the string into a Date
 		monthDate = new Date( Date.parse( month + ' 1, 2012' ) );
@@ -178,7 +178,6 @@ filterMixins.month = function( month ) {
  * @returns The request instance (for chaining)
  */
 filterMixins.day = function( day ) {
-	/* jshint validthis:true */
 	return filterMixins.filter.call( this, 'day', day );
 };
 
@@ -191,7 +190,6 @@ filterMixins.day = function( day ) {
  * @returns The request instance (for chaining)
  */
 filterMixins.path = function( path ) {
-	/* jshint validthis:true */
 	return filterMixins.filter.call( this, 'pagename', path );
 };
 
diff --git a/lib/mixins/index.js b/lib/mixins/index.js
index 1199ffa..4845a7c 100644
--- a/lib/mixins/index.js
+++ b/lib/mixins/index.js
@@ -4,8 +4,8 @@
  */
 'use strict';
 
-var filterMixins = require( './filters' );
-var parameterMixins = require( './parameters' );
+const filterMixins = require( './filters' );
+const parameterMixins = require( './parameters' );
 
 // `.context`, `.embed`, and `.edit` (a shortcut for `context(edit, true)`) are
 // supported by default in WPRequest, as is the base `.param` method. Any GET
@@ -15,7 +15,7 @@ var parameterMixins = require( './parameters' );
 // accepted by the API endpoint corresponds to multiple individual mixin
 // functions, or where the name we use for the function diverges from that
 // of the query parameter that the mixin sets.
-var mixins = {
+const mixins = {
 	categories: {
 		categories: parameterMixins.categories,
 		/** @deprecated use .categories() */
@@ -52,7 +52,7 @@ var mixins = {
 	'password',
 	'status',
 	'sticky',
-].forEach( function( mixinName ) {
+].forEach( ( mixinName ) => {
 	mixins[ mixinName ] = {};
 	mixins[ mixinName ][ mixinName ] = parameterMixins[ mixinName ];
 } );
diff --git a/lib/mixins/parameters.js b/lib/mixins/parameters.js
index e1e7d0f..a9bf32b 100644
--- a/lib/mixins/parameters.js
+++ b/lib/mixins/parameters.js
@@ -7,21 +7,20 @@
  * @module mixins/parameters
  */
 'use strict';
-/*jshint -W106 */// Disable underscore_case warnings in this file b/c WP uses them
 
-var paramSetter = require( '../util/parameter-setter' );
-var argumentIsNumeric = require( '../util/argument-is-numeric' );
+const paramSetter = require( '../util/parameter-setter' );
+const argumentIsNumeric = require( '../util/argument-is-numeric' );
 
 /**
  * @mixin parameters
  */
-var parameterMixins = {};
+const parameterMixins = {};
 
-var filters = require( './filters' );
+const filters = require( './filters' );
 // Needed for .author mixin, as author by ID is a parameter and by Name is a filter
-var filter = filters.filter;
+const filter = filters.filter;
 // Needed for .tag and .category mixin, for deprecated query-by-slug support
-var taxonomy = filters.taxonomy;
+const taxonomy = filters.taxonomy;
 
 // Parameter Methods
 // =================
@@ -44,7 +43,6 @@ var taxonomy = filters.taxonomy;
  * @returns The request instance (for chaining)
  */
 parameterMixins.author = function( author ) {
-	/* jshint validthis:true */
 	if ( author === undefined ) {
 		return this;
 	}
@@ -156,7 +154,6 @@ parameterMixins.categories = paramSetter( 'categories' );
  * @returns The request instance (for chaining)
  */
 parameterMixins.category = function( category ) {
-	/* jshint validthis:true */
 	if ( argumentIsNumeric( category ) ) {
 		return parameterMixins.categories.call( this, category );
 	}
@@ -193,7 +190,6 @@ parameterMixins.tags = paramSetter( 'tags' );
  * @returns The request instance (for chaining)
  */
 parameterMixins.tag = function( tag ) {
-	/* jshint validthis:true */
 	if ( argumentIsNumeric( tag ) ) {
 		return parameterMixins.tags.call( this, tag );
 	}
@@ -230,7 +226,6 @@ parameterMixins.excludeTags = paramSetter( 'tags_exclude' );
  * @returns The request instance (for chaining)
  */
 parameterMixins.before = function( date ) {
-	/* jshint validthis:true */
 	return this.param( 'before', new Date( date ).toISOString() );
 };
 
@@ -251,7 +246,6 @@ parameterMixins.before = function( date ) {
  * @returns The request instance (for chaining)
  */
 parameterMixins.after = function( date ) {
-	/* jshint validthis:true */
 	return this.param( 'after', new Date( date ).toISOString() );
 };
 
diff --git a/lib/path-part-setter.js b/lib/path-part-setter.js
index 1584a4d..497d081 100644
--- a/lib/path-part-setter.js
+++ b/lib/path-part-setter.js
@@ -17,18 +17,16 @@
  */
 function createPathPartSetter( node ) {
 	// Local references to `node` properties used by returned functions
-	var nodeLevel = node.level;
-	var nodeName = node.names[ 0 ];
-	var supportedMethods = node.methods || [];
-	var dynamicChildren = node.children ? Object.keys( node.children )
-		.map( function( key ) {
-			return node.children[ key ];
-		} )
-		.filter( function( childNode ) {
-			return childNode.namedGroup === true;
-		} ) : [];
-	var dynamicChild = dynamicChildren.length === 1 && dynamicChildren[ 0 ];
-	var dynamicChildLevel = dynamicChild && dynamicChild.level;
+	const nodeLevel = node.level;
+	const nodeName = node.names[ 0 ];
+	const supportedMethods = node.methods || [];
+	const dynamicChildren = node.children ?
+		Object.keys( node.children )
+			.map( key => node.children[ key ] )
+			.filter( childNode => ( childNode.namedGroup === true ) ) :
+		[];
+	const dynamicChild = dynamicChildren.length === 1 && dynamicChildren[ 0 ];
+	const dynamicChildLevel = dynamicChild && dynamicChild.level;
 
 	if ( node.namedGroup ) {
 		/**
@@ -44,7 +42,6 @@ function createPathPartSetter( node ) {
 		 * @returns {Object} The handler instance (for chaining)
 		 */
 		return function( val ) {
-			/* jshint validthis:true */
 			this.setPathPart( nodeLevel, val );
 			if ( supportedMethods.length ) {
 				this._supportedMethods = supportedMethods;
@@ -69,7 +66,6 @@ function createPathPartSetter( node ) {
 		 * @returns {Object} The handler instance (for chaining)
 		 */
 		return function( val ) {
-			/* jshint validthis:true */
 			// If the path part is not a namedGroup, it should have exactly one
 			// entry in the names array: use that as the value for this setter,
 			// as it will usually correspond to a collection endpoint.
diff --git a/lib/resource-handler-spec.js b/lib/resource-handler-spec.js
index 9814485..2720142 100644
--- a/lib/resource-handler-spec.js
+++ b/lib/resource-handler-spec.js
@@ -3,7 +3,7 @@
  */
 'use strict';
 
-var createPathPartSetter = require( './path-part-setter' ).create;
+const createPathPartSetter = require( './path-part-setter' ).create;
 
 /** @private */
 function addLevelOption( levelsObj, level, obj ) {
@@ -20,7 +20,7 @@ function addLevelOption( levelsObj, level, obj ) {
  * @param {Object} node    A route hierarchy level node object
  */
 function assignSetterFnForNode( handler, node ) {
-	var setterFn;
+	let setterFn;
 
 	// For each node, add its handler to the relevant "level" representation
 	addLevelOption( handler._levels, node.level, {
@@ -34,12 +34,12 @@ function assignSetterFnForNode( handler, node ) {
 
 		setterFn = createPathPartSetter( node );
 
-		node.names.forEach( function( name ) {
+		node.names.forEach( ( name ) => {
 			// Convert from snake_case to camelCase
-			var setterFnName = name
-				.replace( /[_-]+\w/g, function( match ) {
-					return match.replace( /[_-]+/, '' ).toUpperCase();
-				} );
+			const setterFnName = name.replace(
+				/[_-]+\w/g,
+				match => match.replace( /[_-]+/, '' ).toUpperCase()
+			);
 
 			// Don't overwrite previously-set methods
 			if ( ! handler._setters[ setterFnName ] ) {
@@ -75,7 +75,7 @@ function extractSetterFromNode( handler, node ) {
 
 	if ( node.children ) {
 		// Recurse down to this node's children
-		Object.keys( node.children ).forEach( function( key ) {
+		Object.keys( node.children ).forEach( ( key ) => {
 			extractSetterFromNode( handler, node.children[ key ] );
 		} );
 	}
@@ -91,7 +91,7 @@ function extractSetterFromNode( handler, node ) {
  */
 function createNodeHandlerSpec( routeDefinition, resource ) {
 
-	var handler = {
+	const handler = {
 		// A "path" is an ordered (by key) set of values composed into the final URL
 		_path: {
 			'0': resource,
@@ -112,7 +112,7 @@ function createNodeHandlerSpec( routeDefinition, resource ) {
 	};
 
 	// Walk the tree
-	Object.keys( routeDefinition ).forEach( function( routeDefProp ) {
+	Object.keys( routeDefinition ).forEach( ( routeDefProp ) => {
 		if ( routeDefProp !== '_getArgs' ) {
 			extractSetterFromNode( handler, routeDefinition[ routeDefProp ] );
 		}
diff --git a/lib/route-tree.js b/lib/route-tree.js
index 0db255a..3ffc90d 100644
--- a/lib/route-tree.js
+++ b/lib/route-tree.js
@@ -3,10 +3,10 @@
  */
 'use strict';
 
-var namedGroupRE = require( './util/named-group-regexp' ).namedGroupRE;
-var splitPath = require( './util/split-path' );
-var ensure = require( './util/ensure' );
-var objectReduce = require( './util/object-reduce' );
+const namedGroupRE = require( './util/named-group-regexp' ).namedGroupRE;
+const splitPath = require( './util/split-path' );
+const ensure = require( './util/ensure' );
+const objectReduce = require( './util/object-reduce' );
 
 /**
  * Method to use when reducing route components array.
@@ -33,12 +33,12 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
 	//   index: 15,
 	//   input: '/wp/v2/posts/(?P<id>[\\d]+)'
 	// ]
-	var namedGroup = component.match( namedGroupRE );
+	const namedGroup = component.match( namedGroupRE );
 	// Pull out references to the relevant indices of the match, for utility:
 	// `null` checking is necessary in case the component did not match the RE,
 	// hence the `namedGroup &&`.
-	var groupName = namedGroup && namedGroup[ 1 ];
-	var groupPattern = namedGroup && namedGroup[ 2 ];
+	const groupName = namedGroup && namedGroup[ 1 ];
+	const groupPattern = namedGroup && namedGroup[ 2 ];
 
 	// When branching based on a dynamic capture group we used the group's RE
 	// pattern as the unique identifier: this is done because the same group
@@ -49,17 +49,17 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
 	// a custom route via `.registerRoute` that does not include parameter
 	// validation. In this case we assume the groupName is sufficiently unique,
 	// and fall back to `|| groupName` for the levelKey string.
-	var levelKey = namedGroup ? ( groupPattern || groupName ) : component;
+	const levelKey = namedGroup ? ( groupPattern || groupName ) : component;
 
 	// Level name on the other hand takes its value from the group's name, if
 	// defined, and falls back to the component string to handle situations where
 	// `component` is a collection (e.g. "revisions")
-	var levelName = namedGroup ? groupName : component;
+	const levelName = namedGroup ? groupName : component;
 
 	// Check whether we have a preexisting node at this level of the tree, and
 	// create a new level object if not. The component string is included so that
 	// validators can throw meaningful errors as appropriate.
-	var currentLevel = parentLevel[ levelKey ] || {
+	const currentLevel = parentLevel[ levelKey ] || {
 		component: component,
 		namedGroup: namedGroup ? true : false,
 		level: idx,
@@ -76,7 +76,7 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
 	// on the request URL is of the proper type for the location in which it
 	// is specified. If a group pattern was found, the validator checks whether
 	// the input string exactly matches the group pattern.
-	var groupPatternRE = groupPattern === '' ?
+	const groupPatternRE = groupPattern === '' ?
 		// If groupPattern is an empty string, accept any input without validation
 		/.*/ :
 		// Otherwise, validate against the group pattern or the component string
@@ -84,9 +84,7 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
 
 	// Only one validate function is maintained for each node, because each node
 	// is defined either by a string literal or by a specific regular expression.
-	currentLevel.validate = function( input ) {
-		return groupPatternRE.test( input );
-	};
+	currentLevel.validate = input => groupPatternRE.test( input );
 
 	// Check to see whether to expect more nodes within this branch of the tree,
 	if ( components[ idx + 1 ] ) {
@@ -94,9 +92,8 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
 		currentLevel.children = currentLevel.children || {};
 	} else {
 		// At leaf nodes, specify the method capabilities of this endpoint
-		currentLevel.methods = ( routeObj.methods || [] ).map( function( str ) {
-			return str.toLowerCase();
-		} );
+		currentLevel.methods = ( routeObj.methods || [] ).map( str => str.toLowerCase() );
+
 		// Ensure HEAD is included whenever GET is supported: the API automatically
 		// adds support for HEAD if you have GET
 		if ( currentLevel.methods.indexOf( 'get' ) > -1 && currentLevel.methods.indexOf( 'head' ) === -1 ) {
@@ -108,13 +105,13 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
 		// appropriate parameter mixins
 		if ( routeObj.endpoints ) {
 			topLevel._getArgs = topLevel._getArgs || {};
-			routeObj.endpoints.forEach( function( endpoint ) {
+			routeObj.endpoints.forEach( ( endpoint ) => {
 				// `endpoint.methods` will be an array of methods like `[ 'GET' ]`: we
 				// only care about GET for this exercise. Validating POST and PUT args
 				// could be useful but is currently deemed to be out-of-scope.
-				endpoint.methods.forEach( function( method ) {
+				endpoint.methods.forEach( ( method ) => {
 					if ( method.toLowerCase() === 'get' ) {
-						Object.keys( endpoint.args ).forEach( function( argKey ) {
+						Object.keys( endpoint.args ).forEach( ( argKey ) => {
 							// Reference param definition objects in the top _getArgs dictionary
 							topLevel._getArgs[ argKey ] = endpoint.args[ argKey ];
 						} );
@@ -139,16 +136,18 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
  * @returns {object} The namespaces dictionary memo object
  */
 function reduceRouteTree( namespaces, routeObj, route ) {
-	var nsForRoute = routeObj.namespace;
+	const nsForRoute = routeObj.namespace;
 
-	// Strip the namespace from the route string (all routes should have the
-	// format `/namespace/other/stuff`) @TODO: Validate this assumption
-	// Also strip any trailing "/?": the slash is already optional and a single
-	// question mark would break the regex parser
-	var routeString = route.replace( '/' + nsForRoute + '/', '' ).replace( /\/\?$/, '' );
+	const routeString = route
+		// Strip the namespace from the route string (all routes should have the
+		// format `/namespace/other/stuff`) @TODO: Validate this assumption
+		.replace( '/' + nsForRoute + '/', '' )
+		// Also strip any trailing "/?": the slash is already optional and a single
+		// question mark would break the regex parser
+		.replace( /\/\?$/, '' );
 
 	// Split the routes up into hierarchical route components
-	var routeComponents = splitPath( routeString );
+	const routeComponents = splitPath( routeString );
 
 	// Do not make a namespace group for the API root
 	// Do not add the namespace root to its own group
@@ -161,12 +160,12 @@ function reduceRouteTree( namespaces, routeObj, route ) {
 	ensure( namespaces, nsForRoute, {} );
 
 	// Get a local reference to namespace object
-	var ns = namespaces[ nsForRoute ];
+	const ns = namespaces[ nsForRoute ];
 
 	// The first element of the route tells us what type of resource this route
 	// is for, e.g. "posts" or "comments": we build one handler per resource
 	// type, so we group like resource paths together.
-	var resource = routeComponents[0];
+	const resource = routeComponents[0];
 
 	// @TODO: This code above currently precludes baseless routes, e.g.
 	// myplugin/v2/(?P<resource>\w+) -- should those be supported?
@@ -175,12 +174,15 @@ function reduceRouteTree( namespaces, routeObj, route ) {
 	// to the namespace object. The array will structure the "levels" (path
 	// components and subresource types) of this resource's endpoint handler.
 	ensure( ns, resource, {} );
-	var levels = ns[ resource ];
+	const levels = ns[ resource ];
 
 	// Recurse through the route components, mutating levels with information about
 	// each child node encountered while walking through the routes tree and what
 	// arguments (parameters) are available for GET requests to this endpoint.
-	routeComponents.reduce( reduceRouteComponents.bind( null, routeObj, levels ), levels );
+	routeComponents.reduce(
+		reduceRouteComponents.bind( null, routeObj, levels ),
+		levels
+	);
 
 	return namespaces;
 }
diff --git a/lib/util/alphanumeric-sort.js b/lib/util/alphanumeric-sort.js
index aa407ab..b0ea561 100644
--- a/lib/util/alphanumeric-sort.js
+++ b/lib/util/alphanumeric-sort.js
@@ -8,7 +8,7 @@
  * @param {String|Number} a The second comparator operand
  * @returns -1 if the values are backwards, 1 if they're ordered, and 0 if they're the same
  */
-function alphaNumericSort( a, b ) {
+module.exports = ( a, b ) => {
 	if ( a > b ) {
 		return 1;
 	}
@@ -16,6 +16,4 @@ function alphaNumericSort( a, b ) {
 		return -1;
 	}
 	return 0;
-}
-
-module.exports = alphaNumericSort;
+};
diff --git a/lib/util/apply-mixin.js b/lib/util/apply-mixin.js
index e9e1830..9bfc387 100644
--- a/lib/util/apply-mixin.js
+++ b/lib/util/apply-mixin.js
@@ -10,7 +10,7 @@
  * @param {Function} mixin The mixin method
  * @returns {void}
  */
-module.exports = function( obj, key, mixin ) {
+module.exports = ( obj, key, mixin ) => {
 	// Will not overwrite existing methods
 	if ( typeof mixin === 'function' && ! obj[ key ] ) {
 		obj[ key ] = mixin;
diff --git a/lib/util/argument-is-numeric.js b/lib/util/argument-is-numeric.js
index 3a4c836..5e17f5a 100644
--- a/lib/util/argument-is-numeric.js
+++ b/lib/util/argument-is-numeric.js
@@ -10,7 +10,7 @@
  * @param {Function} mixin The mixin method
  * @returns {void}
  */
-function argumentIsNumeric( val ) {
+const argumentIsNumeric = ( val ) => {
 	if ( typeof val === 'number' ) {
 		return true;
 	}
@@ -20,7 +20,7 @@ function argumentIsNumeric( val ) {
 	}
 
 	if ( Array.isArray( val ) ) {
-		for ( var i = 0; i < val.length; i++ ) {
+		for ( let i = 0; i < val.length; i++ ) {
 			// Fail early if any argument isn't determined to be numeric
 			if ( ! argumentIsNumeric( val[ i ] ) ) {
 				return false;
@@ -32,6 +32,6 @@ function argumentIsNumeric( val ) {
 	// If it's not an array, and not a string, and not a number, we don't
 	// know what to do with it
 	return false;
-}
+};
 
 module.exports = argumentIsNumeric;
diff --git a/lib/util/check-method-support.js b/lib/util/check-method-support.js
index 559019c..5223fb9 100644
--- a/lib/util/check-method-support.js
+++ b/lib/util/check-method-support.js
@@ -8,7 +8,7 @@
  * @param {WPRequest} request A WPRequest object with a _supportedMethods array
  * @returns true iff the method is within request._supportedMethods
  */
-module.exports = function( method, request ) {
+module.exports = ( method, request ) => {
 	if ( request._supportedMethods.indexOf( method.toLowerCase() ) === -1 ) {
 		throw new Error(
 			'Unsupported method; supported methods are: ' +
diff --git a/lib/util/ensure.js b/lib/util/ensure.js
index 1c38f55..46e08f4 100644
--- a/lib/util/ensure.js
+++ b/lib/util/ensure.js
@@ -10,7 +10,7 @@
  * @param {}       propDefaultValue The default value for the property
  * @returns {void}
  */
-module.exports = function( obj, prop, propDefaultValue ) {
+module.exports = ( obj, prop, propDefaultValue ) => {
 	if ( obj && obj[ prop ] === undefined ) {
 		obj[ prop ] = propDefaultValue;
 	}
diff --git a/lib/util/is-empty-object.js b/lib/util/is-empty-object.js
index 72bb803..fcd9646 100644
--- a/lib/util/is-empty-object.js
+++ b/lib/util/is-empty-object.js
@@ -7,7 +7,7 @@
  * @param {} value A value to test for empty-object-ness
  * @returns {boolean} Whether the provided value is an empty object
  */
-module.exports = function( value ) {
+module.exports = ( value ) => {
 	// If the value is not object-like, then it is certainly not an empty object
 	if ( typeof value !== 'object' ) {
 		return false;
@@ -19,7 +19,7 @@ module.exports = function( value ) {
 		return false;
 	}
 
-	for ( var key in value ) {
+	for ( const key in value ) {
 		if ( value.hasOwnProperty( key ) ) {
 			return false;
 		}
diff --git a/lib/util/key-val-to-obj.js b/lib/util/key-val-to-obj.js
index 0650e79..10d185f 100644
--- a/lib/util/key-val-to-obj.js
+++ b/lib/util/key-val-to-obj.js
@@ -8,8 +8,8 @@
  * @param {}       value The value to assign to the provided key
  * @returns {object} A dictionary object containing the key-value pair
  */
-module.exports = function( key, value ) {
-	var obj = {};
+module.exports = ( key, value ) => {
+	const obj = {};
 	obj[ key ] = value;
 	return obj;
 };
diff --git a/lib/util/log-obj.js b/lib/util/log-obj.js
index 125f288..0b856be 100644
--- a/lib/util/log-obj.js
+++ b/lib/util/log-obj.js
@@ -1,6 +1,6 @@
 'use strict';
 
-var inspect = require( 'util' ).inspect;
+const inspect = require( 'util' ).inspect;
 
 /**
  * Helper method for debugging only: use util.inspect to log a full object
@@ -10,7 +10,7 @@ var inspect = require( 'util' ).inspect;
  * @param {object} obj The object to log
  * @returns {void}
  */
-module.exports = function( obj ) {
+module.exports = ( obj ) => {
 	// eslint-disable-next-line no-console
 	console.log( inspect( obj, {
 		colors: true,
diff --git a/lib/util/named-group-regexp.js b/lib/util/named-group-regexp.js
index deaf84f..0a927dd 100644
--- a/lib/util/named-group-regexp.js
+++ b/lib/util/named-group-regexp.js
@@ -3,7 +3,7 @@
  */
 'use strict';
 
-var pattern = [
+const pattern = [
 	// Capture group start
 	'\\(\\?',
 	// Capture group name begins either `P<`, `<` or `'`
@@ -19,8 +19,6 @@ var pattern = [
 	'\\)',
 ].join( '' );
 
-var namedGroupRE = new RegExp( pattern );
-
 module.exports = {
 	/**
 	 * String representation of the exported Regular Expression; we construct this
@@ -37,5 +35,5 @@ module.exports = {
 	 *
 	 * @prop {RegExp} namedGroupRE
 	 */
-	namedGroupRE: namedGroupRE,
+	namedGroupRE: new RegExp( pattern ),
 };
diff --git a/lib/util/object-reduce.js b/lib/util/object-reduce.js
index 62903a8..83e13ae 100644
--- a/lib/util/object-reduce.js
+++ b/lib/util/object-reduce.js
@@ -19,8 +19,9 @@
  * @param {*} initialState The initial value to pass to the reducer function
  * @returns The result of the reduction operation
  */
-module.exports = function( obj, iterator, initialState ) {
-	return Object.keys( obj ).reduce( function( memo, key ) {
-		return iterator( memo, obj[ key ], key );
-	}, initialState );
-};
+module.exports = ( obj, iterator, initialState ) => Object
+	.keys( obj )
+	.reduce(
+		( memo, key ) => iterator( memo, obj[ key ], key ),
+		initialState
+	);
diff --git a/lib/util/parameter-setter.js b/lib/util/parameter-setter.js
index fecd38f..2dd8a57 100644
--- a/lib/util/parameter-setter.js
+++ b/lib/util/parameter-setter.js
@@ -7,7 +7,7 @@
  * @param {String} param The string key of the parameter this method will set
  * @returns {Function} A setter method that can be assigned to a request instance
  */
-module.exports = function( param ) {
+module.exports = ( param ) => {
 	/**
 	 * A setter for a specific parameter
 	 *
@@ -16,7 +16,6 @@ module.exports = function( param ) {
 	 * @returns The request instance on which this method was called (for chaining)
 	 */
 	return function( val ) {
-		/* jshint validthis:true */
 		return this.param( param, val );
 	};
 };
diff --git a/lib/util/split-path.js b/lib/util/split-path.js
index ae25c5e..42fcf3a 100644
--- a/lib/util/split-path.js
+++ b/lib/util/split-path.js
@@ -3,20 +3,20 @@
  */
 'use strict';
 
-var namedGroupPattern = require( './named-group-regexp' ).pattern;
+const namedGroupPattern = require( './named-group-regexp' ).pattern;
 
 // Convert capture groups to non-matching groups, because all capture groups
 // are included in the resulting array when an RE is passed to `.split()`
 // (We re-use the existing named group's capture pattern instead of creating
 // a new RegExp just for this purpose)
-var patternWithoutSubgroups = namedGroupPattern
+const patternWithoutSubgroups = namedGroupPattern
 	.replace( /([^\\])\(([^?])/g, '$1(?:$2' );
 
 // Make a new RegExp using the same pattern as one single unified capture group,
 // so the match as a whole will be preserved after `.split()`. Permit non-slash
 // characters before or after the named capture group, although those components
 // will not yield functioning setters.
-var namedGroupRE = new RegExp( '([^/]*' + patternWithoutSubgroups + '[^/]*)' );
+const namedGroupRE = new RegExp( '([^/]*' + patternWithoutSubgroups + '[^/]*)' );
 
 /**
  * Divide a route string up into hierarchical components by breaking it apart
@@ -29,13 +29,14 @@ var namedGroupRE = new RegExp( '([^/]*' + patternWithoutSubgroups + '[^/]*)' );
  * @param {String} pathStr A route path string to break into components
  * @returns {String[]} An array of route component strings
  */
-module.exports = function( pathStr ) {
+module.exports = pathStr => pathStr
 	// Divide a string like "/some/path/(?P<with_named_groups>)/etc" into an
 	// array `[ "/some/path/", "(?P<with_named_groups>)", "/etc" ]`.
+	.split( namedGroupRE )
 	// Then, reduce through the array of parts, splitting any non-capture-group
 	// parts on forward slashes and discarding empty strings to create the final
 	// array of path components.
-	return pathStr.split( namedGroupRE ).reduce( function( components, part ) {
+	.reduce( ( components, part ) => {
 		if ( ! part ) {
 			// Ignore empty strings parts
 			return components;
@@ -49,4 +50,3 @@ module.exports = function( pathStr ) {
 		// Split the part on / and filter out empty strings
 		return components.concat( part.split( '/' ).filter( Boolean ) );
 	}, [] );
-};
diff --git a/lib/wp-register-route.js b/lib/wp-register-route.js
index 384db59..7a27497 100644
--- a/lib/wp-register-route.js
+++ b/lib/wp-register-route.js
@@ -1,12 +1,10 @@
 'use strict';
 
-var extend = require( 'node.extend' );
-
-var buildRouteTree = require( './route-tree' ).build;
-var generateEndpointFactories = require( './endpoint-factories' ).generate;
-var paramSetter = require( './util/parameter-setter' );
-var applyMixin = require( './util/apply-mixin' );
-var mixins = require( './mixins' );
+const buildRouteTree = require( './route-tree' ).build;
+const generateEndpointFactories = require( './endpoint-factories' ).generate;
+const paramSetter = require( './util/parameter-setter' );
+const applyMixin = require( './util/apply-mixin' );
+const mixins = require( './mixins' );
 
 /**
  * Create and return a handler for an arbitrary WP REST API endpoint.
@@ -22,16 +20,14 @@ var mixins = require( './mixins' );
  * @param {string[]} [options.methods] An array of methods to whitelist (on the leaf node only)
  * @returns {Function} An endpoint handler factory function for the specified route
  */
-function registerRoute( namespace, restBase, options ) {
+function registerRoute( namespace, restBase, options = {} ) {
 	// Support all methods until requested to do otherwise
-	var supportedMethods = [ 'head', 'get', 'patch', 'put', 'post', 'delete' ];
+	let supportedMethods = [ 'head', 'get', 'patch', 'put', 'post', 'delete' ];
 
-	if ( options && Array.isArray( options.methods ) ) {
+	if ( Array.isArray( options.methods ) ) {
 		// Permit supported methods to be specified as an array
-		supportedMethods = options.methods.map( function( method ) {
-			return method.trim().toLowerCase();
-		} );
-	} else if ( options && typeof options.methods === 'string' ) {
+		supportedMethods = options.methods.map( method => method.trim().toLowerCase() );
+	} else if ( typeof options.methods === 'string' ) {
 		// Permit a supported method to be specified as a string
 		supportedMethods = [ options.methods.trim().toLowerCase() ];
 	}
@@ -43,13 +39,13 @@ function registerRoute( namespace, restBase, options ) {
 		supportedMethods.push( 'get' );
 	}
 
-	var fullRoute = namespace
+	const fullRoute = namespace
 		// Route should always have preceding slash
 		.replace( /^[\s/]*/, '/' )
 		// Route should always be joined to namespace with a single slash
 		.replace( /[\s/]*$/, '/' ) + restBase.replace( /^[\s/]*/, '' );
 
-	var routeObj = {};
+	const routeObj = {};
 	routeObj[ fullRoute ] = {
 		namespace: namespace,
 		methods: supportedMethods,
@@ -57,13 +53,13 @@ function registerRoute( namespace, restBase, options ) {
 
 	// Go through the same steps used to bootstrap the client to parse the
 	// provided route out into a handler request method
-	var routeTree = buildRouteTree( routeObj );
+	const routeTree = buildRouteTree( routeObj );
 	// Parse the mock route object into endpoint factories
-	var endpointFactories = generateEndpointFactories( routeTree )[ namespace ];
-	var EndpointRequest = endpointFactories[ Object.keys( endpointFactories )[ 0 ] ].Ctor;
+	const endpointFactories = generateEndpointFactories( routeTree )[ namespace ];
+	const EndpointRequest = endpointFactories[ Object.keys( endpointFactories )[ 0 ] ].Ctor;
 
 	if ( options && options.params ) {
-		options.params.forEach( function( param ) {
+		options.params.forEach( ( param ) => {
 			// Only accept string parameters
 			if ( typeof param !== 'string' ) {
 				return;
@@ -71,7 +67,7 @@ function registerRoute( namespace, restBase, options ) {
 
 			// If the parameter can be mapped to a mixin, apply that mixin
 			if ( typeof mixins[ param ] === 'object' ) {
-				Object.keys( mixins[ param ] ).forEach( function( key ) {
+				Object.keys( mixins[ param ] ).forEach( ( key ) => {
 					applyMixin( EndpointRequest.prototype, key, mixins[ param ][ key ] );
 				} );
 				return;
@@ -87,16 +83,16 @@ function registerRoute( namespace, restBase, options ) {
 	if ( options && typeof options.mixins === 'object' ) {
 
 		// Set any specified mixin functions on the response
-		Object.keys( options.mixins ).forEach( function( key ) {
+		Object.keys( options.mixins ).forEach( ( key ) => {
 			applyMixin( EndpointRequest.prototype, key, options.mixins[ key ] );
 		} );
 	}
 
-	function endpointFactory( options ) {
-		/* jshint validthis:true */
-		options = options || {};
-		options = extend( options, this && this._options );
-		return new EndpointRequest( options );
+	function endpointFactory( options = {} ) {
+		return new EndpointRequest( {
+			...options,
+			...( this ? this._options : {} ),
+		} );
 	}
 	endpointFactory.Ctor = EndpointRequest;
 
diff --git a/package.json b/package.json
index 2891552..9a00981 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "cms",
     "wordpress"
   ],
+  "browserslist": "last 2 versions, ie 11, not dead",
   "jest": {
     "testMatch": [
       "**/__tests__/**/*.js",
@@ -57,16 +58,18 @@
     "test": "jest --coverage"
   },
   "dependencies": {
-    "es6-promise": "^3.2.1",
     "li": "^1.0.1",
     "lodash.uniq": "^4.3.0",
-    "node.extend": "^1.1.5",
     "parse-link-header": "^0.4.1",
     "qs": "^6.2.0",
     "route-parser": "0.0.4",
     "superagent": "^3.3.1"
   },
   "devDependencies": {
+    "@babel/core": "^7.2.2",
+    "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
+    "@babel/preset-env": "^7.2.3",
+    "babel-loader": "^8.0.4",
     "combyne": "^2.0.0",
     "eslint": "^4.19.1",
     "grunt": "^1.0.1",
@@ -76,7 +79,6 @@
     "istanbul": "^0.4.4",
     "jest": "^23.6.0",
     "jsdoc": "^3.4.3",
-    "json-loader": "^0.5.4",
     "kramed": "^0.5.6",
     "load-grunt-tasks": "^3.5.0",
     "lodash.reduce": "^4.6.0",
@@ -84,6 +86,7 @@
     "minimist": "^1.2.0",
     "prompt": "^1.0.0",
     "rimraf": "^2.6.1",
-    "webpack": "^1.13.1"
+    "webpack": "^4.28.1",
+    "webpack-cli": "^3.1.2"
   }
 }
diff --git a/webpack.config.js b/webpack.config.js
index 1b5c420..63bd472 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,26 +1,45 @@
 'use strict';
 
-const path = require( 'path' );
-const outputPath = path.join( __dirname, 'browser' );
+const { join } = require( 'path' );
 
 module.exports = {
 	entry: './wpapi.js',
 
-	devtool: 'source-map',
+	mode: 'development',
+
+	devtool: 'cheap-module-source-map',
+
+	stats: {
+		all: false,
+		assets: true,
+		colors: true,
+		errors: true,
+		performance: true,
+		timings: true,
+		warnings: true,
+	},
 
 	output: {
-		path: outputPath,
+		path: join( process.cwd(), 'browser' ),
 		filename: 'wpapi.js',
 		library: 'WPAPI',
 		libraryTarget: 'umd',
 	},
 
 	module: {
-		loaders: [
+		rules: [
 			{
-				test: /\.json$/,
-				loader: 'json-loader',
+				test: /\.js$/,
+				exclude: /(node_modules|bower_components)/,
+				loader: require.resolve( 'babel-loader' ),
+				options: {
+					presets: [ '@babel/preset-env' ],
+					plugins: [ '@babel/plugin-proposal-object-rest-spread' ],
+					// Cache compilation results in ./node_modules/.cache/babel-loader/
+					cacheDirectory: true,
+				},
 			},
 		],
 	},
+
 };
diff --git a/webpack.config.minified.js b/webpack.config.minified.js
index c450407..43675b9 100644
--- a/webpack.config.minified.js
+++ b/webpack.config.minified.js
@@ -1,15 +1,21 @@
 'use strict';
 
-const webpack = require( 'webpack' );
 const config = require( './webpack.config' );
 
 // Re-use normal Webpack build config, just adding minification
-config.output.filename = 'wpapi.min.js';
-config.plugins = config.plugins || [];
-config.plugins.push( new webpack.optimize.UglifyJsPlugin( {
-	compress: {
-		warnings: false,
+module.exports = {
+	...config,
+
+	mode: 'production',
+
+	devtool: 'hidden-source-map',
+
+	output: {
+		...config.output,
+		filename: 'wpapi.min.js',
 	},
-} ) );
 
-module.exports = config;
+	optimization: {
+		noEmitOnErrors: true,
+	},
+};
diff --git a/wpapi.js b/wpapi.js
index e1c5c32..602f17e 100644
--- a/wpapi.js
+++ b/wpapi.js
@@ -14,31 +14,30 @@
  */
 'use strict';
 
-var extend = require( 'node.extend' );
-var objectReduce = require( './lib/util/object-reduce' );
+const objectReduce = require( './lib/util/object-reduce' );
 
 // This JSON file provides enough data to create handler methods for all valid
 // API routes in WordPress 4.7
-var defaultRoutes = require( './lib/data/default-routes.json' );
-var buildRouteTree = require( './lib/route-tree' ).build;
-var generateEndpointFactories = require( './lib/endpoint-factories' ).generate;
+const defaultRoutes = require( './lib/data/default-routes.json' );
+const buildRouteTree = require( './lib/route-tree' ).build;
+const generateEndpointFactories = require( './lib/endpoint-factories' ).generate;
 
 // The default endpoint factories will be lazy-loaded by parsing the default
 // route tree data if a default-mode WPAPI instance is created (i.e. one that
 // is to be bootstrapped with the handlers for all of the built-in routes)
-var defaultEndpointFactories;
+let defaultEndpointFactories;
 
 // Constant used to detect first-party WordPress REST API routes
-var apiDefaultNamespace = 'wp/v2';
+const apiDefaultNamespace = 'wp/v2';
 
 // Pull in autodiscovery methods
-var autodiscovery = require( './lib/autodiscovery' );
+const autodiscovery = require( './lib/autodiscovery' );
 
 // Pull in base module constructors
-var WPRequest = require( './lib/constructors/wp-request' );
+const WPRequest = require( './lib/constructors/wp-request' );
 
 // Pull in default HTTP transport
-var httpTransport = require( './lib/http-transport' );
+const httpTransport = require( './lib/http-transport' );
 
 /**
  * Construct a REST API client instance object to create
@@ -146,7 +145,7 @@ function WPAPI( options ) {
  */
 WPAPI.prototype.transport = function( transport ) {
 	// Local reference to avoid need to reference via `this` inside forEach
-	var _options = this._options;
+	const _options = this._options;
 
 	// Create the default transport if it does not exist
 	if ( ! _options.transport ) {
@@ -154,7 +153,7 @@ WPAPI.prototype.transport = function( transport ) {
 	}
 
 	// Whitelist the methods that may be applied
-	[ 'get', 'head', 'post', 'put', 'delete' ].forEach( function( key ) {
+	[ 'get', 'head', 'post', 'put', 'delete' ].forEach( ( key ) => {
 		if ( transport && transport[ key ] ) {
 			_options.transport[ key ] = transport[ key ];
 		}
@@ -232,10 +231,10 @@ WPAPI.site = function( endpoint, routes ) {
  * @returns {WPRequest} A WPRequest object bound to the provided URL
  */
 WPAPI.prototype.url = function( url ) {
-	var options = extend( {}, this._options, {
+	return new WPRequest( {
+		...this._options,
 		endpoint: url,
 	} );
-	return new WPRequest( options );
 };
 
 /**
@@ -248,9 +247,11 @@ WPAPI.prototype.url = function( url ) {
  */
 WPAPI.prototype.root = function( relativePath ) {
 	relativePath = relativePath || '';
-	var options = extend( {}, this._options );
+	const options = {
+		...this._options,
+	};
 	// Request should be
-	var request = new WPRequest( options );
+	const request = new WPRequest( options );
 
 	// Set the path template to the string passed in
 	request._path = { '0': relativePath };
@@ -334,8 +335,8 @@ WPAPI.prototype.registerRoute = require( './lib/wp-register-route' );
  * @returns {WPAPI} The bootstrapped WPAPI client instance (for chaining or assignment)
  */
 WPAPI.prototype.bootstrap = function( routes ) {
-	var routesByNamespace;
-	var endpointFactoriesByNamespace;
+	let routesByNamespace;
+	let endpointFactoriesByNamespace;
 
 	if ( ! routes ) {
 		// Auto-generate default endpoint factories if they are not already available
@@ -355,17 +356,21 @@ WPAPI.prototype.bootstrap = function( routes ) {
 	// client instance and passing a registered namespace string.
 	// Handlers for default (wp/v2) routes will also be assigned to the WPAPI
 	// client instance object itself, for brevity.
-	return objectReduce( endpointFactoriesByNamespace, function( wpInstance, endpointFactories, namespace ) {
+	return objectReduce( endpointFactoriesByNamespace, ( wpInstance, endpointFactories, namespace ) => {
 
 		// Set (or augment) the route handler factories for this namespace.
-		wpInstance._ns[ namespace ] = objectReduce( endpointFactories, function( nsHandlers, handlerFn, methodName ) {
-			nsHandlers[ methodName ] = handlerFn;
-			return nsHandlers;
-		}, wpInstance._ns[ namespace ] || {
-			// Create all namespace dictionaries with a direct reference to the main WPAPI
-			// instance's _options property so that things like auth propagate properly
-			_options: wpInstance._options,
-		} );
+		wpInstance._ns[ namespace ] = objectReduce(
+			endpointFactories,
+			( nsHandlers, handlerFn, methodName ) => {
+				nsHandlers[ methodName ] = handlerFn;
+				return nsHandlers;
+			},
+			wpInstance._ns[ namespace ] || {
+				// Create all namespace dictionaries with a direct reference to the main WPAPI
+				// instance's _options property so that things like auth propagate properly
+				_options: wpInstance._options,
+			}
+		);
 
 		// For the default namespace, e.g. "wp/v2" at the time this comment was
 		// written, ensure all methods are assigned to the root client object itself
@@ -373,7 +378,7 @@ WPAPI.prototype.bootstrap = function( routes ) {
 		// methods can be called with e.g. `wp.posts()` and not the more verbose
 		// `wp.namespace( 'wp/v2' ).posts()`.
 		if ( namespace === apiDefaultNamespace ) {
-			Object.keys( wpInstance._ns[ namespace ] ).forEach( function( methodName ) {
+			Object.keys( wpInstance._ns[ namespace ] ).forEach( ( methodName ) => {
 				wpInstance[ methodName ] = wpInstance._ns[ namespace ][ methodName ];
 			} );
 		}
@@ -418,15 +423,15 @@ WPAPI.prototype.namespace = function( namespace ) {
  * to the deduced endpoint, or rejected if an endpoint is not found or the
  * library is unable to parse the provided endpoint.
  */
-WPAPI.discover = function( url ) {
+WPAPI.discover = ( url ) => {
 	// local placeholder for API root URL
-	var endpoint;
+	let endpoint;
 
 	// Try HEAD request first, for smaller payload: use WPAPI.site to produce
 	// a request that utilizes the defined HTTP transports
-	var req = WPAPI.site( url ).root();
+	const req = WPAPI.site( url ).root();
 	return req.headers()
-		.catch( function() {
+		.catch( () => {
 			// On the hypothesis that any error here is related to the HEAD request
 			// failing, provisionally try again using GET because that method is
 			// more widely supported
@@ -434,7 +439,7 @@ WPAPI.discover = function( url ) {
 		} )
 		// Inspect response to find API location header
 		.then( autodiscovery.locateAPIRootHeader )
-		.then( function( apiRootURL ) {
+		.then( ( apiRootURL ) => {
 			// Set the function-scope variable that will be used to instantiate
 			// the bound WPAPI instance,
 			endpoint = apiRootURL;
@@ -442,14 +447,14 @@ WPAPI.discover = function( url ) {
 			// then GET the API root JSON object
 			return WPAPI.site( apiRootURL ).root().get();
 		} )
-		.then( function( apiRootJSON ) {
+		.then( ( apiRootJSON ) => {
 			// Instantiate & bootstrap with the discovered methods
 			return new WPAPI( {
 				endpoint: endpoint,
 				routes: apiRootJSON.routes,
 			} );
 		} )
-		.catch( function( err ) {
+		.catch( ( err ) => {
 			/* eslint-disable no-console */
 			console.error( err );
 			if ( endpoint ) {


Test patch:
diff --git a/tests/integration/autodiscovery.js b/tests/integration/autodiscovery.js
index a873ad8..7505fe9 100644
--- a/tests/integration/autodiscovery.js
+++ b/tests/integration/autodiscovery.js
@@ -30,7 +30,6 @@ describe( 'integration: discover()', () => {
 	} );
 
 	it( 'returns a promise', () => {
-		const Promise = require( 'es6-promise' );
 		expect( apiPromise ).toBeInstanceOf( Promise );
 	} );
 
diff --git a/tests/integration/comments.js b/tests/integration/comments.js
index 2e2755c..9330fe5 100644
--- a/tests/integration/comments.js
+++ b/tests/integration/comments.js
@@ -1,4 +1,3 @@
-/*jshint -W106 */// Disable underscore_case warnings in this file b/c WP uses them
 'use strict';
 
 const WPAPI = require( '../../' );
diff --git a/tests/integration/custom-http-transport.js b/tests/integration/custom-http-transport.js
index 3993274..aa482d8 100644
--- a/tests/integration/custom-http-transport.js
+++ b/tests/integration/custom-http-transport.js
@@ -51,9 +51,6 @@ describe( 'integration: custom HTTP transport methods', () => {
 	} );
 
 	it( 'can be defined to e.g. use a cache when available', () => {
-		let query1;
-		let query2;
-
 		wp = new WPAPI( {
 			endpoint: 'http://wpapi.local/wp-json',
 			transport: {
@@ -61,7 +58,9 @@ describe( 'integration: custom HTTP transport methods', () => {
 			},
 		} ).auth( credentials );
 
-		query1 = wp.posts().id( id );
+		const query1 = wp.posts().id( id );
+		let query2;
+
 		const prom = query1
 			.get()
 			.then( ( result ) => {
@@ -103,7 +102,6 @@ describe( 'integration: custom HTTP transport methods', () => {
 				// If .slug is used, auto-unwrap the returned array
 				get( wpreq, cb ) {
 					if ( ! wpreq._params.slug ) {
-						/* jshint validthis:true */
 						return WPAPI.transport.get.call( this, wpreq, cb );
 					}
 					return WPAPI.transport.get( wpreq ).then( ( results ) => {
@@ -143,7 +141,6 @@ describe( 'integration: custom HTTP transport methods', () => {
 			transport: {
 				// Add collection helper methods to the returned arrays
 				get( wpreq, cb ) {
-					/* jshint validthis:true */
 					return WPAPI.transport.get.call( this, wpreq, cb ).then( ( results ) => {
 						if ( Array.isArray( results ) ) {
 							return new Collection( results );
diff --git a/tests/integration/posts.js b/tests/integration/posts.js
index 51b6665..4013057 100644
--- a/tests/integration/posts.js
+++ b/tests/integration/posts.js
@@ -817,7 +817,7 @@ describe( 'integration: posts()', () => {
 
 			request( cb ) {
 				const self = this;
-				wp.posts().get( function( err, data ) {
+				wp.posts().get( ( err, data ) => {
 					expect( err ).toBeNull();
 
 					// Context is maintained
@@ -832,7 +832,7 @@ describe( 'integration: posts()', () => {
 					expect( Array.isArray( this.state.data ) ).toBe( true );
 					expect( this.state.data.length ).toBe( 10 );
 					cb();
-				}.bind( this ) );
+				} );
 			}
 		}
 		( new Ctor() ).request( done );
diff --git a/tests/unit/build/scripts/simplify-object.js b/tests/unit/build/scripts/simplify-object.js
index 54432ac..f6723d0 100644
--- a/tests/unit/build/scripts/simplify-object.js
+++ b/tests/unit/build/scripts/simplify-object.js
@@ -109,24 +109,7 @@ describe( 'simplifyObject', () => {
 				},
 			}, {
 				methods: [ 'POST' ],
-				args: {
-					date: {},
-					date_gmt: {},
-					password: {},
-					slug: {},
-					status: {},
-					title: {},
-					content: {},
-					author: {},
-					excerpt: {},
-					featured_media: {},
-					comment_status: {},
-					ping_status: {},
-					format: {},
-					sticky: {},
-					categories: {},
-					tags: {},
-				},
+				args: {},
 			} ],
 		} );
 	} );
diff --git a/tests/unit/lib/route-tree.js b/tests/unit/lib/route-tree.js
index a0572dc..6d9b062 100644
--- a/tests/unit/lib/route-tree.js
+++ b/tests/unit/lib/route-tree.js
@@ -20,15 +20,30 @@ describe( 'route-tree utility', () => {
 
 		it( 'includes objects for all default wp/v2 routes', () => {
 			const routes = Object.keys( tree[ 'wp/v2' ] ).sort();
-			expect( routes.length ).toBe( 11 );
-			expect( routes.join( ',' ) )
-				.toBe( 'categories,comments,media,pages,posts,settings,statuses,tags,taxonomies,types,users' );
+			expect( routes.length ).toBe( 15 );
+			expect( routes.sort() ).toEqual( [
+				'block-renderer',
+				'blocks',
+				'categories',
+				'comments',
+				'media',
+				'pages',
+				'posts',
+				'search',
+				'settings',
+				'statuses',
+				'tags',
+				'taxonomies',
+				'themes',
+				'types',
+				'users',
+			] );
 		} );
 
 		it( 'includes objects for all default oembed/1.0 routes', () => {
 			const routes = Object.keys( tree[ 'oembed/1.0' ] ).sort();
-			expect( routes.length ).toBe( 1 );
-			expect( routes.join( ',' ) ).toBe( 'embed' );
+			expect( routes.length ).toBe( 2 );
+			expect( routes.sort().join( ',' ) ).toBe( 'embed,proxy' );
 		} );
 
 		// Inspect the .posts tree as a smoke test for whether parsing the API
@@ -58,10 +73,12 @@ describe( 'route-tree utility', () => {
 					author_exclude: {},
 					before: {},
 					exclude: {},
+					id: {},
 					include: {},
 					offset: {},
 					order: {},
 					orderby: {},
+					parent: {},
 					password: {},
 					slug: {},
 					status: {},
diff --git a/tests/unit/wpapi.js b/tests/unit/wpapi.js
index 777ad7f..5b2d888 100644
--- a/tests/unit/wpapi.js
+++ b/tests/unit/wpapi.js
@@ -588,12 +588,16 @@ describe( 'WPAPI', () => {
 			} );
 
 			it( 'does not impact or overwrite unspecified transport methods', () => {
-				const originalMethods = Object.assign( {}, site._options.transport );
+				const originalMethods = {
+					...site._options.transport,
+				};
 				site.transport( {
 					get() {},
 					put() {},
 				} );
-				const newMethods = Object.assign( {}, site._options.transport );
+				const newMethods = {
+					...site._options.transport,
+				};
 				expect( newMethods.delete ).toBe( originalMethods.delete );
 				expect( newMethods.post ).toBe( originalMethods.post );