BFF
Develop and deploy the BFF component of the inventory application
The Inventory BFF's role in the architecture is to act as an orchestrator between the core business services and the specific digital channel it is focused on supporting. This class article will give you more detail about the Backend For Frontend architectural pattern and the benefits.
Backend For Frontend pattern Overview - source |
The Inventory solution will use GraphQL for its BFF layer, which enables the API to be dynamically controlled from the client using API queries. Follow the steps below to get started.
Setup¶
Create your OpenShift project and register the pipeline¶
-
Create a new repository from the Typescript GraphQL template.
Warning
If you are developing on a shared education cluster, place the repository in the Git Organization listed in your notification email and remember to add your initials as a suffix to the app name. - In order to prevent naming collisions, name the repository
inv-bff-{your initials}
replacing{your initials}
with your actual initials. -
Deploy this application with Tekton pipelines :
-
In the OpenShift web console, head up to Topology menu on the left on the Developer perspective and click Create a new project.
-
Give a name to your project, call it
dev-{your initials}
, the other fields are optional. -
Initialize a web terminal using the
>_
button on the top bar next to your name on the cluster. You should have a terminal with all the necessary development tools.
Note
You should have the oc
and igc
command line tools installed. If not, refer to the developers tools setup page.
-
In the OpenShift web console, click on email address top right, click on Copy login command and get the OpenShift login command, which includes a token.
-
Click on Display Token, copy the Login with the token. oc login command will log you in. Run the login command in your terminal:
$ oc login --token=qvARHflZDlOYfjJZRJUEs53Yfy4F8aa6_L3ezoagQFM --server=https://c103-e.us-south.containers.cloud.ibm.com:30979
Logged into "https://c103-e.us-south.containers.cloud.ibm.com:30979" as "IAM#email@company" using the token provided.
You have access to 71 projects, the list has been suppressed. You can list all projects with 'oc projects'
- Run the following command to setup your project:
oc sync dev-{your initials}
- Register the pipeline, give git credentials if prompted, and
main
as the git branch to use. When prompted for the pipeline, selectibm-nodejs
.
oc pipeline --tekton https://github.com/cnw-team-{team}/inv-bff-{your initials}
-
Open the pipeline to see it running
-
When the pipeline is completed, run
oc endpoints -n dev-{your initials}
. You should see an entry for the app we just pushed. Select the entry and hitEnter
to launch the browser.
Choose your development environment¶
-
Head over to gitpod.io, login with your github account by clicking Continue with GitHub.
-
Grant access to gitpod to your GitHub organization by going to account settings on the top right corner, navigate to Integrations, go to GitHub and click Manage on GitHub.
-
On GitHub, search the organization you have used to create the repository and click grant to allow gitpod to access repositories in that organization.
-
Back to gitpod.io, navigate to workspaces and click New Workspace to create a new workspace, give it your newly created repository URL.
-
If it is your first gitpod workspace, it will ask you for your preferred editor, pick the in browser Visual Studio Code, and the workspace will be created automatically for you.
You are now ready to modify the application!
Clone the project and open it using your favorite text editor or IDE (Visual Studio Code, Atom...).
git clone https://github.com/cnw-team-{team}/inv-bff-{your initials}.git
cd inv-bff-{your initials}
code .
Create the REST interface¶
The controller provides the REST interface for our BFF. The template uses the typescript-rest
package to simplify the tasks required to create a controller.
Since we will be developing this microservice following the Test Driven Development approach, we are first going to create the test for our stock-items
controller.
-
Start the tests by running the following command in a new terminal that you will keep running while running the lab:
npm run tdd
-
Create the controller test:
test/controllers/stock-items.controller.spec.tsimport {Application} from 'express'; import request from 'supertest'; import {buildApiServer} from '../helper'; describe('stock-item.controller', () => { let app: Application; beforeEach(async () => { const apiServer = buildApiServer(); app = await apiServer.getApp(); }); test('canary verifies test infrastructure', () => { expect(true).toEqual(true); }); describe('given GET /stock-items', () => { describe('when service is successful', () => { test('then return 200 status', async () => { return request(app).get('/stock-items').expect(200); }); test('then should return an empty array', async () => { return request(app).get('/stock-items').expect([]); }); }); }); });
-
Notice that tests are now failing for the nex tests.
-
Create the controller component:
src/controllers/stock-items.controller.tsimport {GET, Path} from 'typescript-rest'; @Path('stock-items') export class StockItemsController { @GET async listStockItems(): Promise<any[]> { return []; } }
-
Add the controller to the controllers
index.ts
. (Usingindex.ts
is a good way to manage which components are exposed by a component and provide a good way to load the modules that will be injected into other components):src/controllers/index.tsexport * from './hello-world.controller'; export * from './health.controller'; export * from './stock-items.controller';
-
Start the service to see it running:
npm start
-
Access the running service. This service runs on port
3000
:
- Once you run the application, Gitpod gives the option to make the port Public, in the bottom-right corner.Once you make the port Public, it gives you the option to Open Preview or Open Browser.
-
Open a browser to
http://localhost:3000/api-docs
to see the swagger page -
Expand our service from the list, click
Try it out
, then clickExecute
-
Push the changes we've made to the repository:
git add . git commit -m "Adds stock items controller" git push
-
On the openshift console, open the pipeline to see it running.
Update the controller to call a service¶
The pattern recommended for the REST controllers is to let it focus on translating REST protocols into javascript and to put the business logic in a separate service component.
The pattern recommended for the REST controllers is to let it focus on translating REST protocols into javascript and to put the business logic in a separate service component.
-
Add a StockItem model that contains the values needed for the UI:
src/models/stock-item.model.tsexport class StockItemModel { id: string; name: string; stock: number; unitPrice: number; manufacturer: string; picture: string; }
-
Register the model with the
index.ts
file in the models directory. Append this to end of the file:src/models/index.ts... export * from './stock-item.model';
-
Define an abstract class to provide the interface for our API:
src/services/stock-items.api.tsimport { StockItemModel } from '../models'; export abstract class StockItemsApi { abstract listStockItems(): Promise<StockItemModel[]>; }
Note
Why an abstract class and not an interface? TypeScript introduces both abstract classes and interfaces. When TypeScript gets transpiled into JavaScript, abstract classes are generated as classes but interfaces disappear since there isn't an equivalent type in JavaScript. As a result, they cannot be used as a binding type for the
typescript-ioc
framework. Fortunately, abstract classes can be used and they have the quirky behavior in TypeScript allowing them to either beextended
like a class orimplemented
like an interface. -
Add the abstract class to the
index.ts
file in the services directory. Add it to the end of other export statements, do not overwrite the file:src/services/index.ts... export * from './stock-items.api'; ...
-
Lets create an implementation that will provide mock data for now. Add a
stock-items-mock.service
to services:src/services/stock-items-mock.service.tsimport { StockItemsApi } from './stock-items.api'; import { StockItemModel } from '../models'; export class StockItemsMockService implements StockItemsApi { async listStockItems(): Promise<StockItemModel[]> { return [ { id: "1", name: "Self-sealing stem bolt", stock: 10, unitPrice: 10.5, picture: "https://via.placeholder.com/32.png", manufacturer: "Bajor Galactic" }, { id: "2", name: "Heisenberg compensator", stock: 20, unitPrice: 20.0, picture: "https://via.placeholder.com/32.png", manufacturer: "Federation Imports" }, { id: "3", name: "Tooth sharpener", stock: 30, unitPrice: 5.25, picture: "https://via.placeholder.com/32.png", manufacturer: "Farenginar Exploits" } ]; } }
-
Add the mock service to the
index.ts
file in the services directory:src/services/index.ts... export * from './stock-items-mock.service'; ...
-
Update the controller test to inject the service into the controller and to return the value from the service:
test/controllers/stock-items.controller.spec.tsimport { Application } from 'express'; import request from 'supertest'; import { Container } from 'typescript-ioc'; import { buildApiServer } from '../helper'; import Mock = jest.Mock; import { StockItemsMockService } from '../../src/services'; describe('stock-item.controller', () => { let app: Application; let service_listStockItems: Mock; beforeEach(async () => { service_listStockItems = jest.fn(); Container.bind(StockItemsMockService).factory( () => ({ listStockItems: service_listStockItems }), ); const apiServer = buildApiServer(); app = await apiServer.getApp(); }); test('canary verifies test infrastructure', () => { expect(true).toEqual(true); }); describe('given GET /stock-items', () => { describe('when service is successful', () => { const expectedResult = [{ value: 'val' }]; beforeEach(() => { service_listStockItems.mockResolvedValue(expectedResult); }); test('then return 200 status', async () => { return request(app).get('/stock-items').expect(200); }); test('then should return value from service', async () => { return request(app).get('/stock-items').expect(expectedResult); }); }); describe('when service fails', () => { beforeEach(() => { service_listStockItems.mockRejectedValue(new Error('service failed')); }); test('then return 502 error', async () => { return request(app).get('/stock-items').expect(502); }); }); }); });
-
Update the controller to inject the service and use it:
src/controllers/stock-items.controller.tsimport { Inject } from 'typescript-ioc'; import { GET, Path } from 'typescript-rest'; import { HttpError } from 'typescript-rest/dist/server/model/errors'; import { StockItemModel } from '../models'; import { StockItemsMockService } from '../services'; class BadGateway extends HttpError { constructor(message?: string) { super("BadGateway", message); this.statusCode = 502; } } @Path('stock-items') export class StockItemsController { @Inject service: StockItemsMockService; @GET async listStockItems(): Promise<StockItemModel[]> { try { return await this.service.listStockItems(); } catch (err) { throw new BadGateway('There was an error'); } } }
-
Start the service
npm start
-
Access the running service. This service runs on port
3000
:
- Once you run the application, Gitpod gives the option to make the port Public, in the bottom-right corner.Once you make the port Public, it gives you the option to Open Preview or Open Browser.
-
Open a browser to
http://localhost:3000/api-docs
to see the swagger page -
Expand our service from the list, click
Try it out
, then clickExecute
-
Push the changes we've made to the repository:
git add . git commit -m "Adds a mock service implementation" git push
-
On the openshift console, open the pipeline to see it running.
Add a GraphQL implementation of Stock Items¶
The GraphQL template supports both REST and GraphQL APIs for accessing backend services. We have created a REST controller to expose the results from the service and now we will do the same for GraphQL.
-
Create a
stock-items
GraphQL schema in theschemas
directory:src/schemas/stock-item.schema.tsimport { Field, Float, Int, ObjectType } from 'type-graphql'; import { StockItemModel } from '../models'; @ObjectType() export class StockItem implements StockItemModel { @Field() id: string; @Field() manufacturer: string; @Field() name: string; @Field({ nullable: true }) picture: string; @Field(type => Int) stock: number; @Field(type => Float) unitPrice: number; }
-
Add the stock-items schema to the
index.ts
in the schemas directory:src/schemas/index.ts... export * from './stock-item.schema'
-
Add a
stock-item
GraphQL resolver in theresolvers
directory:src/resolvers/stock-item.resolver.tsimport { Query, Resolver } from 'type-graphql'; import { Inject } from 'typescript-ioc'; import { resolverManager } from './_resolver-manager'; import { StockItem } from '../schemas'; import { StockItemModel } from '../models'; import { StockItemsMockService } from '../services'; @Resolver(of => StockItem) export class StockItemResolver { @Inject service: StockItemsMockService; @Query(returns => [StockItem]) async stockItems(): Promise<StockItemModel[]> { return this.service.listStockItems(); } } resolverManager.registerResolver(StockItemResolver);
Note
The template includes a
resolverManager
component that simplifies the steps to make the resolver available. All that is required to use the resolver is to register it, preferably at the bottom of the module where it is defined. -
Add the stock-items resolver to
index.ts
in the resolvers directory:src/resolvers/index.ts... export * from './stock-item.resolver';
-
Start the service:
npm start
- Verify that the that the resolver is available using the Graph QL browser provided by the template:
- Once you run the application, Gitpod gives the option to make the port Public, in the bottom-right corner.Once you make the port Public, it gives you the option to Open Preview or Open Browser.
- In your Browser, open GraphQL playground:
http://localhost:3000/graphql
- Run the query
query { stockItems { name } }
-
Push the changes we've made to the repository:
git add . git commit -m "Adds a graphql interface" git push
-
On the openshift console, open the pipeline to see it running.
Create a service implementation that calls the microservice¶
-
Add a
stock-item-service.config
file in thesrc/config
directory:src/config/stock-item-service.config.tsexport class StockItemServiceConfig { baseUrl: string; }
-
Add a
stock-item-service.config.provider
file in thesrc/config
directory:src/config/stock-item-service.config.provider.tsimport {ObjectFactory} from 'typescript-ioc'; const baseUrl: string = process.env.SERVICE_URL || 'localhost:9080'; export const stockItemConfigFactory: ObjectFactory = () => ({ baseUrl, });
The config class separates how the config is loaded from how it is used. In this case the config is simply retrieved from an environment variable but in more complex cases the value(s) can be retrieved from external data sources.
-
Add the
stock-item-service.config
to anindex.ts
file of theconfig
directory:src/config/index.tsimport { StockItemServiceConfig } from './stock-item-service.config'; import { stockItemConfigFactory } from './stock-item-service.config.provider'; import { Container } from 'typescript-ioc'; export * from './stock-item-service.config'; Container.bind(StockItemServiceConfig).factory(stockItemConfigFactory);
-
Create a
stock-items
service in the services directory that uses the config:src/services/stock-items.service.tsimport { Inject } from 'typescript-ioc'; import { get, Response } from 'superagent'; import { StockItemsApi } from './stock-items.api'; import { StockItemModel } from '../models'; import { StockItemServiceConfig } from '../config'; import { LoggerApi } from '../logger'; class StockItem { 'id'?: string; 'manufacturer'?: string; 'picture'?: string; 'name'?: string; 'price'?: number; 'stock'?: number; } export class StockItemsService implements StockItemsApi { @Inject _logger: LoggerApi; @Inject config: StockItemServiceConfig; get logger(): LoggerApi { return this._logger.child('StockItemsService'); } async listStockItems(): Promise<StockItemModel[]> { return new Promise((resolve, reject) => { get(`${this.config.baseUrl}/stock-items`) .set('Accept', 'application/json') .then(res => { resolve(this.mapStockItems(res.body)); }) .catch(err => { reject(err); }); }); } mapStockItems(data: StockItem[]): StockItemModel[] { return data.map(this.mapStockItem); } mapStockItem(item: StockItem): StockItemModel { return { id: item.id, name: item.name, stock: item.stock, unitPrice: item.price, picture: item.picture ?? 'https://via.placeholder.com/32.png', manufacturer: item.manufacturer, }; } }
-
Add
stock-item.service
toindex.ts
in theservices
directory:src/services/index.ts... export * from './stock-items.service';
-
Replace
StockItemsMockService
withStockItemsService
in the following files: src/resolvers/stock-item.resolver.ts
src/controllers/stock-items.controller.ts
-
test/controllers/stock-items.controller.spec.ts
-
Modify
connectsTo
property to the values.yaml file of the Helm chart. The value of the property should match the Kubernetes service of the microservice. (For template projects, the service name is the same as the name of the application which is that same as the name of the repository.)chart/base/values.yaml... connectsTo: inv-svc-{your initials} ...
Info
The
values.yaml
file of the Helm chart defines the variables that can be provided to the template as input. Now that we've added a new variable, we will need to update the appropriate template file to use our new variable. -
Add a new environment variable named
SERVICE_URL
to the list of existing environment variables in deployment.yaml. (SERVICE_URL
is the name we gave the environment variable in ourstock-item-service.config
class as the first step in this section.) The value of this environment variable should come from theconnectsTo
value we defined. You can add| quote
to wrap the value in quotes in case the value is not formatted correctly:chart/base/templates/deployment.yaml... env: - name: INGRESS_HOST value: "" - name: PROTOCOLS value: "" - name: LOG_LEVEL value: {{ .Values.logLevel | quote }} - name: SERVICE_URL value: {{ printf "%s:80" .Values.connectsTo | quote }} ...
Info
deployment.yaml
is a templatized Kubernetes yaml file that describes the deployment of our component. The deployment will create one or more pods based on the pod template defined in the deployment. Each pod that starts will have the environment variables that we have defined in theenv
section available for the container image to reference. -
Commit and push the changes to git:
git add . git commit -m "Adds service implementation" git push
-
On the openshift console, open the pipeline to see it running.