Create your own super scaffolding — automatically add custom code

eddyy stop
8 min readDec 10, 2018

--

You can automatically scaffold new apps customized to your particular needs by using the feathers-plus/cli pipeline (cli+). Your developers can more quickly be productive, with less of a learning curve.

A previous article described how cli+ can generate your customized boilerplate. This article explains how to automatically add custom code to that boilerplate.

FeathersJS is an open source GraphQL, REST and realtime API layer for modern applications. It has 10k GitHub stars; it’s one of the 8 back-end frameworks selected by the State of JavaScript survey in 2017 and one of the 6 in 2018.

Feathers’ cli+ is a round trip generator, meaning you can still use it to make scaffolding additions and changes even after you’ve made custom changes to your app. I’m one of the people maintaining it.

Different strokes for different folks

Let’s say you specialize in developing apps for clients who need video training courses for their offerings. Or perhaps you specialize in geolocation apps.

The video training apps will have different services, models and hooks than the geolocation ones. But all the video training apps will resemble one another, as will the geolocation ones.

You can describe the scaffolding for a typical video training app — its authentication, services, hooks, models, fake data, tests — and use the cli+ pipeline to automatically generate an app with all these features already in place. Your developers have less of a learning curve and they are more productive because they can start working right away on the peculiarities of the project.

You can also describe scaffolding for geolocation apps and, if you specialize in developing both of these, you generate using one scaffold or the other.

All your specs are known to me

We’ve previously discussed how the feathers-gen-specs.json file (a.k.a. specs.json) contains a description of your app, and how cli+ generates all the boilerplate for your app from it.

All your app’s boilerplate can be generated from feathers-gen-specs.json.

The design of cli+ is based on the cli+ prompts being written to specs.json, while generation of the boilerplate itself is based only on the contents of that file. So provide specs.json and a feathers-plus generate all command will produce all your boilerplate.

Examples of scaffolds

I’d like to show you some examples of specs.json for different industries and different types of apps, but we run into a problem. The examples I’ve seen or did consulting for are proprietary. Software developers and clients consider them as providing a competitive advantage and are unwilling to share them.

However here are some specs.json files related to cli+ development:

  • The basic scaffolding for many of the cli+ tests. It contains authentication, three services and a GraphQL endpoint. Other instances of this scaffolding use different adapters (MongoDB, Mongoose, Sequelize), generate JavaScript or TypeScript, and add different types of tests. 26 cli+ tests have been generated based on this scaffolding.
  • The schema below is used in feathers-plus/cli-generator-example, a repo containing reference implementations for the cli+ GraphQL endpoint. I’ve also used the schema in several articles. Instances of this scaffolding are used 6 times in feathers-plus/cli-generator-example and a few times in my articles.
A schema often used with cli+.

The scaffolding is just a folder containing a specs.json. (It’ll likely also contain custom code blocks, something we’ll talk about below.) Its straight forward to create an app based on a scaffold.

  • Copy the scaffolding folder to the new app’s folder.
  • Run feathers-plus generate all.
  • Start customizing the app by running other feathers-plus commands.
  • Continue changing existing or adding new custom code.

I‘m nothing without you, data model

The specs.json file specifies what services our scaffold needs. However the scaffold will only be useful once we define the data model for each service. This means we have to include code blocks in our scaffold as cli+ models are defined using code blocks.

You’ve seen code blocks throughout the cli+ generated code. They start with // !code: and end with // !end. Here’s module src/index.js

