Improve Angular code with Betterer

 64 total views,  3 views today

Introduction

In software development, when developers work on a project for a period of time, they tend to add code smell into the code that Eslint rules identify as problems. When architect adds a new Eslint rule to fix these errors, npm run lint reports error messages in terminal that require immediate attention.

I have two approaches to handle Eslint errors at work.

When npm run lint returns few errors, I fix all of them at once, verify on my machine and push the codes to remote repository.

When the number of errors is significant, I choose a few to fix, turn off the Eslint rule and git push the code to remote repository, Then, I enable the rule, re-run npm run lint and repeat the process. Therefore, the process continues until the Eslint rule passes and I can enable the rule permanently in the project.

Approach two is tedious and it disrupts the flow of developer by enabling and disabling the rule to fix issues. Fortunately, Craig Spence created betterer to improve the code base incrementally and I can throw approach two out of the window.

Add code smell to Angular

I add code smell to the project to demonstrate betterer. The example calls async/await statements in a for loop to sum some numbers in ngOnit function.

sum = 0
async ngOnInit(): Promise<void> {
  for (let i = 1; i <= 4; i++) {
    this.sum += await this.square(i)
    this.sum += await this.cube(i)
  }
}

private async square(num: number): Promise<number> {
  return Promise.resolve(num * num)
}

private async cube(num: number): Promise<number> {
  return Promise.resolve(num * num * num)
}

Executing await in a loop is code smell and Eslint even has a no-await-in-loop rule to check for its existence.

Next, I will show the readers how to write betterer test to look for await in a loop and to improve the test values by refactoring the code.

Install dependencies

npx @betterer/cli init
npm i --save-dev @betterer/angular @betterer/eslint

The npx command generates betterer script in package.json and creates a blank betterer.ts for the tests as a result.

package.json
{
  "betterer": "betterer"
}

Write betterer tests for Eslint and Angular

Writing tests for Eslint and Angular is very simple because betterer library provides eslint and angular functions that accept the name of the rule and its options.

// betterer.ts

import { angular } from '@betterer/angular'
import { eslint } from '@betterer/eslint'

export default {
  'no more await in loop': () => eslint({ 'no-await-in-loop': 'error' }).include('src/**/*.ts'),
  'stricter template compilation': () =>
    angular('./tsconfig.json', {
      strictTemplates: true,
    }).include('src/**/*.ts', 'src/**/*.html'),
}

eslint({ 'no-await-in-loop': 'error' }).include('src/**/*.ts') test looks for await in any loop in TypeScript files of src folder and outputs error messages.

Then, we execute npm run betterer to run the tests and store the initial values in betterer.results

“no more await in loop test” fails and finds two await calls in the for loop. Therefore, our task is to refactor ngOnInit to get rid of them.

(Optional) Run betterer in pre-commit hook

If your project has husky installed, you can run betterer precommit in pre-commit hook to update the values in betterer.result file

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
npm run betterer precommit --cache true

Refactor code to improve the values of betterer

// food-shell.component.ts
const sumAndCubePromises = await Promise.all(
  [1, 2, 3, 4].map(async (i) => (await this.square(i)) + (await this.cube(i))),
)

this.sum = sumAndCubePromises.reduce((acc, value) => acc + value)

I refactor the codes to create all promises and obtains the results in await Promise.all(). The result is an array of numbers and we compute the total by Array.reduce().

Code rewrite eliminates both for loop and await calls; thus, the betterer values get better.

The “no more await in loop” test has met its goal while “stricter template compilation” test stays the same.

Since the code base does not have await in loop, I can delete the test and add the eslint rule in the configuration file

Remove betterer test that has met it’s goal
Add no-await-in-loop rule in eslintrc.json configuration file

After I commit the latest changes, betterer is run again to update betterer.results file.

The terminal displays the result of the only test and the test value stays the same.

Final thought

Betterer allows developers to add tests to improve codes in stages. These tests stay on until the issues are resolved. When one issue is fully resolved, we can remove the corresponding test and include the rule to eslintrc configuration file. One benefit is to make better codes without widespread changes. The other benefit is prevent massive code refactoring that breaks existing functionality.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, architecture and good engineering practice.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Betterer: https://phenomnomnominal.github.io/betterer/docs/introduction
  3. Angular Nation and Betterer w/Craig Spence: https://www.youtube.com/watch?v=BCdDEhNWpUU
  4. Craig Spence’s twitter: https://twitter.com/phenomnominal

Angular and Storybook – Mock Data in Component

 133 total views

Introduction

After building a few presentational components, I am ready to build a container component, food menu component, with them. Food menu component is responsible for displaying an option dropdown and a list of menu items.

This is the component tree of food menu component.

The functions of food menu component are to retrieve menu data from Netlify function, listen to selected option from the dropdown, filter the data and eventually pass it to food menu card to display.

Visualizing container component in Storybook involves mock data because we don’t want to pull data from real data sources. Angular makes it easy because Angular provides dependency injection to inject mock service to provide fake data.

The process is similar to mock data in Angular unit testing except the result is seen visually and I am going to show the readers how to do it.

Create Food menu component in food module

ng g c foodMenu --module=food
// food-menu.component.ts 
... omit import statement ...

import { Choice, MenuItem, OrderedFoodChoice } from '../interfaces'
import { FoodService } from '../services'

