How to Quickly Generate Microservices with Yeoman

Written by iksena | Published 2021/01/22
Tech Story Tags: microservices | javascript | nodejs | typescript | expressjs | yeoman | code-generator | boilerplate | web-monetization

TLDR Yeoman is a Node.js module that will help you kickstart new projects with any tech stacks. It is a code generator to reduce repetition, so engineers can focus only on developing a microservice's business logic. Yeoman will automatically detect the main generator and sub-generators if you follow the folder structures below. Each task will run in sequence according to the order in which it is written. You can name your methods anything, or you can use the reserved Yeoman method names to keep task priority in sequence.via the TL;DR App

Many popular projects and companies have greatly implemented Microservices architecture. One of its benefits is that we can create a service with a single responsibility for specific business logic. So whenever the business has a new feature, we can create a new service loosely coupled with other microservices.
Another benefit of microservices is that we can deploy them independently. It means we can use whatever languages or frameworks that a service sees fit. But in practice, companies usually define a default language/framework for their microservices. This strategy enables engineers to build a service quickly, and it is useful to increase productivity and maintainability with limited engineer resources.
When the need for new services is high, engineers will have to write many boilerplate codes of the same framework. Therefore, we can create a code generator to reduce repetition, so engineers can focus only on developing a microservice's business logic. We can also establish a code convention by defining the folder structure and design pattern within its template and workflow with a code generator.

Getting started with Yeoman

Yeoman is a scaffolding tool that will help you kickstart new projects with any tech stacks. It is a Node.js module, and you can install it from npm:
npm install -g yo
After you’ve installed it, you can run Yeoman by the command yo and it will list all the globally installed generators. For example, you can try a generator I have built to generate a microservice with Express.js and Typescript by cloning the repo here, then run
npm link
in the directory,
Now you can run the generator with
yo ts-express
. And the prompts below will be shown to ask for your customization.
The process will then continue to write boilerplate files from templates and installing dependencies until the message below is displayed.

Creating a new generator

A Yeoman generator is simply a Node.js module. To create a generator, you have to create a new node project by running
npm init
and set the name with
generator-
prefix (e.g.
generator-microservice
).
A generator requires Yeoman dependency, which you can install by running
npm install yeoman-generator
. Yeoman will automatically detect the main generator and sub-generators if you follow the folder structures below.
├───package.json
└───generators/
    ├───app/
    │   └───index.js
    └───route/
        └───index.js
  • package.json
    is the NPM manifest file for the generator project.
  • generators/app/index.js 
    is where the main generator workflow is written. It can be executed by running
    yo microservice
    .
  • generators/route/index.js
    is where the sub-generator workflow is written. It can be executed by running
    yo microservice:route
    .
In the generator, you should create a class that extends the Yeoman base generator. Then we can define the tasks by creating methods inside the class.
Each task will run in sequence according to the order in which it is written. You can name your methods anything, or you can follow the reserved Yeoman method names.
Yeoman uses these method names to keep task priority in sequence; the names in order are
initializing
,
prompting
,
configuring
,
default
,
writing
,
conflicts
,
install
, and
end
. If your method names don’t match the reserved names, they will run under the
default
task priority.
💡 Pro Tip: Name a method with underscore prefix (
_helper_method
) to create a helper method which won’t be listed as a task

Interacting with users

The Yeoman generator we are building is a CLI app, so users should interact or define their customization via terminal. Yeoman provides three approaches for user interaction; they are:
  • Arguments (
    yo microservice [argument]
    )
    Arguments are passed directly after the generator command. To receive an argument, we must call
    this.argument(<name>, <options>)
    in the class constructor. Then we can get the data from
    this.options[name]
    .
  • Options (
    yo microservice --[option]
    )
    Options are passed as command-line flags. Similar to arguments, we must call
    this.option(<name>, <options>)
    in the constructor. Then it will be accessible from
    this.options[name]
    .
  • Prompts
    Prompts are a series of questions asked to the user in sequence. It is the most recommended approach for user interaction as it has a better user experience and supports a conditional question. It also supports multiple question types like radio, checkbox, etc., as it is based on Inquirer.js. We can call
    this.prompt()
    which receives an array of prompts as an argument. It is asynchronous, and we should call it in the
    prompting
    task. When the user has finished answering the prompts, the method will return the answers object.
Here is an example of using argument, option, and prompt methods to receive the
name
of the microservice.
const Generator = require('yeoman-generator');