/* Module src/index.js *//* eslint-disable no-console */
// Start the server. (Can be re-generated.)
const logger = require('./logger');
const app = require('./app');
// !code: imports
const startup = require('./startup);
// !end
// !code: init // !end


const port = app.get('port');
const server = app.listen(port);
// !code: init2 // !end

process.on('unhandledRejection', (reason, p) => {
// !<DEFAULT> code: unhandled_rejection_log
logger.error('Unhandled Rejection at: Promise ', p, reason);
// !end
// !code: unhandled_rejection // !end

});

server.on('listening', async () => {
// !<DEFAULT> code: listening_log
logger.info('Started on http://%s:%d', app.get('host'), port);
// !end
// !code: listening // !end
// !code: listening1

await startup();
// !end
});

// !code: funcs // !end
// !code: end // !end

We are not interested in code blocks starting with // !<DEFAULT> code: because they contain code cli+ will regenerate anyway. We are also not interested in empty code blocks, which look like // !code: ... // !end. We are not interested in code outside code blocks as cli+ will also regenerate them.

So cli+ will regenerate exactly this module given the two code blocks below, as they are the only customizations in the module.

// !code: imports
const startup = require('./startup);
// !end
// !code: listening1
await startup();
// !end

Of more interest to us are modules which contain the data model for a service like this src/services/users/users.schema.js

// Define the Feathers schema for service `users`. (Can be re-generated.)
// !code: imports // !end
// !code: init // !end


// Define the model using JSON-schema
let schema = {
// !<DEFAULT> code: schema_header
title: 'Users',
description: 'Users database.',
// !end
// !code: schema_definitions // !end


// Required fields.
required: [
// !code: schema_required
'uuid',
'email',
'firstName',
'lastName'
// !end
],
// Fields with unique values.
uniqueItemProperties: [
// !code: schema_unique // !end
],

// Fields in the model.
properties: {
// !code: schema_properties
id: { type: 'ID' },
_id: { type: 'ID' },
uuid: { type: 'integer' },
email: {},
firstName: {},
lastName: {},
// !end
},
// !code: schema_more // !end
};

// Define optional, non-JSON-schema extensions.
let extensions = {
// GraphQL generation.
graphql: {
// !code: graphql_header
name: 'User',
service: {
sort: { uuid: 1 },
},
// !end
discard: [
// !code: graphql_discard // !end
],
add: {
// !code: graphql_add
comments: { type: '[Comment!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
followed_by: { type: '[Relationship!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'followeeUuid' } },
following: { type: '[Relationship!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'followerUuid' } },
fullName: { type: 'String!', args: false },
likes: { type: '[Like!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
posts: { type: '[Post!]' , relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
// !end
},
// !code: graphql_more // !end
},
};

// !code: more // !end

let moduleExports = {
schema,
extensions,
// !code: moduleExports // !end
};

// !code: exports // !end
module.exports = moduleExports;

// !code: funcs // !end
// !code: end // !end

Cli+ can exactly regenerate this module with

// !code: schema_required
'uuid',
'email',
'firstName',
'lastName'
// !end

// !code: schema_properties
id: { type: 'ID' },
_id: { type: 'ID' },
uuid: { type: 'integer' },
email: {},
firstName: {},
lastName: {},
// !end

// !code: graphql_header
name: 'User',
service: {
sort: { uuid: 1 },
},
// !end

// !code: graphql_add
comments: { type: '[Comment!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
followed_by: { type: '[Relationship!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'followeeUuid' } },
following: { type: '[Relationship!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'followerUuid' } },
fullName: { type: 'String!', args: false },
likes: { type: '[Like!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
posts: { type: '[Post!]' , relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
// !end

The simple way to include code blocks

You can include files in your scaffolding folder which contain custom code. Those files need contain just the necessary code blocks as described above. The scaffolding folder may also contain other needed modules.

The scaffolding folder for the above example would look like

A scaffolding folder with code blocks and a referenced module.

The app can be exactly regenerated by cli+ with this information.

Another way to include code blocks

Its also possible to include code blocks in the feathers-gen-code.js module (or its .ts equivalent). The // !module lines indicate which module the following code blocks are for.

// !module src/index.js

// !code: imports
const startup = require('./startup);
// !end

// !code: listening1
await startup();
// !end

// !module src/services/users/users.schema.js

// !code: schema_required
'uuid',
'email',
'firstName',
'lastName'
// !end

// !code: schema_properties
id: { type: 'ID' },
_id: { type: 'ID' },
uuid: { type: 'integer' },
email: {},
firstName: {},
lastName: {},
// !end

// !code: graphql_header
name: 'User',
service: {
sort: { uuid: 1 },
},
// !end

// !code: graphql_add
comments: { type: '[Comment!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
followed_by: { type: '[Relationship!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'followeeUuid' } },
following: { type: '[Relationship!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'followerUuid' } },
fullName: { type: 'String!', args: false },
likes: { type: '[Like!]', args: false, relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
posts: { type: '[Post!]' , relation:
{ ourTable: 'uuid', otherTable: 'authorUuid' } },
// !end

This approach is more software friendly as only one file has to be read or written. You can get a display of all the code blocks in an app, in this format, by running feathers-plus generate codelist.

The scaffolding folder for the above example would look like

A scaffolding folder consolidating its code blocks.

The feathers-gen-code.js file should be deleted once the app is generated. No damage will occur if its not, though you will keep seeing messages as cli+ finds custom blocks in the generated modules which now duplicate those in feathers-gen-code.js.

Using generator version 0.6.61
. Found replacement custom code for location 'imports'
in src/index.**
. Found replacement custom code for location 'listening1'
in src/index.**
. Found replacement custom code for location 'schema_required'
in src/services/users/users.schema.js
. Found replacement custom code for location 'schema_properies'
in src/services/users/users.schema.js
. Found replacement custom code for location 'graphql_header'
in src/services/users/users.schema.js
. Found replacement custom code for location 'graphql_add'
in src/services/users/users.schema.js
Source scan took 0s 38ms

Can our flexibility be improved?

The feathers-gen-specs.json contains a normal JSON object, while feathers-gen-code.js is a text file. Both can be easily manipulated with JavaScript. You could write a Node program which asks some questions and modifies both files, thus customizing your scaffolding for the current needs.

Making your scaffolding flexible.

In conclusion

This article is part of a series of articles on writing your own app generator by leveraging feathers-plus/cli. In the next article we’ll discuss how to use Yeoman to make your scaffolding flexible.

So we’re not done yet. Subscribe to The Feathers Flightpath publication to be informed of the coming articles.

As always, feel free to join Feathers Slack to join the discussion or just lurk around.

--

--