@Component({
  selector: 'app-food-menu',
  templateUrl: './food-menu.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodMenuComponent implements OnInit, OnDestroy {
  @Output()
  addDynamicFoodChoice = new EventEmitter<OrderedFoodChoice>()

  menuItems$: Observable<MenuItem[] | undefined>
  handleFoodChoiceSub$ = new Subject<OrderedFoodChoice>()
  menuOptionSub$ = new BehaviorSubject<string>(MenuOptions.all)
  unsubscribe$ = new Subject<boolean>()

  qtyMap: Record<string, number> | undefined

  constructor(private service: FoodService) {}

  ngOnInit(): void {
    const menuUrl = `${environment.baseUrl}/menu`

    this.menuItems$ = combineLatest([
      this.service.getFood(menuUrl),
      this.menuOptionSub$,
      this.service.quantityAvailableMap$,
    ]).pipe(
      map(([menuItems, option]) => ({
        menuItems,
        option,
      })),
      map(({ menuItems, option }) => this.filterMenuItems(menuItems, option)),
      takeUntil(this.unsubscribe$),
    )

    this.service.quantityAvailableMap$.pipe(takeUntil(this.unsubscribe$)).subscribe((updatedQtyMap) => {
      if (!updatedQtyMap) {
        this.qtyMap = undefined
      } else {
        this.qtyMap = {
          ...updatedQtyMap,
        }
      }
    })

    this.handleFoodChoiceSub$
      .pipe(
        tap(({ id, quantity }) => this.service.updateQuantity(id, quantity)),
        takeUntil(this.unsubscribe$),
      )
      .subscribe((choice) => this.addDynamicFoodChoice.emit(choice))
  }

  .. omit trivial logic ...

  filterMenuItems(menuItems: MenuItem[] | undefined, option: string): MenuItem[] | undefined {
    ... return filtered menu item ...
  }
}

FoodMenuComponent injects FoodService that retrieves data from Netlify function and calculates remaining quantity of the menu items. Furthermore, the html template resolves the observable, this.menuItem$, to render the filtered menu items in the presentational components.

Set up mock service and data

Rather than using FoodService in Storybook, we create a mock service, MockFoodService, and implement the same functions that exist in FoodService.

// mock.ts

export class MockFoodService {
  private quantityAvailableSub$ = new BehaviorSubject<Record<string, number> | undefined>(undefined)
  quantityAvailableMap$ = this.quantityAvailableSub$.asObservable()

  constructor(private menuItems?: MenuItem[]) {}

  private buildQtyMap(): Record<string, number> | undefined {
    if (!this.menuItems) {
      return undefined
    }
    return this.menuItems.reduce((acc, mi) => {
      mi.choices.forEach(({ id, quantity }) => {
        acc[id] = quantity
      })
      return acc
    }, {} as Record<string, number>)
  }

  getFood(): Observable<MenuItem[] | undefined> {
    const qtyMap = this.buildQtyMap()
    this.quantityAvailableSub$.next(qtyMap)
    return of(this.menuItems)
  }

  updateQuantity(id: string, quantity: number): void {
    const qtyAvailableMap = this.quantityAvailableSub$.getValue()
    if (qtyAvailableMap) {
      const oldQty = qtyAvailableMap[id]
      const nextQty = oldQty - quantity
      if (nextQty >= 0) {
        this.quantityAvailableSub$.next({
          ...qtyAvailableMap,
          [id]: nextQty,
        })
      }
    }
  }

  private getLatestQtyMap() {
    const qtyAvailableMap = this.quantityAvailableSub$.getValue()
    if (!qtyAvailableMap) {
      const qtyMap = this.buildQtyMap()
      this.quantityAvailableSub$.next(qtyMap)
    }
    return this.quantityAvailableSub$.getValue()
  }

  getQuantity(id: string): number {
    const qtyAvailableMap = this.getLatestQtyMap()
    if (qtyAvailableMap) {
      return qtyAvailableMap[id] || 0
    }
    return 0
  }
}

The constructor of MockFoodService expects MenuItem array; therefore, we prepare static mock data in a separate file.

// constants.ts

export const MockData: MenuItem[] = [
  {
    id: '1',
    question: 'Which appetizer(s) do you wish to order?',
    choices: [
      {
        id: 'a',
        name: 'Egg salad',
        quantity: 10,
        ... other properties ...
      },
      {
        id: 'd',
        name: 'Buffalo Chicken Wings',
        quantity: 10,
        ... other properties ...
      },
      {
        id: 'b',
        name: 'Oven Baked Zucchini Chips',
        quantity: 10,
        ... other properties ...
      },
    ],
  },
  {
    id: '2',
    question: 'Which dessert(s) do you wish to order?',
    choices: [
      {
        id: 'a1',
        name: 'Ice cream',
        quantity: 10,
        ... other properties ...
      },
      {
        id: 'b1',
        name: 'Tiramisu',
        quantity: 10,
        ... other properties ...
      },
    ],
  },
]

Mock service and data is in place and we can finally add new storybook to visualize the food menu.

Add Storybook for Food Menu Component

Create food-menu.stories.ts under the food-menu folder

// food-menu.storeis.ts

import { HttpClientModule } from '@angular/common/http'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Meta, moduleMetadata, Story } from '@storybook/angular'

import { FoodChoiceComponent } from '../food-choice'
import { FoodChoiceFormComponent } from '../food-choice-form'
import { FoodMenuCardComponent } from '../food-menu-card'
import { FoodMenuOptionComponent } from '../food-menu-option'
import { FoodQuestionComponent } from '../food-question'
import { FoodService } from '../services'
import { MockData, MockFoodService, SoldOutMockData } from '../storybook-mock'
import { FoodMenuComponent } from './food-menu.component'

export default {
  title: 'Food Menu',
  component: FoodMenuComponent,
  decorators: [
    moduleMetadata({
      declarations: [
        FoodChoiceComponent,
        FoodQuestionComponent,
        FoodChoiceFormComponent,
        FoodMenuCardComponent,
        FoodMenuOptionComponent,
      ],
      imports: [ReactiveFormsModule, FormsModule, HttpClientModule],
      providers: [
        {
          provide: FoodService,
          useFactory: () => new MockFoodService(MockData),
        },
      ],
    }),
  ],
  argTypes: { onClick: { action: 'clicked' } },
} as Meta

const Template: Story<FoodMenuComponent> = (args: FoodMenuComponent) => ({
  props: args,
})

export const Menu = Template.bind({})

The magic of the storybook is the providers array of moduleMetadata where we use useFactory to inject an instance of MockFoodService for FoodService.

providers: [
  {
      provide: FoodService,
      useFactory: () => new MockFoodService(MockData),
   },
],

When Storybook invokes this.service.getFood() and this.service.quantityAvailableMap$ in ngOnInit() of FoodMenuComponent, it actually invokes the implementation defined in MockFoodService. Therefore, there is no dependency between Storybook and Netlify function.

