Generate Angular ReactiveForms from Swagger/OpenAPI

martinmcwhorter

Martin McWhorter

Posted on August 1, 2020

Generate Angular ReactiveForms from Swagger/OpenAPI

Angular ReactiveForms, despite their problems, are a powerful tool for encoding form validation rules reactively.

Single Source of Truth for Validation Rules

Your backend code should be the single source of truth for validation rules. Of course we should validate input in the UI for a better user experience.

Likely we are either implementing the same rules from the same spec, or copying what has been implemented in the API, or layers behind that. We should be asking ourselves, Where should the single source of truth for validation rules live? It probably shouldn't be in the Angular app. We can eliminate this manual duplication by generating Angular ReactiveForms from OpenAPI/Swagger specs, rather than hand coding them.

This eliminates bugs where the validation rules between the UI and API fall out of sync -- or copied incorrectly from the backend to the frontend. When new validation rules change, just rerun the command to generate the reactive form from the updated OpenAPI spec.

This works very well in conjunction with Rest API proxies generated using the openapi-generator.

Prerequisite

If your projects do not meet following prerequisite, you can still use the Quick Start.

In order for this to work with your Rest API you will need to have your backend provide a well formed Swagger (OpenAPI 2) or OpenAPI 3 spec that includes model-metadata for validation expressed as type, format, pattern, minLength, maxLength, etc. Frameworks such as SwashbuckleCore and SpringFox (and many others) do this for you based on metadata provided using attributes or annotations.

Quick Start

This quick start uses a hosted swagger spec, so if you can still go through it whether or not your API exposes the required model-metadata.

First, let's create a new app.



npm i -g @angular/cli
ng n example
cd example


Enter fullscreen mode Exit fullscreen mode

Second, install the generator into your Angular project as a dev dependancy.



npm install --save-dev @verizonconnect/ngx-form-generator


Enter fullscreen mode Exit fullscreen mode

Third, update your package.json scripts to include a script to generate the form. This way when the API changes we can easily rerun this script to regenerate the form.



{ 
 . . . 
  "scripts": { 
  . . .
    "generate:address-form": "ngx-form-generator -i https://raw.githubusercontent.com/verizonconnect/ngx-form-generator/master/demo/swagger.json -o src/app/"
  },
  . . .
}


Enter fullscreen mode Exit fullscreen mode

Now run the script.



npm run generate:address-form


Enter fullscreen mode Exit fullscreen mode

Within your src/app you will now have a new generated file based on the name of the OpenAPI title property, in this case myApi.ts. You can change this using the -f argument and provide the filename you like.

We can now import the form into a component and expose it to the template.



import { Component } from '@angular/core';
import { addressModelForm } from './myApi'; // <- import the form

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  addressForm = addressModelForm; // <- expose form to template
}


Enter fullscreen mode Exit fullscreen mode

We will add Angular Material to our project to provide styles for our form in this example. Of course there is no dependency on any CSS or component library.



ng add @angular/material


Enter fullscreen mode Exit fullscreen mode

In the module lets import ReactiveFormModule, MatFormFieldModule and MatInputModule.



import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms'; // <- ESM imports start
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    ReactiveFormsModule, // <- NgModule imports start
    MatFormFieldModule,
    MatInputModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


Enter fullscreen mode Exit fullscreen mode

We will build the form. For demonstration purposes only need the first field.



<form [formGroup]="addressForm">

  <mat-form-field>
    <mat-label>First Name</mat-label>
    <input matInput formControlName="firstName">
    <mat-error *ngIf="addressForm.controls.firstName.invalid">This field is invalid</mat-error>
  </mat-form-field>

</form>


Enter fullscreen mode Exit fullscreen mode

We can now run this with ng serve and see our single field form.

Empty Field

An invalid name can be entered into the field and we will see this validated based on the validation rules from exposed to the Angular application through the OpenAPI spec.

Invalid Field

Here we can enter a valid name and see that the validation updates.

Valid Field

If this were a real RestAPI we could add the rest of our form fields and go.

Isolate Generated Code into a Library

We can improve this by putting the generated form into its own library within the Angular workspace. The benefits of this are:

  1. Clear separation of boundaries between the generated and crafted code. One of the power-dynamics of generating API proxies and forms is being able to safely regenerate them. This will help prevent a team-member from manually modifying the generated form.
  2. No need to recompile the form during local development. The form project will only need to be recompiled when after it has been regenerated.
  3. We can add this generation process as part of the CICD build process.

Create a new library



ng g lib address-form


Enter fullscreen mode Exit fullscreen mode

We can now remove the scaffolded component, service and module from the lib.



rm -fr projects/address-form/src/lib/* 


Enter fullscreen mode Exit fullscreen mode

We are placing only generated code into this library. We do not want to create unit tests for generated code. Tests should live with the code generator itself. So lets get rid of the unit test support files.



rm projects/address-form/karma.conf.js
rm projects/address-form/tsconfig.spec.json
rm projects/address-form/src/test.ts


Enter fullscreen mode Exit fullscreen mode

We don't need to lint generated code, so lets get rid of tslint.json



rm projects/address-form/tslint.json


Enter fullscreen mode Exit fullscreen mode

We need to remove the references to the test and lint files in the workspace angular.json. Open angular.json and find "address-form": { property. Remove the "test": { and "lint": { sections.

It should then look something like this.



    "address-form": {
      "projectType": "library",
      "root": "projects/address-form",
      "sourceRoot": "projects/address-form/src",
      "prefix": "lib",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-ng-packagr:build",
          "options": {
            "tsConfig": "projects/address-form/tsconfig.lib.json",
            "project": "projects/address-form/ng-package.json"
          },
          "configurations": {
            "production": {
              "tsConfig": "projects/address-form/tsconfig.lib.prod.json"
            }
          }
        }
      }
    }


Enter fullscreen mode Exit fullscreen mode

In package.json we need to add a script to build our lib as well as update the path where we generate the form.



    "generate:address-form": "ngx-form-generator -i https://raw.githubusercontent.com/verizonconnect/ngx-form-generator/master/demo/swagger.json -o projects/address-form/src/lib",
    "build:libs": "ng build address-form"


Enter fullscreen mode Exit fullscreen mode

Now lets generate the form into the lib.



npm run generate:address-form


Enter fullscreen mode Exit fullscreen mode

Now replace all of the exports in proects/address-form/src/public-api.ts with:



export * from './lib/myApi';


Enter fullscreen mode Exit fullscreen mode

Build the lib with:



npm run build:libs


Enter fullscreen mode Exit fullscreen mode

We can remove the old generated myApi.ts



rm src/app/myApi.ts


Enter fullscreen mode Exit fullscreen mode

Last, update the import of the form



import { addressModelForm } from 'address-form';

Enter fullscreen mode Exit fullscreen mode




Conclusion

Generating forms will allow you to keep your validation rules in sync with the backend. Like generating proxies, this will greatly reduce integration bugs that occur by trying to manually implement backend to UI data contracts.

💖 💪 🙅 🚩
martinmcwhorter
Martin McWhorter

Posted on August 1, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related