Standard Banking Demo: JHipster Generated Microservices and Micro Frontends

The second episode of the Entando Standard Banking Demo series brings us to discover how to call JHipster generated microservices using micro frontends.

Anthony Viard - giovedì 2 settembre 2021
Tags: Engineering

Hi my fellow developers,

The second episode of the Entando Standard Banking Demo series brings us to discover how to call JHipster generated microservices using micro frontends.

Taking this one step beyond a hello world app, the Standard Banking Demo helps to understand how a complex distributed application works with Entando.

This article will detail the code architecture, the entities definition from the domain level to the top API level, and finally how the frontend code leverages it.

Let’s deep dive into the code.

This blog post is part of the Entando Standard Banking Demo series:

  • Standard Banking Demo: Introduction
  • Standard Banking Demo: JHipster Generated Microservices and Micro Frontends
  • Standard Banking Demo: Discovering Entando CMS Components

Introduction

Entando defines component types to describe the different parts of your applications as code.

Every part of an Entando application can be defined using components such as the assets, content, pages, plugins, and widgets that make up your application.

A microservice is deployed as a plugin, using an image to run a container on Kubernetes as a pod. A micro frontend is deployed as a widget using a javascript web component and is included in a page.

These components can be created from scratch. However, Entando provides a JHipster blueprint, called Entando Component Generator (ECG), and speeds up the coding time by scaffolding your component, creating the data layer (domain and repository), the business layer including the service and data transfer objects, and the API which can be consumed with HTTP requests.

By default, the ECG generates 3 micro frontends per entity to view, edit, and list the data. These micro frontends cover the CRUD operations and can be customized to meet your needs. For advanced use cases, you can also implement your own micro frontends.

This article will cover the banking microservice and the micro frontend that uses its API.

Banking Microservice

The banking app will be used along this blog post to demonstrate what we can find in the Standard Banking Demo.

You can find the code for the Standard Banking Demo bundle here.

You can find the code for the banking microservice here.

Backend Code: Focus on the CreditCard Entity

The backend contains 9 entities defined using the JHipster Domain Language.

In this article, we will focus on the Creditcard Entity.

image3.png
image6.png

For this Entity, you can find several generated classes.

The domain layer

The lowest level is the domain object in the org.entando.demo.banking.domain package.