After laying down all the hard work, we can launch the storybook application and see the new story in action.

Start storybook application

npm run storybook

Click the title Food Menu -> Menu and view the dropdown, menu questions and items.

When dropdown value is “Show all”, the menu displays all questions and choices.

Next, I select “Show sold out” in the dropdown and the menu displays an appetizer that is no longer served.

Last, I select “Show available only” in the dropdown and the menu displays appetizers and desserts that have positive quantity.

Final thought

Storybook has the ability to inject mock service for container component. Story loads fake data from mock service on behalf of the container component and pass it down to presentational components to display. Then, we can interact with components to confirm the expected behavior in the story. When the results are satisfactory, we can even publish the new story to Chromatic for collaboration (refer to “Angular and Storybook – Publish to Chromatic”).

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, Storybook and other web technologies.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Storybook: Create a template component: https://storybook.js.org/docs/angular/workflows/stories-for-multiple-components#creating-a-template-component

Add value to commit message in Angular

 73 total views

Introduction

When a team of developers works on the project, we should add tools to ensure all developers adopt the same convention of commit messages. The tools ng-spanish-menu application use to enforce commit conventions are commitlint, commitizen and husky.

show me, don't tell me

Install dependencies of commitlint and husky locally

First, we install commitlint and husky to set up automated testing of commit messages. The automation occurs when we run git commit command.

npm install --save-dev @commitlint/{cli,config-conventional}
npm install --save-dev husky

Generate commitlint configuration file

Secondly, we add a new configuration file for commitlint.

echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > commitlint.config.js

Test

Before we go any further, we launch a new terminal to test comments using commitlint

Failed test

In our first example, we display a comment that does not adopt the convention and the program outputs the error messages.

echo “hello world” | ./node_modules/.bin/commitlint

Passed test

In our second example, this comment adopts the convention and the program outputs nothing when run.

echo “chore(test): hello world” | ./node_modules/.bin/commitlint

Add husky hook to lint of commit messages

Thirdly, we automate commitlint by registering a commit-msg husky hook.

# Active hooks
npx husky install

# Add hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

When developers type git commit -m "{type}{scope}: subject" to commit changes, commitlint validates the format of the message before the staged files are committed.

However, reckless developers can add –no-verify to bypass husky and defeat the intention of good engineering practice.

By chance, I discovered commitzen/cz-cli that launches interactive interface to prompt user to fill in required information and it also ignores –no-verify flag

Install commitizen

Lastly, we install the required dependencies and we should all set.

npm install --save-dev commitizen
npm install --save-dev @commitlint/prompt

Generate commitizen configuration file

echo '{ "path": "@commitlint/prompt" }' > ~/.czrc

Create npm script for commitizen

"scripts: {
   "commit": "git-gz"
}

See commitizen in action

Finally, we can make changes to the codes and witness commitzen in action for the first time.

git add .
npm run commit

Type help to see all available types to choose from and I choose “feat” as an example.

Scope is optional and I type food to indicate the change is about food service.

Enter :skip when commitizen prompts body and footer to skip providing descriptions.

Git commits my new changes successfully and keeps the message in git history.

Conclusion

Commitizen is a powerful command-line utility for advocating and sharing commit convention. It is especially when multiple teams and vendors are working on a large project and we want them to write message that has the same format, descriptive and understandable.

This is the end of the post and I hope you find this article useful and consider to become upstanding commit citizens by using commitlint and commitizen in Angular application.

Good bye!

we made it to the end of the blog post

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Commitlint install guide: https://commitlint.js.org/#/guides-local-setup
  3. Commitizen: https://github.com/commitizen/cz-cli
  4. Husky: https://github.com/typicode/husky
  5. Add commitlint and commitizen to create-react-app: https://annacoding.com/article/4ynYYJEnB733BHEnbrApnX/Add-commitlint-and-commitizen-to-Create-react-app

Angular and Storybook – Publish to Chromatic

 93 total views

Introduction

In the ng spanish menu repo, I created some Angular components and storybooks for the application. I thought it would be an excellent idea to deploy the storybooks to Chromatic to share with other Angular developers for feedback.

Install Chromatic dependencies

npm i --save-dev chromatic

Create Chromatic app for Github repository

  • Sign in with Github account
  • Choose a project to add to Chromatic and set up npm script in package.json for publication
"scripts": {
    "chromatic": "npx chromatic --project-token=<Chromatic project token>",
}
  • Run "npm run chromatic" to manually publish build 1

Whenever we finish the design of components and test thoroughly in Storybook, we can publish them to Chromatic with the script.

However, the process is manual and interrupts developers’ concentration on coding. Developers’ time is valuable and developers should focus on building application rather than manual devOps. Therefore, we should leverage the Github action of Chromatic to automate it’s publish process.

Automate Chromatic with Github action

First, add a new repository secret to store the Chromatic project token.

Go to Settings -> Secrets -> New repository secret,

Name: CHROMATIC_PROJECT_TOKEN

Value: <the project token seen in chromatic script>

Second, in .github/workflow directory, create a main.yml that uses the Chromatic action

// main.yml

name: CI
on:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          persist-credentials: false
          fetch-depth: 0 # 👈 Required to retrieve git history

      - name: Setup node 14
        uses: actions/setup-node@v1
        with:
          node-version: 14.x

      - name: Publish to Chromatic
        uses: chromaui/action@v1
        # Chromatic GitHub Action options
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          exitZeroOnChanges: true 
          autoAcceptChanges: true

exitZeroOnChanges and autoAcceptChanges are optional properties and can be included depending on use cases.

exitZeroOnChanges: Option to prevent the workflow from failing

autoAcceptChanges: Option to accept all changes

Publish Storybook

Next, Make changes in the project and push codes to Github repository to trigger Github action to run.

Build passed and new changes were auto-accepted.

Finally, Select storybooks on the side menu to display components and interact with input fields to confirm outputs and events are emitted.