class MicroserviceGenerator extends Generator {
  constructor(args, opts) {
    super(args, opts);

    // yo microservice Example
    this.argument('name', {
      type: String,
      description: 'Microservice Name',
      required: true,
      default: 'Example',
    });
    console.log('Name as argument', this.options.name);

    // yo microservice --another-name=Example
    // yo microservice -n=Example
    this.option('anotherName', {
      alias: 'n',
      type: String,
      description: 'Microservice Name',
      default: 'Example',
    });
    console.log('Name as option', this.options.anotherName);
  }

  async prompting() {
    // > yo microservice
    // ? What is the name of the microservice? (Example)
    this.answers = await this.prompt([
      {
        name: 'name',
        type: 'input',
        default: 'Example',
        message: 'What is the name of the microservice?',
      },
    ]);
    console.log('Name as prompt', this.answers.name);
  }
}

module.exports = MicroserviceGenerator
💡 Pro Tip: To give better experience for the user, prompts can display multiple types of question, conditional question, and answer validation. You can follow the documentation here.

Configuring user customization data

After we received the user’s customization data, we might need to process and configure them before using them further in other tasks. Common tasks to be handled in the configuration step:
  • Create new variables based on user data to be passed to template files.
  • Combine user data from arguments, options, and answers.
  • Save user answers and config data in Yeoman config file (
    .yo-rc.json
    ) to be used for future execution or from other generators/sub-generators.
We can code these tasks in the
configuring
method like below.
const Generator = require('yeoman-generator');

class MicroserviceGenerator extends Generator {
  // ... other methods

  configuring() {
    // creating new variables
    const { name } = this.options;
    const title = `ms-${name.toLowerCase()}`; // will output as "ms-example"

    // combining user data
    this.answers = {
      ...this.config.getAll(), // getting saved config data
      ...this.answers,
      name,
      title,
    };

    // saving to config file
    this.config.set(this.answers);
  }
}

module.exports = MicroserviceGenerator
With this method, a 
.yo-rc.json
file will be generated inside the project, which will look like this.
{
  "generator-microservice": {
    "name": "Example",
    "title": "ms-example"
  }
}

Creating template files

In our generator project, we can put template files inside a generator or sub-generator templates directory like below.
└───generators/
    ├───app/
    │   ├───templates/
    │   │   ├───index.js
    │   │   └───package.json
    │   └───index.js
A template file can be any file with any extension. Besides boilerplate codes, we can also put project config and manifest files as templates.
Yeoman uses EJS for its templating syntax. With EJS, we can compile a JS code in a template. So we can pass variables or have conditional writing in the template. Here is an example of EJS usage in a 
.env
file template, which is a microservice environment variables file.
# ------------------------------------------------------------------
# General
# ------------------------------------------------------------------
SERVICE_NAME=<%= name %>
PORT=3000
LOG_LEVEL=debug

<%_ if (hasDb) { _%>
# ------------------------------------------------------------------
# Database
# ------------------------------------------------------------------
DATABASE_HOST=<%= dbHost %>
DATABASE_PORT=<%= dbPort %>
DATABASE_NAME=<%= dbName %>
DATABASE_USERNAME=<%= dbUser %>
DATABASE_PASSWORD=<%= dbPassword %>
<%_ } _%>
In the file above, we can easily write a value from a variable with EJS syntax
<%=
. We can also see that the database config block (line 9–16) will only be written when
hasDb
variable resolve to
true
.
💡 Pro Tip: We can use
<%_ _%>
instead of
<% 
%>
to clear out whitespace (empty line) around it.

Generating boilerplate from templates

For this step, we can finally generate the microservice boilerplate from our template files. We only need to copy the files from the templates directory into the target directory while passing variables that we have received and configured from the user. To do this, we can code inside the
writing
task priority like below.
const Generator = require('yeoman-generator');

class MicroserviceGenerator extends Generator {
  // ...other tasks

  writing() {
    const templates = [
      '.dockerignore',
      '.env',
      '.eslintrc.yml',
      'Dockerfile',
      'package.json',
      'tsconfig.json',
      'tslint.json',
      'config/index.ts',
      'src/index.ts',
      'src/routes/index.ts',
    ];

    templates.forEach((filePath) => {
      this.fs.copyTpl(
        this.templatePath(filePath),
        this.destinationPath(filePath),
        this.answers,
      );
    });
  }
}

module.exports = MicroserviceGenerator
As we can see, we are using Yeoman’s built-in method to copy template files
this.fs.copyTpl
that receive three params: source path, target path, and template variables. Yeoman also provide path resolver functions to get the current generator’s templates path (
this.templatePath
) and its target path (
this.destinationPath
).

Setting up the generated microservice