@Entity
@Table(name = "creditcard")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Creditcard implements Serializable {

   private static final long serialVersionUID = 1L;

   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
   @SequenceGenerator(name = "sequenceGenerator")
   private Long id;

   @Column(name = "account_number")
   private String accountNumber;

   @Column(name = "balance", precision = 21, scale = 2)
   private BigDecimal balance;

   @Column(name = "reward_points")
   private Long rewardPoints;

   @Column(name = "user_id")
   private String userID;

The Repository is the interface that extends a Spring Data interface to retrieve content from the Database and defines requests that can be used for this given entity, it can be found under the org.entando.demo.banking.repository package.

@Repository
public interface CreditcardRepository extends JpaRepository, JpaSpecificationExecutor {
   Optional findByUserID(String userID);
}

The Service layer

The Service layer contains the business code for this entity. Basically, the service is placed just between the data and the API layer. Here, we have the Service Class that implements the interface.

@Service
@Transactional
public class CreditcardServiceImpl implements CreditcardService {

   private final Logger log = LoggerFactory.getLogger(CreditcardServiceImpl.class);

   private final CreditcardRepository creditcardRepository;

   public CreditcardServiceImpl(CreditcardRepository creditcardRepository) {
       this.creditcardRepository = creditcardRepository;
   }

   @Override
   public Creditcard save(Creditcard creditcard) {
       log.debug("Request to save Creditcard : {}", creditcard);
       return creditcardRepository.save(creditcard);
   }

   @Override
   @Transactional(readOnly = true)
   public Page findAll(Pageable pageable) {
       log.debug("Request to get all Creditcards");
       return creditcardRepository.findAll(pageable);
   }

   @Override
   @Transactional(readOnly = true)
   public Optional findOne(Long id) {
       log.debug("Request to get Creditcard : {}", id);
       return creditcardRepository.findById(id);
   }

   @Override
   public void delete(Long id) {
       log.debug("Request to delete Creditcard : {}", id);
       creditcardRepository.deleteById(id);
   }

   @Override
   @Transactional(readOnly = true)
   public Optional findOneWithUserID(String userID) {
       log.debug("Request to get Creditcard with userID: {}", userID);
       return creditcardRepository.findByUserID(userID);
   }

}

Next, we have the QueryService for advanced search requests using Spring Data Specifications.

@Service
@Transactional(readOnly = true)
public class CreditcardQueryService extends QueryService {

   private final Logger log = LoggerFactory.getLogger(CreditcardQueryService.class);

   private final CreditcardRepository creditcardRepository;

   public CreditcardQueryService(CreditcardRepository creditcardRepository) {
       this.creditcardRepository = creditcardRepository;
   }

   @Transactional(readOnly = true)
   public List findByCriteria(CreditcardCriteria criteria) {
       log.debug("find by criteria : {}", criteria);
       final Specification specification = createSpecification(criteria);
       return creditcardRepository.findAll(specification);
   }

   @Transactional(readOnly = true)
   public Page findByCriteria(CreditcardCriteria criteria, Pageable page) {
       log.debug("find by criteria : {}, page: {}", criteria, page);
       final Specification specification = createSpecification(criteria);
       return creditcardRepository.findAll(specification, page);
   }

   @Transactional(readOnly = true)
   public long countByCriteria(CreditcardCriteria criteria) {
       log.debug("count by criteria : {}", criteria);
       final Specification specification = createSpecification(criteria);
       return creditcardRepository.count(specification);
   }

   protected Specification createSpecification(CreditcardCriteria criteria) {
       Specification specification = Specification.where(null);
       if (criteria != null) {
           if (criteria.getId() != null) {
               specification = specification.and(buildSpecification(criteria.getId(), Creditcard_.id));
           }
           if (criteria.getAccountNumber() != null) {
               specification = specification.and(buildStringSpecification(criteria.getAccountNumber(), Creditcard_.accountNumber));
           }
           if (criteria.getBalance() != null) {
               specification = specification.and(buildRangeSpecification(criteria.getBalance(), Creditcard_.balance));
           }
           if (criteria.getRewardPoints() != null) {
               specification = specification.and(buildRangeSpecification(criteria.getRewardPoints(), Creditcard_.rewardPoints));
           }
           if (criteria.getUserID() != null) {
               specification = specification.and(buildStringSpecification(criteria.getUserID(), Creditcard_.userID));
           }
       }
       return specification;
   }
}

And the Data Transfer Object (DTO) to store the criteria that’s passed as an argument to the QueryService.

public class CreditcardCriteria implements Serializable, Criteria {

   private static final long serialVersionUID = 1L;

   private LongFilter id;

   private StringFilter accountNumber;

   private BigDecimalFilter balance;

   private LongFilter rewardPoints;

   private StringFilter userID;

   public CreditcardCriteria(){
   }

   public CreditcardCriteria(CreditcardCriteria other){
       this.id = other.id == null ? null : other.id.copy();
       this.accountNumber = other.accountNumber == null ? null : other.accountNumber.copy();
       this.balance = other.balance == null ? null : other.balance.copy();
       this.rewardPoints = other.rewardPoints == null ? null : other.rewardPoints.copy();
       this.userID = other.userID == null ? null : other.userID.copy();
   }
}

The Web layer

The Web layer for the microservice aka the Rest layer, is the exposed part of the application defining Rest endpoints to be consumed by clients such as micro frontends.

The request sent to an endpoint will be caught by the web layer and according to the code logic, calls will be made to the Service and indirectly to the Domain layer.

@RestController
@RequestMapping("/api")
@Transactional
public class CreditcardResource {

   private final Logger log = LoggerFactory.getLogger(CreditcardResource.class);

   private static final String ENTITY_NAME = "creditcardCreditcard";

   @Value("${jhipster.clientApp.name}")
   private String applicationName;

   private final CreditcardService creditcardService;

   private final CreditcardQueryService creditcardQueryService;

   public CreditcardResource(CreditcardService creditcardService, CreditcardQueryService creditcardQueryService) {
       this.creditcardService = creditcardService;
       this.creditcardQueryService = creditcardQueryService;
   }

   @PostMapping("/creditcards")
   public ResponseEntity createCreditcard(@RequestBody Creditcard creditcard) throws URISyntaxException {
       log.debug("REST request to save Creditcard : {}", creditcard);
       if (creditcard.getId() != null) {
           throw new BadRequestAlertException("A new creditcard cannot already have an ID", ENTITY_NAME, "idexists");
       }
       Creditcard result = creditcardService.save(creditcard);
       return ResponseEntity.created(new URI("/api/creditcards/" + result.getId()))
           .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString()))
           .body(result);
   }

   @PutMapping("/creditcards")
   public ResponseEntity updateCreditcard(@RequestBody Creditcard creditcard) throws URISyntaxException {
       log.debug("REST request to update Creditcard : {}", creditcard);
       if (creditcard.getId() == null) {
           throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
       }
       Creditcard result = creditcardService.save(creditcard);
       return ResponseEntity.ok()
           .headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, creditcard.getId().toString()))
           .body(result);
   }

   @GetMapping("/creditcards")
   public ResponseEntity> getAllCreditcards(CreditcardCriteria criteria, Pageable pageable) {
       log.debug("REST request to get Creditcards by criteria: {}", criteria);
       Page page = creditcardQueryService.findByCriteria(criteria, pageable);
       HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
       return ResponseEntity.ok().headers(headers).body(page.getContent());
   }

   @GetMapping("/creditcards/count")
   public ResponseEntity countCreditcards(CreditcardCriteria criteria) {
       log.debug("REST request to count Creditcards by criteria: {}", criteria);
       return ResponseEntity.ok().body(creditcardQueryService.countByCriteria(criteria));
   }

   @GetMapping("/creditcards/{id}")
   public ResponseEntity getCreditcard(@PathVariable Long id) {
       log.debug("REST request to get Creditcard : {}", id);
       Optional creditcard = creditcardService.findOne(id);
       return ResponseUtil.wrapOrNotFound(creditcard);
   }

   @DeleteMapping("/creditcards/{id}")
   public ResponseEntity deleteCreditcard(@PathVariable Long id) {
       log.debug("REST request to delete Creditcard : {}", id);
       creditcardService.delete(id);
       return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString())).build();
   }

   @GetMapping("/creditcards/user/{userID}")
   public ResponseEntity getCreditcardByUserID(@PathVariable String userID) {
       log.debug("REST request to get Creditcard by user ID: {}", userID);
       Optional creditcard = creditcardService.findOneWithUserID(userID);
       return ResponseUtil.wrapOrNotFound(creditcard);
   }
}