This is the end of the blog post and I hope the contents explain the necessary procedure to publish storybooks to Chromatic and share the static app in team for review and collaboration.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Chromatic Application: https://613821be390968003af8c529-lzttnlkywp.chromatic.com
  3. Deploy Storybook: https://storybook.js.org/tutorials/intro-to-storybook/angular/en/deploy/
  4. Automate Chromatic with Github Action: https://www.chromatic.com/docs/github-actions

How Angular calls CORS enabled Netlify Function

 76 total views

Introduction

In previous post (article), I created a Netlify function to return menu data in JSON format. In this post, I will show how to deploy the Netlify function to production environment and call the endpoint in the Angular Spanish Menu app from Github Page

show me, don't tell me

Enable CORS in Netlify function

Github page and Netlify have different domain names, we must enable CORS in our Netlify function to ensure that it accepts HTTP request from any domain.

First, we modify netlify.toml to add HTTP header, Access-Control-Allow-Origin to specify allowed origins.

[[headers]]
  # Define which paths this specific [[headers]] block will cover.
  for = "/*"
  [headers.values]
    Access-Control-Allow-Origin = "*"

Deploy Netlify function to production

Second, we will deploy the function to production site such that our Angular app can access it in Github page.

  1. Login Netlify site
  2. Choose the site, navigate to Deploys menu and click Trigger deploy button to deploy the function manually
deploy netlify function to production

Update Angular environment variable

Third, we will update menuUrl variable in environment.prod.ts to point to the function at netlify.app.

export const environment = {
  production: true,
  menuUrl: 'https://<site name>.netlify.app/.netlify/functions/menu',
}

Deploy Angular to Github page

Next, we deploy our Angular app to Github page to verify integration with Netlify function actually works.

  • Install angular-cli-ghpages.
ng add angular-cli-ghpages
  • Add ng deploy configuration in angular.json
"deploy": {
   "builder": "angular-cli-ghpages:deploy",
   "options": {
       "baseHref": "https://railsstudent.github.io/ng-spanish-menu/",
       "name": "<user name>",
       "email": "<user email>",
       "message": "<github commit message>",
       "repo": "https://github.com/railsstudent/ng-spanish-menu.git",
       "noSilent": true
   }
}
  • Simplify deployment of our app by auhoring a new npm script
"github-page-deploy": "ng deploy"

npm run github-page-deploy deploys our app to Github page on our behalf

Finally, we can browse our application to verify integration with the production Netlify function.

amost there

Verify integration

Browse https://railsstudent.github.io/ng-spanish-menu/food

browser angular app in github page

Open Network tab to verify that our application calls menu function successfully. It is made possible because the value of access-control-allow-origin response header is “*”.

result of http response

The result of the HTTP response is a valid JSON object containing our food menu.

Conclusion

We made few configuration changes and our Angular application can load data from the Netlify function.

This is the end of the post and I hope you find this article useful and consider to use serverless function to prototype Angular application.

Good bye!

we made it to the end of the blog post

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Github Page: https://railsstudent.github.io/ng-spanish-menu/food
  3. Access-Control-Allow-Origin Policy – https://answers.netlify.com/t/access-control-allow-origin-policy/1813/6
  4. https://www.netlify.com/blog/2021/01/25/getting-started-with-netlify-functions-for-angular/

Build Angular app with Netlify function

 182 total views

Introduction

I created a container component, FoodMenuComponent, to display a menu for users to order food and drink.

There are several ways to create the menu data in JSON format:

  1. Hardcoded data in the component
  2. Create static JSON data in assets folder and use a service to retrieve the data for the component
  3. Design a backend API and use a service to call the endpoint to retrieve the data
  4. Add a serverless function to return the menu data to the component

Option 1 makes component bloated because it contains both business logic and data. Business logic and data should belong to service and external data source respectively.

Option 2 is ideal for Angular beginners such that they don’t have to worry about backend design and implementation.

Option 3 requires developers to have knowledge of one or more backend frameworks, for example, Express and NestJS. Because of the additional skills, it demands more development efforts for a simple side project.

After weighing all alernatives, I chose Netlify function because I can leverage backend capability of cloud provider instead of building my own backend server.

Result

This is the end result of FoodMenuComponent

The menu asks two questions and each question has at least one choice for user to choose.

Let me show you how to create a Netlify function to return menu data that FoodMenuComponent consumes and displays on browser.

Install Netlify CLI

Firstly, we have to install Netlify CLI globally.

npm install netlify-cli -g

Create Netlify function Configuration

Secondly, we create netlify.toml that is the configuration file of Netlify. Add the following sections to toml file:

[build]
  publish = "dist/ng-spanish-menu"
  command = "ng build"
  functions = "./src/assets/functions"
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

and the most important line is functions = "./assets/functions".

functions = "./assets/functions" specifies the folder of Netlify functions and all functions should save to it.

Then, start Netlify CLI with Angular in development mode

ntl dev

and open the application at http://localhost:8888

Setting up First Severless Function

Thirdly, we define our first serverless function to return menu data. We can find the function in ./assets/functions/menu.ts.

// menu.ts 

import { Handler } from "@netlify/functions";

const data = {
  "menu": [
    {
      "id": "1",
      "question": "Which appetizer(s) do you wish to order?",
      "choices": [
        {
          "id": "a",
          "name": "Egg salad",
          "description": "Egg salad",
          "currency": "USD",
          "price": 4.99,
          "available": true
        },
        ... omitted for the sake of brevity ...
      ]
    },
    {
      "id": "2",
      "question": "Which dessert(s) do you wish to order?",
      "choices": [
        {
          "id": "a1",
          "name": "Ice cream",
          "description": "Ice cream",
          "currency": "USD",
          "price": 1.99,
          "available": true
        },
        ... omitted for the sake of brevity ...
      ]
    }
  ]
}

const handler: Handler = async () => {
  return {
    statusCode: 200,
    body: JSON.stringify(data),
  };
};

export { handler };

Testing Severless Function

Next, we launch Postman application to test the GET endpoint to verify it is working as expected.

Get Endpoint: http://localhost:8888/.netlify/functions/menu

If the request is successful, the response displays menu data.

Adding endpoint in Angular

