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.
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.
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<Creditcard, Long>, JpaSpecificationExecutor<Creditcard> {
Optional<Creditcard> 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<Creditcard> findAll(Pageable pageable) {
log.debug("Request to get all Creditcards");
return creditcardRepository.findAll(pageable);
}
@Override
@Transactional(readOnly = true)
public Optional<Creditcard> 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<Creditcard> 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<Creditcard> {
private final Logger log = LoggerFactory.getLogger(CreditcardQueryService.class);
private final CreditcardRepository creditcardRepository;
public CreditcardQueryService(CreditcardRepository creditcardRepository) {
this.creditcardRepository = creditcardRepository;
}
@Transactional(readOnly = true)
public List<Creditcard> findByCriteria(CreditcardCriteria criteria) {
log.debug("find by criteria : {}", criteria);
final Specification<Creditcard> specification = createSpecification(criteria);
return creditcardRepository.findAll(specification);
}
@Transactional(readOnly = true)
public Page<Creditcard> findByCriteria(CreditcardCriteria criteria, Pageable page) {
log.debug("find by criteria : {}, page: {}", criteria, page);
final Specification<Creditcard> specification = createSpecification(criteria);
return creditcardRepository.findAll(specification, page);
}
@Transactional(readOnly = true)
public long countByCriteria(CreditcardCriteria criteria) {
log.debug("count by criteria : {}", criteria);
final Specification<Creditcard> specification = createSpecification(criteria);
return creditcardRepository.count(specification);
}
protected Specification<Creditcard> createSpecification(CreditcardCriteria criteria) {
Specification<Creditcard> 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<Creditcard> 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<Creditcard> 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<List<Creditcard>> getAllCreditcards(CreditcardCriteria criteria, Pageable pageable) {
log.debug("REST request to get Creditcards by criteria: {}", criteria);
Page<Creditcard> 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<Long> 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<Creditcard> getCreditcard(@PathVariable Long id) {
log.debug("REST request to get Creditcard : {}", id);
Optional<Creditcard> creditcard = creditcardService.findOne(id);
return ResponseUtil.wrapOrNotFound(creditcard);
}
@DeleteMapping("/creditcards/{id}")
public ResponseEntity<Void> 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<Creditcard> getCreditcardByUserID(@PathVariable String userID) {
log.debug("REST request to get Creditcard by user ID: {}", userID);
Optional<Creditcard> 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:
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.
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(
<KeycloakContext.Provider value={this.keycloak}>{ReactComponent}</KeycloakContext.Provider>,
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'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.
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).
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
Top comments (0)