The Micro Frontends

You can find all the micro frontends under the ui/widgets folder. Each of them matches with a business use case and is implemented as Web Components and consumes APIs from the banking microservice.

Architecture for banking microservice and micro frontends:

image8.png

We will focus on the Dashboard Card React instance that uses the Banking API and the CreditCard endpoints to display the amount and points for a credit card. You can find it under the ui/widgets/banking-widgets/dashboard-card-react folder.

image1.png

Frontend Code: Focus on the CreditCard Implementation

The micro frontend is generic enough to handle more than one kind of transaction exposed by the Banking API: Checking, Savings, and Credit Cards.

Basically, the same frontend component can be used multiple times, and be configured to display different sets of data.

Declare a React application as a Custom Element

The custom element is a part of the Web Component specification. The micro frontend is declared as a custom element in the React application.

In the src/custom-elements folder, you can find a SeedscardDetailsElement.js file that defines the whole component by implementing the HTMLElement interface.

const ATTRIBUTES = {
 cardname: 'cardname',
};

class SeedscardDetailsElement extends HTMLElement {
 onDetail = createWidgetEventPublisher(OUTPUT_EVENT_TYPES.transactionsDetail);

 constructor(...args) {
   super(...args);

   this.mountPoint = null;
   this.unsubscribeFromKeycloakEvent = null;
   this.keycloak = getKeycloakInstance();
 }