Lastly. open environment.ts and add a new environment variable, menuUrl, to it. Next, we will create food service to retrieve menu data through this endpoint.

export const environment = {
  production: false,
  menuUrl: '/.netlify/functions/menu',
}

Creating food service

@Injectable({
  providedIn: 'root',
})
export class FoodService {
  constructor(private http: HttpClient) {}

  getFood(url: string): Observable<MenuItem[] | undefined> {
    return this.http.get<Menu>(url).pipe(
      pluck('menu'),
      catchError((err: Error) => {
        console.error(err)
        return of(undefined)
      }),
      share(),
    )
  }
}

getFood function requests menu data from our serverless function and returns the result as Observable<MenuItem[] | undefined>. When request fails, we return undefined since menu does not exist.

Calling service in Angular component with RxJS

Lastly, FoodMenuComponent can combine RxJS operators and food service to retrieve menu data and assign to Observable<Menu[] | undefined>.

@Component({
  selector: 'app-food-menu',
  templateUrl: './food-menu.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodMenuComponent implements OnInit, OnDestroy {
  menuItems$: Observable<MenuItem[] | undefined>

  ngOnInit(): void {
    this.menuItems$ = this.service
       .getFood(environment.menuUrl)
       .pipe(takeUntil(this.unsubscribe$))
  }

  menumItemTrackByFn(index: number, menuItem: MenuItem): string | number {
    return menuItem ? menuItem.id : index
  }

  choiceTrackByFn(index: number, choice: Choice) {
    return choice ? choice.id : index
  }
  ... Omitted for the sake of brevity ....
}

In the template of FoodMenuComponent, async pipe resolves menuItems$ into MenuItem array (menuItems) and Angular provides ngFor directive that can iterate an array to render values in each row.

<div class="container" *ngIf="menuItems$ | async as menuItems; else notAvailable">
  <app-food-menu-card *ngFor="let menuItem of menuItems; index as i; trackBy: menumItemTrackByFn">
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice [choice]="choice" (foodChoiceAdded)="handleFoodChoice.emit($event)"></app-food-choice>
    </ng-container>
  </app-food-menu-card>
</div>
<ng-template #notAvailable>No menu</ng-template>

The container div iterates menuItems and passes each menuItem to <app-food-menu-card> element. Each menuItem has one question and each question displays some options for user to select. <app-food-question> is responsible for rendering menu item question and <app-food-choice> renders all available and unavailable options of the question.

We have reached the end of the post and FoodMenuComponent is working. You should see the same component that I showed at the beginning of the post.

We are finally at the end of the post! I hope you find this article useful and leverage serverless function in your Angular . Good bye!

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. https://www.netlify.com/blog/2021/01/25/getting-started-with-netlify-functions-for-angular/

Angular and Storybook – Component with content projection

 74 total views

In Bonnie’s visual DOM course, I learnt the technique of applying multiple content projections in a component.

I created a FoodMenuCardComponent with two content projections; i.e., a component with two <ng-content> elements. The first <ng-content> has selector ‘head’ that projects FoodQuestion component and the second <ng-content> has selector ‘body’ that projects a list of FoodChoice components.

The same result can be achieved without content projections but I want to practice the new concept and create a reusable card component that has head and body sections.

Create a food menu card component in food module

ng g c foodMenuCard --module=food
// food-menu-card.component.ts 
import { Component } from '@angular/core'

@Component({
  selector: 'app-food-menu-card',
  template: `
  <div>
    <ng-content select="[head]"></ng-content>
    <ng-content select="[body]"></ng-content>
  </div>`,
})
export class FoodMenuCardComponent {}

Create Storybook for Food Menu Card Component

Create food-menu-card.stories.ts under the food folder

// food-menu-card.storeis.ts

import { moduleMetadata } from '@storybook/angular'
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { action } from '@storybook/addon-actions'
import { Story, Meta } from '@storybook/angular/types-6-0'
import { FoodChoiceComponent, FoodMenuCardComponent, FoodQuestionComponent } from '@/food'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

export default {
  title: 'Food Menu Card',
  component: FoodMenuCardComponent,
  decorators: [
    moduleMetadata({
      imports: [ReactiveFormsModule, FormsModule],
      declarations: [FoodQuestionComponent, FoodChoiceComponent],
    }),
  ],
} as Meta

const FoodMenuCardTemplate: Story<FoodMenuCardComponent> = (args: FoodMenuCardComponent) => ({
  props: {
    ...args,
    foodChoiceAdded: action('foodChoiceAdded'),
  },
  template: `<app-food-menu-card>
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice [choice]="choice" (foodChoiceAdded)="foodChoiceAdded($event)"></app-food-choice>
    </ng-container>
  </app-food-menu-card>`,
})

export const Primary = FoodMenuCardTemplate.bind({})
Primary.args = {
  menuItem: {
    id: '1',
    question: 'Do you wish to order dessert?',
    choices: [
      {
        id: 'd',
        name: 'Buffalo Chicken Wings',
        description: 'Spicy chicken wings',
        price: 8.99,
        available: true,
      },
    ],
  },
}

export const Soldout = FoodMenuCardTemplate.bind({})
Soldout.args = {
  menuItem: {
    id: '1',
    question: 'Do you wish to order dessert?',
    choices: [
      {
        id: 'a',
        name: 'Egg salad',
        description: 'Egg salad',
        price: 4.99,
        available: false,
      },
    ],
  },
}

To order to create story for component with content projection, my solution is to create template component for FoodChoiceMenuComponent

template: <app-food-menu-card>
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice [choice]="choice" (foodChoiceAdded)="foodChoiceAdded($event)"></app-food-choice>
    </ng-container>
</app-food-menu-card>

<app-food-menu-card> is root element containing <app-food-question> and <ng-container> children. <app-root-question> has head attribute and replaces <ng-content select='[head]’> element whereas <ng-container> has body attribute and replaces <ng-content select='[body]’> element.

Two stories are created for FoodMenuCardComponent: Primary and Soldout. Primary renders question and available food choices whereas Soldout renders question and a sold out message.

Unfortunately, Primary story did not log action when Submit button was clicked. After googling, I found the solution in Github issue and Storybook examples repo. I had to pass addon action to props to listen to foodChoiceAdded event emitter of FoodChoiceComponent

props: {
    ...args,
    foodChoiceAdded: action('foodChoiceAdded'),
}

Property name is foodChoiceAdded that is the name of the event emitter. Action name is ‘foodChoiceAdded’ but it can be arbitrary text such as action(‘log’). When button is clicked, Storybook Actions tab logs ‘foodChoiceAdded’ name and its data.

Start storybook application

npm run storybook

Click the title Food Menu Card -> Primary and the component is rendered with a question and once choice

When quantity is updated and submitted, event is emitted and logged on Actions tab

Click the title Food Menu Card -> Soldout and the component is rendered with a question and a sold out message.

This is the end of the blog post and we will keep you posted after progress is made, thanks.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Storybook: Create a template component: https://storybook.js.org/docs/angular/workflows/stories-for-multiple-components#creating-a-template-component
  3. Angular Storybook Netlify App: https://storybooks-angular.netlify.app/?path=/story/custom-style–default
  4. Angular Storybook Examples: https://github.com/storybookjs/storybook/blob/next/examples/angular-cli/src/stories/core/styles/story-styles.stories.ts
  5. Action Logger does not handle event with template prop: https://github.com/storybookjs/storybook/issues/4820

Angular and Storybook – Simple Component with inputs and actions

 51 total views

Previously, I created a story book for simple component with inputs only (link) . In this post, I want to add new stories for a simple components with both input and action.

The component is FoodChoiceComponent that has two states: choice available to order and choice that has sold out.

If choice is available, user can input quantity and submit the reactive form. Otherwise, the component hides the form and displays a message, “Sorry, this choice is sold out”.

Create a food choice component in food module

ng g c foodChoice --module=food

Food choice component is a simple presentational component that is consisted of name, description, price, quantity and a reactive form.

// food-choice.component.ts 

// for the sakes of brevity, import statements are omitted

@Component({
  selector: 'app-food-choice',
  templateUrl: './food-choice.component.html',
  styleUrls: ['./food-choice.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodChoiceComponent implements OnInit, OnDestroy {
  @Input()
  choice: Choice

  @Output()
  foodChoiceAdded = new EventEmitter<OrderedFoodChoice>()

  submitChoice$ = new Subject<Event>()
  unsubscribe$ = new Subject<boolean>()
  processing = false

  form = this.fb.group({
    quantity: new FormControl(1, [Validators.required, Validators.min(1)]),
  })

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.submitChoice$
      .pipe(
        tap(($event) => {
          $event.preventDefault()
          $event.stopPropagation()
          this.processing = true
        }),
        delay(1000),
        map(() => ({
          ...this.form.value,
          name: this.choice.name,
          description: this.choice.description,
          price: this.choice.price,
        })),
        tap(() => (this.processing = false)),
        takeUntil(this.unsubscribe$),
      )
      .subscribe((value) => this.foodChoiceAdded.emit(value))
  }

  get quantity() {
    return this.form.get('quantity') as FormControl
  }

  ngOnDestroy(): void {
    this.unsubscribe$.next(true)
    this.unsubscribe$.complete()
  }
}
// food-choice.component.html

<div class="container">
  ...
  <ng-container [ngSwitch]="choice.available">
    <form [formGroup]="form" (ngSubmit)="submitChoice$.next($event)" *ngSwitchCase="true">
      <label name="quantity" class="item">
        <span class="field">Quantity: </span>
        <input type="number" formControlName="quantity" />
      </label>
      <div *ngIf="quantity.invalid && (quantity.touched || quantity.dirty)" class="alert alert-danger">
        <div *ngIf="quantity.errors?.min" class="error">Quantity must be at least {{ quantity.errors?.min?.min }}.</div>
      </div>
      <div *ngIf="processing" class="alert">
        <div class="processing">Loading...</div>
      </div>
      <button type="submit" [disabled]="processing || form.value.quantity <= 0">Submit</button>
    </form>
    <ng-container *ngSwitchDefault>
      <ng-container *ngTemplateOutlet="soldout; context: { choice: choice }"> </ng-container>
    </ng-container>
  </ng-container>
  <ng-template #soldout let-choice="choice">
    <span class="item sold-out">Sorry, {{ choice.name }} is sold out</span>
  </ng-template>
</div>

Input is a Choice interface that has several properties

export interface Choice {
  id: string
  name: string
  description: string
  price: number
  available: boolean
}
If available is true, reactive form is rendered.
If available is false, a message is displayed to indicate that the choice is sold out.

The food choice component has an event emitter that emits the user’s choice to container component. When user submits the reactive form, the submit event notifies RxJS subject to process the data and to emit the result to container component.

<form [formGroup]="form" (ngSubmit)="submitChoice$.next($event)" *ngIf="choice.available; else soldout">
....
</form>
submitChoice$ = new Subject<Event>()

@Output()
foodChoiceAdded = new EventEmitter<OrderedFoodChoice>()

form = this.fb.group({
  quantity: new FormControl(1, [Validators.required, Validators.min(1)]),
})

ngOnInit(): void {
    this.submitChoice$
      .pipe(
        tap(($event) => {
          $event.preventDefault()
          $event.stopPropagation()
          this.processing = true
        }),
        delay(1000),
        map(() => ({
          ...this.form.value,
          name: this.choice.name,
          description: this.choice.description,
          price: this.choice.price,
        })),
        tap(() => (this.processing = false)),
        takeUntil(this.unsubscribe$),
      )
      .subscribe((value) => this.foodChoiceAdded.emit(value))
}

Create Storybook for Food Choice Component

Create food-choice.stories.ts under the food folder

// food-choice.storeis.ts
import { moduleMetadata } from '@storybook/angular'
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/angular/types-6-0'
import { FoodChoiceComponent } from '@/food'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

export default {
  title: 'Food Choice',
  component: FoodChoiceComponent,
  decorators: [
    moduleMetadata({
      imports: [ReactiveFormsModule, FormsModule],
    }),
  ],
  argTypes: { onClick: { action: 'clicked' } },
} as Meta

const Template: Story<FoodChoiceComponent> = (args: FoodChoiceComponent) => ({
  props: args,
})

export const Primary = Template.bind({})
Primary.args = {
  choice: {
    id: '1',
    name: 'Vino tinto',
    description: 'Red wine',
    price: 12.99,
    available: true,
  },
}

export const Soldout = Template.bind({})
Soldout.args = {
  choice: {
    id: '1',
    name: 'Vino tinto',
    description: 'Red wine',
    price: 12.99,
    available: false,
  },
}

Two stories are created for FoodChoiceComponent, Primary and Soldout. Primary renders that state of an available food choice while Soldout renders the state of a sold out food choice.

Start storybook application

npm run storybook

Click the title Food Choice -> Primary and the component is rendered with reactive form

When quantity is updated and submitted, event is emitted and logged on Actions tab

Click the title Food Choice -> Soldout and the component is rendered with a sold out message.

This is the end of the blog post and we will keep you posted after progress is made, thanks.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Storybook: Build a simple component: https://storybook.js.org/tutorials/intro-to-storybook/angular/en/simple-component/
  3. Actions: https://storybook.js.org/docs/angular/essentials/actions

Angular and Storybook – Simple Component

 54 total views

I recently completed Visual DOM course at Angular Nation and the course covers advanced materials and is beneficial to Angular developers of any level. I personally struggle with the concept of view container ref (vcr) and would like to build a side project about it with my mentor, Nati.

The idea of the application is to displays a restaurant menu in Spanish (I am also learning Spanish), user can order food and beverages from the menu and the selection is added to the page dynamically through vcr. The project is at the early stage and we use storybook to visualize the components as the application comes along the way.

Create an angular application

ng new ng-spanish-menu

Install storybook dependencies

# Add Storybook:
npx sb init

The command adds storybook script in package.json and developer can run npm run storybook to launch storybook site at http://localhost:6006.

Create a food module

ng g module food

This module keeps food-related components and services.

Create a food card component in food module

ng g c foodCard --module=food

Food card component is a simple presentational component that displays name, description, price, quantity and total amount of a food/beverage.

// food-card.component.ts 

import { ChangeDetectionStrategy, Component, Input } from '@angular/core'

@Component({
  selector: 'app-food-card',
  templateUrl: './food-card.component.html',
  styleUrls: ['./food-card.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodCardComponent {
  @Input()
  name: string

  @Input()
  description: string

  @Input()
  price: number

  @Input()
  quantity: number

  get total() {
    return Math.round(this.price * this.quantity * 100) / 100
  }
}
// food-card.component.html

<div class="container">
  <label name="name" class="item">
    <span class="field">Name:</span>
    <span>{{ name }}</span>
  </label>
  <label class="item" name="description">
    <span class="field">Description:</span>
    <span>{{ description }}</span>
  </label>
  <label class="item" name="price">
    <span class="field">Price:</span>
    <span>{{ price }}</span>
  </label>
  <label class="item" name="quantity">
    <span class="field">Quantity:</span>
    <span>{{ quantity }}</span>
  </label>
  <label class="item" name="total">
    <span class="field">Total:</span>
    <span>USD {{ total }}</span>
  </label>
</div>

Create Storybook for Food Card Component

Create a new food folder under stories.

Create food-card.stories.ts under the food folder

// food-card.storeis.ts
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/angular/types-6-0'
import { FoodCardComponent } from '@/food'

export default {
  title: 'Food Card',
  component: FoodCardComponent,
} as Meta

const Template: Story<FoodCardComponent> = (args: FoodCardComponent) => ({
  props: args,
})

export const Primary = Template.bind({})
Primary.args = {
  name: 'Vino tinto',
  description: 'Red wine',
  price: 12.99,
  quantity: 3,
}

Then, input values are supplied to Primary.args and subsequently passed to FoodCardComponent to be displayed.

Start storybook application

npm run storybook

Click the title Food Card -> Primary and the component is rendered with initial input values.

Input values can be updated and then the component re-renders with new values.

CSS is very simple currently but Nati and I can work on the styling later after functionality is completed.

I am very excited of this project because Nati is very talented Angular developer and we can learn from each other while we work it together and she teaches me Spanish.

This is the end of the blog post and we will keep you posted after progress is made, thanks.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Install Storybook: https://storybook.js.org/docs/react/get-started/install
  3. Storybook: Build a simple component: https://storybook.js.org/tutorials/intro-to-storybook/angular/en/simple-component/

Swagger In NestJS

 38 total views

Problem: Frontend developers complained that integration with API is a painful process because there is no documentation on available endpoints, expected payloads and responses. Therefore, our backend team leads elect to enable swagger such that frontend developers can browse all the APIs on dedicated documentation site.

When they showed me how it was done, I was thoroughly impressed and wanted to show NestJS peers the simple steps.

Create a NestJS application

nest new nest-swagger

Install configuration, swagger and other dependencies

npm install --save @nestjs/config joi class-transformer class-validator
npm install --save @nestjs/swagger swagger-ui-express

Store environment variables in environment file

Store Swagger environment variables in .env and we will use configuration service to retrieve the values in main.ts

// main.ts
NODE_ENV=development
PORT=3000
API_VERSION=1.0
SWAGGER_TITLE=Local Swagger Documentation Site
SWAGGER_DESCRIPITON=The task API description

Import Configuration Module and validate schema

// envSchema.ts
import * as Joi from 'joi'

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),
  PORT: Joi.number().default(3000),
  API_VERSION: Joi.string().default('1.0'),
  SWAGGER_TITLE: Joi.string().required(),
  SWAGGER_DESCRIPITON: Joi.string().required(),
})
// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { validationSchema } from './envSchema'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Configure Swagger

Initialize Swagger in main.ts such that API documentation is generated automatically for controllers, entities and data transfer objects (DTOs).

// main.ts
async function bootstrap() {
   ....
   app.useGlobalPipes(
     new ValidationPipe({
       whitelist: true,
     }),
   )

   const configService = app.get(ConfigService)
   const version = configService.get<string>('API_VERSION', '1.0')
   const title = configService.get<string>('SWAGGER_TITLE', '')
   const description = configService.get<string>('SWAGGER_DESCRIPITON', '')

   const config = new DocumentBuilder()
     .setTitle(title)
     .setDescription(description)
     .setVersion(version)
     .addBearerAuth()
     .build()
   const document = SwaggerModule.createDocument(app, config)
   SwaggerModule.setup('api', app, document)

   const port = configService.get<number>('PORT', 0)
   await app.listen(port)
}

Enable Swagger plugin in nest-cli.json

// nest-clis.json
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": ["@nestjs/swagger"]
  }
}

