Generate minimalistic TypeScript API layer for Angular with full type reflection of backend model.
npm i swagger-angular-generator
-h
- show help
-s
, --src
- source directory
-d
, --dest
- destination directory, default: src/api
--no-store
- do not generate the ngrx modules
-u, --swagger-URL-path
- swagger URL path, where the swagger ui documentation can be found; default: /swagger
, i.e. the resulting address would be http://example/swagger
-o, --omit-version
- disables API version information to be generated in comments for each file
run via
./node_modules/.bin/swagger-angular-generator
swagger-angular-generator
package, npm run generate
"script": {
"generate": "swagger-angular-generator -s src/api/scheme.json -d src/api/generated"
...
}
or programatically as a method invocation
import {generate} from 'swagger-angular-generator';
// or using CommonJS loader
const {generate} = require('swagger-angular-generator');
generate('conf/api/api-docs.json', 'src/api');
The resulting API layer contains the following structure in the destination directory:
controllers
directory stores services containing all API methods devided by controllersdefs
directory stores all response interfaces and enumsstore
directory has modules, which contain associated form service and NGRX actions, reducers and effectsmodel.ts
file reexports all of them together for a simple accessWhen updating your code for new backend version, we recommend you to follow these steps:
git diff
the changestsc
for immediate problemsIn order to consume generated model, follow the steps 1-9 in the following example to use generated API model.
// 1. import used response interfaces
import {ItemDto, PageDto} from '[relative-path-to-destination-directory]/model';
// 2. import used API service and optionally param interfaces
import {DataService, MethodParams} from '[relative-path-to-destination-directory]/api/DataService';
@Component({
...
// 3. make the service injectable (can be also imported in the module)
providers: [DataService],
})
export class MyComponent implements OnInit {
// 4. declare response object variables based on the generated API interfaces
public items: ItemDto[] = [];
public page: PageDto;
// 5. declare request params based on the generated API interface (all params are passed together in one object)
private params: MethodParams = {
page: 0,
size: 10,
sort: ['name:asc']
};
// 6. inject the API service
constructor(private dataService: DataService) {}
public ngOnInit() {
// 7. the returned observable is fully typed
this.dataService
.get(this.params)
// 8. returned data are fully typed
.subscribe(data => {
// 9. assignments are type-checked
this.items = data.content;
this.page = data.page;
});
}
}
exampleFormService
service is generated and holds the FormGroup
definition that corresponds
with the request data structureFormArrayExtended
that extends native Angulars' FormArray
and holds the definition of array item so new items can be created for data via .setValue()
or empty via .createControl()
.FormMap
that extends native Angulars' FormGroup
and holds the definition of map value item so new items can be created for data via .setValue()
or empty via .createControl()
.safeSetValue()
that sets the shape and data of all AbstractControl
's ancestors and never fails (compatible data form the shape and are set, the rest is ignored).<form [formGroup]="exampleFormService.form" (ngSubmit)="sendForm()" class="full-width">
<input type="text" name="email" placeholder="email"
formControlName="email" />
<button type="submit"
[disabled]="exampleFormService.form.invalid">Save</button>
</form>
this is the corresponding component
@Component({
selector: 'example-component',
templateUrl: 'example-component.html',
})
export class ExampleComponent implements OnDestroy {
constructor(public exampleFormService: ExampleFormService) {}
sendForm() {...}
}
the generated service looks like this
export class ExampleFormService {
form: FormGroup;
constructor(private exampleService: ExampleService) {
this.form = new FormGroup({
email: new FormControl(undefined, [Validators.email, Validators.required]),
});
}
}
@NgModule({
imports: [
...,
ExampleModule,
...,
],
})
export class YourModule {}
@NgModule({
imports: [
FormsSharedModule,
NgrxStoreModule.forFeature(selectorName, ExampleReducer),
NgrxEffectsModule.forFeature([ExampleEffects]),
],
providers: [
ExampleService,
ExampleFormService,
],
})
export class ExampleModule {}
In the component, send the above created form via sendForm()
method. Notice the way a generated anction is dispatched.
import {Component, OnDestroy} from '@angular/core';
import {Store} from '@ngrx/store';
import {takeUntil} from 'rxjs/operators';
import {Subject} from 'rxjs/Subject';
import {ExampleFormService} from '../../generated/store/example/exampleModule/example.service';
import {Start as ExampleStart} from '../../generated/store/example/exampleModule/states/actions';
import {AppState} from '../states/exmaple.models';
@Component({
selector: 'example-component',
templateUrl: 'example-component.html',
})
export class ExampleComponent implements OnDestroy {
constructor(
public exampleFormService: ExampleFormService,
private store: Store<AppState>,
) {}
sendForm() {
this.store.dispatch(new ExampleStart(this.exampleFormService.form.value));
}
ngOnDestroy() {
this.ngDestroy.next();
this.ngDestroy.complete();
}
}
Success
action is dispatched (payload is the server response data)Error
action is dispatched (payload is the error message sent from the server)@Injectable()
export class ExampleEffects {
CreateProductCategory = createEffect(() => this.storeActions.pipe(
ofType<actions.Start>(actions.Actions.START),
switchMap((action: actions.Start) => this.exampleService.exampleEndpointMethod(action.payload)
.pipe(
map(result => new actions.Success(result)),
catchError((error: HttpErrorResponse) => of(new actions.Error(error.message))),
),
),
));
constructor(
private storeActions: Actions,
private adminproductService: AdminProductService,
) {}
}
Success
/ Error
actions dispatched by the generated effect and stores the payloads to the store
export interface ExampleState {
data: __model.ExampleServerResponseInterface;
loading: boolean;
error: string;
}
export const initialExampleState: ExampleState = { data: null, loading: false, error: null, };
export const selectorName = 'Example';
export const getExampleSelector = createFeatureSelector
export function ExampleReducer( state: ExampleState = initialExampleState, action: actions.ExampleAction): ExampleState { switch (action.type) { case actions.Actions.START: return {...state, loading: true, error: null}; case actions.Actions.SUCCESS: return {...state, data: action.payload, loading: false}; case actions.Actions.ERROR: return {...state, error: action.payload, loading: false}; default: return state; } }
##### Component (again)
- the data can be retrieved by subscribing to the store
```typescript
ngOnInit() {
this.exampleState = this.store.pipe(
takeUntil(this.ngDestroy),
select(getExampleSelector));
// OR
this.data = this.store.select(s => ExampleState.data)
this.loading = this.store.select(s => ExampleState.loading)
this.error = this.store.select(s => ExampleState.error)
}
tags
attribute defined. In addition, there must be exactly one tag defined.
The http methods are grouped to services based on the tags, i.e. if two methods have tag "order", both will be
generated inside Order.tsget
and delete
methods do not contain body
host
and basePath
so that each generated service method can contain a link to the swagger UI method reference, e.g. http://example.com/swagger/swagger-ui.html#!/Order/Order
definitions
section in swagger file does not contain types with inline definitions, i.e. only named subtypes worktests
folderTo run client tests in interactive mode:
npm test
git checkout -b tech/release
on master or other branch you want to releasenpm version patch
or other version change you wantnpm publish
Please do the following before making a PR:
npm run build
.npm test
.npm run eslint
.