 static get observedAttributes() {
   return Object.values(ATTRIBUTES);
 }

 attributeChangedCallback(cardname, oldValue, newValue) {
   if (!Object.values(ATTRIBUTES).includes(cardname)) {
     throw new Error(`Untracked changed attribute: ${cardname}`);
   }
   if (this.mountPoint && newValue !== oldValue) {
     this.render();
   }
 }

 connectedCallback() {
   this.mountPoint = document.createElement('div');
   this.appendChild(this.mountPoint);

   const locale = this.getAttribute('locale') || 'en';
   i18next.changeLanguage(locale);

   this.keycloak = { ...getKeycloakInstance(), initialized: true };

   this.unsubscribeFromKeycloakEvent = subscribeToWidgetEvent(KEYCLOAK_EVENT_TYPE, () => {
     this.keycloak = { ...getKeycloakInstance(), initialized: true };
     this.render();
   });

   this.render();
 }

 render() {
   const customEventPrefix = 'seedscard.details.';
   const cardname = this.getAttribute(ATTRIBUTES.cardname);

   const onError = error => {
     const customEvent = new CustomEvent(`${customEventPrefix}error`, {
       details: {
         error,
       },
     });
     this.dispatchEvent(customEvent);
   };

   const ReactComponent = React.createElement(SeedscardDetailsContainer, {
     cardname,
     onError,
     onDetail: this.onDetail,
   });
   ReactDOM.render(
     {ReactComponent},
     this.mountPoint
   );
 }

 disconnectedCallback() {
   if (this.unsubscribeFromKeycloakEvent) {
     this.unsubscribeFromKeycloakEvent();
   }
 }
}

if (!customElements.get('sd-seeds-card-details')) {
 customElements.define('sd-seeds-card-details', SeedscardDetailsElement);
}

We can see the cardname attribute is passed to the custom element to switch between the different kinds of data we want to retrieve.

The 'sd-seeds-card-details' tag can be used to instantiate a new component. Here is an example from the public/index.html where the default cardname is “checking”.

<body onLoad="onLoad();">
   <noscript>You need to enable JavaScript to run this app.</noscript>
   <sd-seeds-card-details cardname="checking" />
   <sd-seeds-card-config />
</body>
Calling the Banking API

The Banking API exposed some endpoints generated from the JHipster entities’ declarations. The MFE is able to consume this API through HTTP calls.

The src/api/seedscard.js file contains the endpoint definitions:

import { DOMAIN } from 'api/constants';

const getKeycloakToken = () => {
 if (
   window &&
   window.entando &&
   window.entando.keycloak &&
   window.entando.keycloak.authenticated
 ) {
   return window.entando.keycloak.token;
 }
 return '';
};

const defaultOptions = () => {
 const token = getKeycloakToken();

 return {
   headers: new Headers({
     Authorization: `Bearer ${token}`,
     'Content-Type': 'application/json',
   }),
 };
};

const executeFetch = (params = {}) => {
 const { url, options } = params;
 return fetch(url, {
   method: 'GET',
   ...defaultOptions(),
   ...options,
 })
   .then(response =>
     response.status >= 200 && response.status < 300
       ? Promise.resolve(response)
       : Promise.reject(new Error(response.statusText || response.status))
   )
   .then(response => response.json());
};

export const getSeedscard = (params = {}) => {
 const { id, options, cardname } = params;

 const url = `${DOMAIN}${DOMAIN.endsWith('/') ? '' : '/'}banking/api/${cardname}s/${id}`;

 return executeFetch({ url, options });
};