After Swagger plugin is enabled, Swagger automatically generates documentation for controller and files with either .entity.s or .dto.ts suffix.

Verify Swagger is working by browsing http://localhost:3000/api

The documentation site works because it publishes the GET endpoint of app controller successfully.

Swagger setup is complete and we can add new API and publish the new endpoints in the dedicated documentation site.

Create a new Task Module to generate API documentation

nest g mo Task

Next, I generate task service and task controller and register them in the TaskModule. The following is the declaration of the TaskModule:

import { Module } from '@nestjs/common'
import { TaskController } from './task.controller'
import { TaskService } from './services'

@Module({
  controllers: [TaskController],
  providers: [TaskService],
})
export class TaskModule {}

Define new entities and new DTOs for Task API

// create-task.dto.ts
import { IsNotEmpty, IsString } from 'class-validator'

export class CreateTaskDto {
  @IsNotEmpty()
  @IsString()
  name: string
}

// update-task.dto.ts
import { IsBoolean, IsDefined } from 'class-validator'

export class UpdateTaskDto {
  @IsDefined()
  @IsBoolean()
  completed: boolean
}

Define new entities for the Task API

// task.entity.ts
import { IsBoolean, IsString } from 'class-validator'

export class Task {
  @IsString()
  id: string

  @IsString()
  name: string

