Applying Contract-First Development To UI/UX
Video
COMING SOON
In most of the other segments we have focused on applying contracts to the creation of the API server or the backend. I would like to state that I feel that is far more important to apply contracts to the development of a UI. Why? Generally, user interfaces are what stakeholders need to see in order to determine product-market fit. In other words, if they cannot see and interact with the user interface, they cannot tell if the application we are developing solves the problem it is meant to solve. In my experience, that means that showing a UI which is at least somewhat functional is far more important that a completed or functional backend/API.
Some may ask "Without a backend, how can I tell if my UI is functional?" That's the key goal of this segment. Showing you how you can build, test, and validate a UI and the associated user experience without having an API at all. By leveraging tools available via Contract-First techniques.
Prerequisites
- NodeJS >= 12.x
- NPM >= 6.14.x
- Java JRE >= 1.8.x
- An IDE, preferably one with support for TypeScript
- Git
Setting Up
We're going to start with the beginnings of a user interface created using Angular 11 & Angular Material. The choice of the framework/toolkit for the UI is not really important. What is important is the workflow of how we implement the UI, update the API Spec, interface with the API, and do development using a Mock API server.
Install The Tools
- prism - A Mock API server which generates "fake" responses based on the definitions in an OpenAPI Specification
npm install -g @stoplight/prism-cli
- Angular CLI - The CLI tool for creating and building Angular applications
npm install -g @angular/cli
- OpenAPI Generator CLI - A CLI tool for generating code from an OpenAPI Specification file
npm install -g @openapitools/openapi-generator-cli
Clone The Repo
git clone https://github.com/redhat-appdev-practice/angular-material-prism-openapi.git
cd angular-material-prism-openapi
Developing An Angular UI Using Contract-First Tooling
Set up our build environment
- Open the source directory in your favorite IDE
- Open the
openapi.yml
file in the root of the project. You will notice that there is currently only a single operation defined for a "health check" endpoint. - In a terminal, run
npm i
to install the required dependencies - You may note, if you are familiar with Angular, that routing and a Material navigation component have already been created and configured. The example project also already has 2 stubbed components for the Todo List and for creating a New Todo item.
- Add npm-watch as a "dev" dependency. We will use it to "watch" for changes to files and restart certain tools.
yarn add -D npm-watch
- Add new "script"s to the
package.json
file as shown:
"watch": "npm-watch",
"prism": "prism mock -d --cors openapi.yml",
"openapi": "rm -f src/sdk; mkdir src/sdk; openapi-generator-cli generate -g typescript-angular -i openapi.yml -o src/sdk/",
- Add a new "watch" section to your
package.json
after the "scripts" section.- This is allows us to (re)start components automatically as needed while we develop
- Each time the OpenAPI Spec file changes, the Angular Services will be regenerated and the Prism mock server will be updated. The Angular Dev server will only restart if we change our underlying libraries.
"watch": {
"openapi": "openapi.yml",
"prism": {
"patterns": ["openapi.yml"],
"inherit": true
},
"start": "yarn.lock"
},
- Start all of our tooling using the command
npm run watch
- Prism will start a Mock API server on port 4010
- OpenAPI Generate will output a set of Angular Services based on the contents of the
openapi.yml
file - Angular dev server will start running on port 4200
Create Models In The API Specification
Before we can start using the API to perform CRUD operations, we need to know the data types we will be operating on. In our imaginary situation, we need a Todo model. We can add this model to the openapi.yml
file in a new section as shown:
components:
schemas:
NewTodo:
type: object
properties:
title:
type: string
maxLength: 255
description:
type: string
id:
type: string
format: uuid
If we add this to the bottom of our openapi.yml
file and save the file, we should see the openapi-generator-cli
regenerate our code under src/sdk/model
and we'll now see a newTodo.ts
file. That models is perfect for when we POST a new todo item to our API. Let's create another model for all other use cases.
Todo:
type: object
required:
- id
allOf:
- $ref: '#/components/schemas/NewTodo'
This will define a new Model called Todo, which inherits all of the properties of NewTodo, but also sets an extra constraint which requires the id
field to be set.
Define CRUD Operations In The OpenAPI Specification File
Now that we have the models defined, we can use those models to define API CRUD operations:
Get All Todos
paths:
/todos:
get:
description: Get all todos
operationId: getAllTodos
tags:
- todos
responses:
'200':
description: 'OK'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Todo'
Add New Todo
post:
description: 'Add new Todo'
operationId: addNewTodo
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/NewTodo'
responses:
'200':
description: 'OK'
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
Get Todo By ID
/todo/{id}:
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
get:
operationId: getTodoById
tags:
- todos
responses:
'200':
description: 'OK'
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
Delete Todo By Id
Place this just after the get
operation defined above an at the same indentation level
delete:
operationId: deleteTodoById
tags:
- todos
responses:
'204':
description: 'No content'
Update Todo By Id
put:
operationId: updateTodoById
tags:
- todos
responses:
'200':
description: 'OK'
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
Add The Generated Angular Services In Our TodoListComponent
- Open
src/app/todo-list/todo-list.component.ts
- Import
HttpClient
,Todo
,TodosService
, and theConfiguration
types.typescriptimport { HttpClient } from '@angular/common/http'; import { Configuration } from 'src/sdk'; import { TodosService } from '../../sdk/api/todo.service.ts'; import { Todo } from '../../sdk/model/todo';
- Add new fields and constants to the
TodoListComponent
class as follows:typescriptexport class TodoListComponent implements OnInit { API_BASE_URL = 'http://localhost:4010'; todoService: TodosService; todos: Todo[] = []; // SNIP....
- Instantiate/Create the todoService in the constructor, then configure it to update the Todo listtypescript
constructor(private httpClient: HttpClient) { this.todoService = new TodosService(httpClient, this.API_BASE_URL, new Configuration({ basePath: this.API_BASE_URL })); this.todoService.getAllTodos().subscribe({ next: todos => this.todos = todos, complete: () => console.log('End of todos observable'), error: err => console.error(err) }); }
Use The Todos Array In The HTML Template
We now will have a component which will load our Mock Todos from our Mock API automatically when it is loaded. We can then implement HTML Template code which will take advantage of that.
- Install the Angular Flex Layout module
ng add @angular/flex-layout
- Add FlexLayoutModule to
src/app/app.modules.ts
typescriptimport { FlexLayoutModule } from '@angular/flex-layout'; // SNIP @NgModule({ declarations: [ // SNIP ], imports: [ // SNIP FlexLayoutModule ],
- Open the file
src/app/todo-list/todos-list.component.html
- Add the following structure:html
<div> <div fxLayout="row" fxLayoutAlign="start start"> <div class="header" fxFlex="25%">Title</div> <div class="header" fxFlex="75%">Description</div> </div> <div *ngFor="let todo of todos" fxLayout="row" fxLayoutAlign="start start"> <div fxFlex="25%">{{ todo.title }}</div> <div fxFlex="75%">{{ todo.description }}</div> </div> </div>
- Open the file
src/app/todo-list/todos-list.component.css
- Add the following to implement our FlexBox arrangement:css
.header { background-color: #EAEAEA; border-left: 1px solid #6A6A6A; color: black; font-size: 2rem; text-align: center; height: 2.4rem; padding: 0.3rem; }
- View the updated page and you should see something like:
::::