export const getSeedscardByUserID = (params = {}) => {
 const { userID, options, cardname } = params;

 const url = `${DOMAIN}${DOMAIN.endsWith('/') ? '' : '/'}banking/api/${cardname}s/user/${userID}`;

 return executeFetch({ url, options });
};

The requests defined here are flexible enough to be used with multiple types of credit cards. This is why the path depends on the cardname and the userID banking/api/${cardname}s/user/${userID}

Render banking info

The src/components folder contains the rendering part with both SeedcardDetails.js and SeedcardDetailsContainer.js.

const SeedscardDetails = ({ classes, t, account, onDetail, cardname }) => {
 const header = (
   <div className={classes.SeedsCard__header}>
     <img alt="interest account icon" className={classes.SeedsCard__icon} src={seedscardIcon} />
     <div className={classes.SeedsCard__title}>
       {t('common.widgetName', {
         widgetNamePlaceholder: cardname.replace(/^\w/, c => c.toUpperCase()),
       })}
     </div>
     <div className={classes.SeedsCard__value}>
       ...
       {account &&
         account.id &&
         account.accountNumber.substring(
           account.accountNumber.length - 4,
           account.accountNumber.length
         )}
     </div>
     <div className={classes.SeedsCard__action}>
       <i className="fas fa-ellipsis-v" />
     </div>
   </div>
 );

 return (
   // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
   <div
     onClick={account && account.id ? () => onDetail({ cardname, accountID: account.id }) : null}
   >
     <div className={classes.SeedsCard}>
       {account && account.id ? (
         <>
           {header}
           <p className={classes.SeedsCard__balance}>
             ${account.balance.toString().replace(/\B(?<!\.\d)(?=(\d{3})+(?!\d))/g, ',')}
           </p>
           <p className={classes.SeedsCard__balanceCaption}>Balance</p>
           {account.rewardPoints && (
             <p className={classes.SeedsCard__balanceReward}>
               Reward Points:{' '}
               <span className={classes.SeedsCard__balanceRewardValue}>
                 {account.rewardPoints}
               </span>
             </p>
           )}
         </>
       ) : (
         <>
           {header}
           <p className={classes.SeedsCard__balanceCaption}>
             You don&apos;t have a {cardname} account
           </p>
         </>
       )}
     </div>
   </div>
 );
};

The SeedcardDetailsContainer.js. handle the API call:

getSeedscardByUserID({ userID, cardname })
 .then(account => {
   this.setState({
     notificationStatus: null,
     notificationMessage: null,
     account,
   });
   if (cardname === 'checking' && firstCall) {
     onDetail({ cardname, accountID: account.id });
   }
 })
 .catch(e => {
   onError(e);
 })
 .finally(() => this.setState({ loading: false }));

When the Widget is deployed, the requests contain the right Card name value, and the data retrieved matches with it, here is the first screenshot from the Dashboard.

image5.png
image7.png
Configure the Widget in the Entando Platform

As Entando wraps the micro frontend as a widget, it can come with a configuration widget to set values such as the cardname.

This allows you to change the cardname value from Entando App Builder without needing to deploy the micro frontend again.

To access that, you need to design a page, click on a widget kebab menu and click on settings (The settings menu is only present when a configuration widget is provided with a widget).

image4.png
image2.png

What’s next

In this article, we saw a lot of code from the data layer with the domain definition to the data rendering in the micro frontend to display the CreditCard information.

The next blog post will dive into the CMS components of the Standard Banking Demo. It will contain less code and will focus more on the Standard Banking Demo bundle by explaining the different CMS components you can use to build the content of your pages.

Bonus: the Standard Banking Demo Video

White_Paper_cover.png

Learn How To Create Better Apps, Portals, & Websites--Faster.

This white paper outlines how your organization can accelerate UX innovation by developing with micro frontends on Kubernetes, as well as how a micro frontend platform can help you execute this methodology more effectively.