  @IsBoolean()
  completed: boolean
}

// delete-result.entity.ts
import { IsNumber, IsOptional, Min, ValidateNested } from 'class-validator'
import { Task } from './task.entity'

export class DeleteResult {
  @IsNumber()
  @Min(0)
  count: number

  @IsOptional()
  @ValidateNested()
  task?: Task
}

Create CRUD methods in Task Service

Create basic methods to add, update, delete and retrieve a task object from a tasks array

// task.service.ts
import { BadRequestException, Injectable } from '@nestjs/common'
import { v4 } from 'uuid'
import { CreateTaskDto, UpdateTaskDto } from '../dtos'
import { DeleteResult, Task } from '../entities'

@Injectable()
export class TaskService {
  tasks: Task[] = [
    ...some tasks objects
  ]

  listAll(): Promise<Task[]> {
    return Promise.resolve(this.tasks)
  }

  findById(id: string): Promise<Task> {
    const task = this.tasks.find((task) => task.id === id)
    if (!task) {
      throw new BadRequestException('Task not found')
    }
    return Promise.resolve(task)
  }

  deleteById(id: string): Promise<DeleteResult> {
    const task = this.tasks.find((task) => task.id === id)
    this.tasks = this.tasks.filter((task) => task.id !== id)
    const deletedResult: DeleteResult = {
      count: task ? 1 : 0,
      task,
    }
    return Promise.resolve(deletedResult)
  }