After generating the microservice boilerplate project, we can install the dependencies needed to start running in this step. From what I learned, we have two different approaches to manage dependencies of the generated microservice,
  1. Install dependencies with exact versions pre-defined by the generator. With this approach, we can create the
    package.json
    file as a template where all the dependencies are defined. So we will only call
    this.npmInstall()
    in the
    install
    task.
  2. Install dependencies with the latest versions every time the generator runs. To do this, we can pass a list of dependency names to be passed to
    this.npmInstall
    function as a param.
Using the second approach, you can see the example of implementing it in the
install
task below.
const Generator = require('yeoman-generator');

class MicroserviceGenerator extends Generator {
  // ...other tasks

  install() {
    const devDependencies = [
      '@types/cors',
      '@types/express',
      '@types/jest',
      '@typescript-eslint/eslint-plugin',
      '@typescript-eslint/parser',
      'eslint',
      'eslint-config-airbnb-base',
      'eslint-plugin-import',
      'jest',
      'ts-jest',
      'ts-node',
      'typescript',
    ];
    const dependencies = [
      'body-parser',
      'cors',
      'dotenv',
      'express',
      'joi',
    ];

    this.npmInstall(devDependencies, {'save-dev': true});
    this.npmInstall(dependencies);
  }

  end() {
    this.log('Your microservice is ready!');
  }
}

module.exports = MicroserviceGenerator;
With the example above, we can differentiate dependencies into dev dependencies to be installed separately. We can also code the end task to print a message to the user indicating the generator process has successfully ended.

Creating and composing a sub-generator

Now that we have finished creating the main microservice generator, we can create a sub-generator. It is useful to generate an app component in an existing project like routes, data model, service, etc.
We can also create a sub-generator to make the generator more modularized, as it can be composed with the main generator. Here is an example of a sub-generator module for creating a route/endpoint in a microservice.
const Generator = require('yeoman-generator');

class RouteGenerator extends Generator {
  async prompting() {
    this.log('--- Generate a route ---');

    this.answers = await this.prompt([
      {
        name: 'routeName',
        type: 'input',
        default: 'healthcheck',
        message: 'What is the name of the route?',
      },
      {
        name: 'routeMethod',
        type: 'list',
        choices: ['get', 'post', 'delete', 'patch', 'put'],
        default: 'get',
        message: 'What is the method of the route?',
      },
    ]);
  }

  configuring() {
    const { routeMethod, routeName } = this.answers;
    const routeTitle = `${routeMethod.toUpperCase()} /${routeName}`;

    this.answers = {
      ...this.answers,
      routeTitle,
    };
    this.config.set(this.answers);
  }

  _writeCode() {
    const { routeMethod, routeName } = this.answers;

    this.fs.copyTpl(
      this.templatePath('src/routes/name/index.ts'),
      this.destinationPath(`src/routes/${routeName}/index.ts`),
      this.answers,
    );

    this.fs.copyTpl(
      this.templatePath('src/routes/name/method/index.ts'),
      this.destinationPath(`src/routes/${routeName}/${routeMethod}/index.ts`),
      this.answers,
    );

    this.fs.copyTpl(
      this.templatePath('src/routes/name/method/handler.ts'),
      this.destinationPath(`src/routes/${routeName}/${routeMethod}/handler.ts`),
      this.answers,
    );
  }

  _writeTest() {
    const { routeMethod, routeName } = this.answers;

    this.fs.copyTpl(
      this.templatePath('test/routes/name/method/handler.test.ts'),
      this.destinationPath(`test/routes/${routeName}/${routeMethod}/handler.test.ts`),
      this.answers,
    );
  }

  writing() {
    this.log(`Generating route for ${this.answers.routeTitle}`);

    this._writeCode();
    this._writeTest();
  }

  end() {
    this.log(`Route ${this.answers.routeTitle} has been generated`);
  }
}

module.exports = RouteGenerator;
As we can see, the sub-generator module has the same concept as the main generator. It has user interaction, configuring data, and copying template tasks. Therefore we can combine it with our main generator to run their tasks in the same order. To do this, we only need to call
this.composeWith()
function in the
initializing
task like below.
const Generator = require('yeoman-generator');

class MicroserviceGenerator extends Generator {
  // ...constructor

  initializing() {
    this.composeWith(require.resolve('../route'));
  }

  // ...other tasks
}

module.exports = MicroserviceGenerator;
💡 Pro Tip: You can use Yeoman config file .yo-rc.json to combine data from both generators.
I hope now you can try to create your own microservice generator with Yeoman. I am sure it worth the extra effort to help increase the velocity of your team.
Feel free to contribute to my example repository. You can also ask me anything there.

References


Written by iksena | Self-taught full-stack engineer @jeniusconnect | React Native, Node.js, MongoDB, Next.js
Published by HackerNoon on 2021/01/22