Daniel Sogl
Posted on April 11, 2024
As an Angular trainer and consultant, I've encountered numerous Angular projects. Many developers express their dislike for writing boilerplate code, such as with Redux, or when defining API-Services alongside response models. In this blog post, I'll demonstrate how to use Swagger and its API generator to create Angular HTTP-Services, API-DTOs, and configure the endpoint based on your deployment environment.
What is Swagger
Swagger is a fantastic open-source toolset that's perfect for developing and describing RESTful APIs. It offers you a user-friendly interface to understand a service's capabilities without needing to dig into the code. It even provides a way for you to interact directly with the API, which means you get to test its functionality.
What makes Swagger really stand out is how it streamlines the process of defining and documenting APIs. It offers a centralized, language-agnostic definition of an API. This means that both you and machines can easily discover and understand the capabilities of the service. Thanks to Swagger, you can avoid getting bogged down in extensive documentation or wasting time explaining how your API works, because the API definition is self-explanatory and easy to understand.
Swagger offers more than just a user-friendly interface for exploring APIs. It also provides multiple generators that can produce code typically written by hand. As an Angular developer, this blog post will focus on the typescript-angular
generator.
How to Set Up the Swagger Generator
I will guide you through the process of setting up the typescript-angular generator. I will define a separate Angular library to keep the generated code away from our application code. I will also create a script that pulls the current swagger config so you don't have to check the current version by yourself and manage the required swagger definition file in your Angular repository. Last but not least, I will show you how to configure the base path of the endpoint based on your environment. If you want to take a closer look into the presented code you can also check out the demo project published on GitHub.
Setting Up the Library
First of all, I will create a separate Angular Library with the help of the Angular CLI, so we can keep the generated code away from our application code.
ng g lib api
This will generate a library called api
inside a folder named projects
. It also modifies our tsconfig.json
file by adding a path to the dist
folder. Because we don't want to build the API lib each time we generate the code, we can instead point to the public_api.ts
file inside the api lib src folder.
{
"compileOnSave": false,
"compilerOptions": {
"paths": {
"api": ["./projects/api/src/public-api.ts"]
},
...
},
}
This helps us later to import the generated code from our library without referencing to the absolute/relative paths as shown below.
import { Configuration, ConfigurationParameters } from 'api';
Setting Up the Generator
To generate our API, I will first add a dev dependency to the project. Under the hood, the generator will execute a Java library, so it is also necessary to install the JDK on your system. I recommend using Homebrew on macOS.
brew install openjdk
...
npm install -D @openapitools/openapi-generator-cli@latest
After we've installed all the required dependencies, we can create a definition file to configure the generated code.
touch openapitools.json
chmod -R 777 openapitools.json // can be required later
After creating the file, I define the generator configuration. It's possible to define as many generators as needed in your project. So, if your application accesses multiple endpoints or microservices, just extend the config as needed. In the shown example below, I'm defining the output path of the generator as well as some useful parameters. These are just my go-to parameters. If you'd like to dig deeper into the generator, I recommend you read through the documentation. The configuration may vary depending on when you're reading this article.
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.4.0",
"generators": {
"pet-store-api": {
"generatorName": "typescript-angular",
"glob": "swagger.json",
"output": "projects/api/src/lib",
"additionalProperties": {
"apiModulePrefix": "PetStore",
"withInterfaces": "true",
"fileNaming": "kebab-case",
"useSingleRequestParameter": "true",
"supportsES6": "true",
"stringEnums": "true"
}
}
}
}
}
To execute the generator, I create another file. The script not only executes the generator but also downloads the latest swagger definition file from the web and cleans up the library folder. This prevents us from committing deleted files to our git repository.
touch generate-api.sh
chmod -R 777 generate-api.sh
#!/bin/bash
# download the latest swagger definition file
curl -o swagger.json https://petstore.swagger.io/v2/swagger.json
# remove the existing api folder
npx rimraf projects/api/src/lib
# generate the api client
npx openapi-generator-cli generate --generator-key=pet-store-api
# delete the swagger definition file
rm ./swagger.json
I also recommend adding a custom script to your package.json
to execute the script using the npm run
command.
{
"name": "ng-swagger-generator",
"version": "0.0.0",
"scripts": {
...
"generate:pet-store-api": "./generate-api.sh"
},
...
}
Now it's time to execute the generator using the previously defined script
npm run generate:pet-store-api
When you take a closer look at the generated code, you will find two new folders inside the API project. Inside the api
folder, you will find the generated services that we will later import to call our backend. Inside the model
folder, you will find all the generated data models (in this case, interfaces).
Only one last step is required to be able to use the create api library in our Angular application. Adjust the existing public_api.ts
file to export the index.ts
file from the generate code.
/*
* Public API Surface of api
*/
export * from './lib';
Using the Generated API Code
As I write this post, the Swagger Angular generator does not support the new standalone API out of the box, so I had to wrap the import using the importProvidersFrom
helper function. However, as you can see, the configuration of the generated library is simple. First, define a configuration factory. In my example, I am using the static environment file. For larger applications, I recommend using a configuration service that fetches the API endpoints from the backend to maintain flexibility. After that, you need to import the generated PetStoreApiModule and provide the previously defined config factory. If your backend requires basic authentication, you can also provide the username
and password
parameters. Also don't forget to provide the HttpClient by adding the provideHttpClient
function.
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { Configuration, ConfigurationParameters, PetStoreApiModule } from 'api';
import { provideHttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
export function apiConfigFactory(): Configuration {
const params: ConfigurationParameters = {
basePath: environment.basePath,
// if needed
password: 'password',
username: 'user',
};
return new Configuration(params);
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
importProvidersFrom(
PetStoreApiModule.forRoot(apiConfigFactory)
),
],
};
Now, we can import the also-generated PetService to communicate with our backend. We can also use the generated data models to work in a type-safe manner.
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Pet, PetService } from 'api';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
private readonly client = inject(PetService);
constructor() {
this.client.getPetById({ petId: 1 }).subscribe((pet: Pet) => {
console.log(pet);
});
}
}
Thats it! You now have a generated API-library with pre defined Http-Services and data models that can be used inside your application code without writing any single line of code again!
Conclusion
Utilizing Swagger streamlines Angular development by automating the creation of HTTP-Services and API-DTOs, drastically reducing manual coding and minimizing errors. It's important to note, however, that leveraging Swagger effectively does require upfront effort in the backend to provide comprehensive Swagger documentation. Despite this, the investment pays off by significantly easing frontend development tasks.
I encourage you to integrate Swagger into your Angular projects and share your insights. How did it impact your development workflow? Were there any challenges or valuable lessons learned along the way? Your experiences not only enrich our collective knowledge but also guide our exploration of new topics.
What other Angular tools or strategies would you like to see discussed? Your suggestions are crucial for shaping our future content.
Happy coding, and I look forward to your stories and ideas!
Posted on April 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.