  updateTask(id: string, dto: UpdateTaskDto): Promise<Task> {
    this.tasks = this.tasks.map((task) => {
      if (task.id !== id) {
        return task
      }
      return {
        ...task,
        completed: dto.completed,
      }
    })

    return this.findById(id)
  }

  createTask(dto: CreateTaskDto): Promise<Task> {
    const newTask: Task = {
      ...dto,
      id: v4(),
      completed: false,
    }
    this.tasks = [...this.tasks, newTask]
    return Promise.resolve(newTask)
  }
}

Create endpoints in Task Controller

task.controller.ts
import { UpdateTaskDto } from './dtos/update-task.dto'
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post, Put } from '@nestjs/common'
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger'
import { CreateTaskDto } from './dtos'
import { TaskService } from './services'
import { DeleteResult, Task } from './entities'

@ApiTags('Task')
@Controller('task')
export class TaskController {
  constructor(private service: TaskService) {}

  @Get()
  getAll(): Promise<Task[]> {
    return this.service.listAll()
  }

  @Get(':id')
  @ApiBadRequestResponse({ description: 'Task not found' })
  getTask(@Param('id', ParseUUIDPipe) id: string): Promise<Task> {
    return this.service.findById(id)
  }

  @Delete(':id')
  deleteTask(@Param('id', ParseUUIDPipe) id: string): Promise<DeleteResult> {
    return this.service.deleteById(id)
  }

  @Post()
  createTask(@Body() dto: CreateTaskDto): Promise<Task> {
    return this.service.createTask(dto)
  }

  @Put(':id')
  updateTask(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateTaskDto): Promise<Task> {
    return this.service.updateTask(id, dto)
  }
}

@ApiBadRequestResponse and @ApiBadRequestResponse are Swagger decorators.

DecoratorDescription
ApiTagsProvide name of the API and in this example, the name is Task
ApiBadRequestResponseIndicate the endpoint returns BadRequestException when task is not found

Refresh http://localhost:3000/api and Task API is published.

Task lists all available endpoints (two GETs, POST, DELETE and POST) and Schema section auto-generates its entities (Task and DeleteResult) and DTOs (CreateTaskDto and UpdateTaskDto).

That’s the end of Swagger introduction.

Final thoughts

Swagger provides clear documentation of Restful API that facilitates communication between frontend and backend developers. Rather than constantly asking backend team for sample requests and responses, frontend developers can refer to the Swagger documentation that illustrates all available endpoints and the expected payloads and responses. When teams have no confusion and argument, developers become happy and happy developers are the most productive in application development.

I hope you like this article and share it to people who are interested in NestJS!

Resources:

  1. Repo: https://github.com/railsstudent/nest-swagger
  2. OpenAPI (Swagger): https://docs.nestjs.com/openapi/introduction