Dependency injection by abstract class in NestJS

 2 total views,  2 views today

When developers start using NestJS, they create new module with controllers and services to design backend APIs. Developers register controllers and services in the module and inject service into controller to perform CRUD operations.

Provider Registration

The following is a code snippet of a user module; user controller is registered in controller array while user service is registered in provider array .

@Module({
   controller: [UserController],
   provider: [UserService]
})
export class UserModule {}

[UserService] is concrete class registration and is the short form of

provider: [
  {
     provide: UserService
     useClass: UserService
  }
]

When UserService parameter is seen in the constructor of UserController, an instance of UserService injected.

Abstract Class Registration

At work, the business requirements of our application are to build different type of reports. Each type of report is encapsulated in a module and the controller defines an endpoint that exports report in PDF format and send the attachment to email users.

The naive implementation injects a service and executes functions in controller function to fulfill the purpose. The export function writes contents into a buffer stream and sendEmail attaches it to mail body and sends to some email addresses.

// User Report Module 

@Module({
   controller: [UserReportController],
   provider: [UserReportService]
})
export class UserReportModule {}
// User Report Controller

@Controller('user-report')
export class UserReportController {
   constructor(private service: UserReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       const buffer = await this.service.export()
       await this.service.sendEmail(buffer, 'user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send user report to emails:', emails)
  }
}

When there is one type of report, the codes pass our code review process. When the application enhances to support three types of reports; shareReport, export and sendEmail are copied to three modules and DRY principle is violated.

  • It is time to refactor the codes
  • It is time to refactor the codes
  • It is time to refactor the codes

Step 1: Create an abstract report service class in core directory

// Abstract report service

export abstract class AbstractReportService {
  abstract export(): Promise<Buffer>

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send report to emails:', emails)
  }

  async shareReport(...emails: string[]): Promise<void> {
    const buffer = await this.export()
    await this.sendEmail(buffer, ...emails)
  }
}

AbstractReportService class defines shareReport function that exports PDF file and sends the buffer stream to email addresses. sendEmail is reused by different types of reports but export must be implemented by subclasses.

Step 2: Dependency injection by abstract class

Rewrite UserReportModule to provide registration of AbstractReportService

// User Report Module

@Module({
   controller: [UserReportController],
   provider: [
     { 
        provide: AbstractReportService,
        useClass: UserReportService
     }
   ]
})
export class UserReportModule {}
// User Report Controller 

@Controller('user-report')
export class UserReportController {
   constructor(private service: AbstractReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       await this.service.shareReport('user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }
}

Step 3: Add new report type after refactoring

When new report type is introduced to the application, the new module registers AbstractReportService and implements the abstract function. Suppose I create PaymentReport module to export a payment report.

// Payment Report Module

@Module({
  controllers: [PaymentReportController],
  providers: [
    {
      provide: AbstractReportService,
      useClass: PaymentReportService,
    },
  ],
})
export class PaymentReportModule {}
// Payment Report Controller

@Controller('payment-report')
export class PaymentReportController {
  constructor(private service: AbstractReportService) {}

  @Post()
  async shareReport(): Promise<void> {
    await this.service.shareReport('john.doe@gmail.com', 'jane.doe@gmail.com')
  }
}
// Payment Report Service

@Injectable()
export class PaymentReportService extends AbstractReportService {
  async export(): Promise<Buffer> {
    return Buffer.from('Export in payment report service')
  }
}

Step 4: CURL POST endpoints

curl -X POST "http://localhost:3000/user-report"
// buffer:  Export in user report service
// send report to emails: [ 'black.doe@gmail.com', 'white.doe@gmail.com' ]

curl -X POST "http://localhost:3000/payment-report"
// buffer:  Export in payment report service
// send report to emails: [ 'john.doe@gmail.com', 'jane.doe@gmail.com' ]

Final thoughts

Dependency injection by abstract class returns an instance of concrete service registered in @Module decorator. This solution has many benefits:

  1. DRY principle because reusable functions are defined in abstract class and subclasses implement module-specific behavior
  2. Shorter controller class since shareReport function is reduced to one line
  3. Write less testing code because shareReport and sendEmail are tested once each. Before refactoring, sendEmail is tested three times.

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-interface-injection
  2. https://dev.to/ef/nestjs-dependency-injection-with-abstract-classes-4g65

Use TypeOrm Custom Repository in NestJS

 0 total views

NestJS has out of box integration with TypeORM, a TypeScript and JavaScript ORM that has good support of PostgreSQL. The library offers decorators to model data types and entities and tools to generate migration scripts to update database schema; therefore, it is a popular choice to perform CRUD operations.

TypeORM offers custom repository that allows developer to extend a repository and add custom methods. It is a powerful feature because developers can inject custom repository in a service and call custom methods as if they are built-in. Moreover, it is easier to mock custom methods than builder methods such as leftJoinAndSelect() and getMany() in unit test.

Our company uses nestJS at work and we recently adopted custom repository to encapsulate TypeORM queries in our custom repositories. I am going to illustrate how the team did it for the rest of the post.

Step 1: Create docker-compose.yml to pull PostgreSQL image to docker

docker-compose.yml

version: '3.7'

services:
  postgres:
    container_name: postgres_container2
    image: postgres:12.7-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      PGDATA: /data/postgres
    volumes:
      - postgres:/data/postgres
    ports:
      - '5433:5432'
    restart: always
    networks:
      - nest-poc

  pgadmin:
    container_name: pgadmin_container2
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org
      PGADMIN_DEFAULT_PASSWORD: pgadmin
    volumes:
      - pgadmin:/root/.pgadmin
    ports:
      - '5556:80'
    links:
      - 'postgres:pgsql-server'
    restart: always
    networks:
      - nest-poc

volumes:
  postgres:
  pgadmin:

networks:
  nest-poc:
    name: nest-poc

Step 2: Install TypeORM and PostgreSQL dependencies

npm install --save @nestjs/typeorm typeorm pg

ormconfig.json

{
    "type": "postgres",
    "host": "localhost",
    "port": 5433,
    "username": "postgres",
    "password": "postgres",
    "entities": ["dist/**/*.entity{.ts,.js}"],
    "migrations": ["dist/migrations/**/*.js"],
    "synchronize": false,
    "cli": {
      "migrationsDir": "src/migrations"
    }
}

Step 3: Create entities, custom repositories and repository module

Depends on your preference, our company creates a repository module that defines all the entities and custom repositories used in the project. Then, we export them from the module such that they are accessible to other modules.

user.entity.ts

import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
  VersionColumn,
} from 'typeorm'
import { Report } from './report.entity'

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ type: 'text' })
  name: string

  @Column({ type: 'text' })
  lastname: string

  @Column({ type: 'integer' })
  age: number

  @CreateDateColumn({ type: 'timestamp with time zone' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamp with time zone' })
  updatedAt: Date

  @VersionColumn({ type: 'integer', default: 1 })
  version: number

  @OneToMany(() => Report, (report) => report.owner)
  reports: Report[]
}

user.repository.ts

import { NewUser } from './type'
import { User } from '@/entities'
import { EntityRepository, Repository } from 'typeorm'

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async getUsers(): Promise<User[]> {
    return this.find({
      order: {
        name: 'ASC',
        lastname: 'ASC',
      },
    })
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.findOne(id)
  }

  async createUser(newUser: NewUser): Promise<User> {
    return this.save(this.create(newUser))
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
  }
}
repositories.module.ts

import { ReportRepository } from './report.repository'
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User, Report } from '@/entities'
import { UserRepository } from './user.repository'

@Module({
  imports: [TypeOrmModule.forFeature([User, Report, UserRepository, ReportRepository])],
  exports: [TypeOrmModule],
})
export class RepositoryModule {}

Step 4: Create new modules to use repository module

We create user and report modules and import RepositoryModule in UserModule and ReportModule respectively.

user.module.ts

import { RepositoryModule } from '@/repositories'
import { Module } from '@nestjs/common'
import { UserService } from './services'
import { UserController } from './user.controller'

@Module({
  imports: [RepositoryModule],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}

We create user controller and user service to perform user actions. Similarly, there are report controller and report service to handle report actions

user.service.ts

import { User } from '@/entities'
import { UserRepository } from '@/repositories'
import { Injectable } from '@nestjs/common'
import { CreateUserDto } from '../dtos'

@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async getUsers(): Promise<User[]> {
    return this.userRepository.getUsers()
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.userRepository.getUser(id)
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.userRepository.getUserWithReports(id)
  }

  async createUser(dto: CreateUserDto): Promise<User> {
    return this.userRepository.createUser(dto)
  }
}

user.controller.ts

import { UserService } from './services'
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { User } from '@/entities'
import { CreateUserDto } from './dtos'

@Controller('user')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  async getUsers(): Promise<User[]> {
    return this.userService.getUsers()
  }

  @Get(':id/report')
  async getUserWithReports(@Param('id') id: string): Promise<User | undefined> {
    return this.userService.getUserWithReports(id)
  }

  @Get(':id')
  async getUser(@Param('id') id: string): Promise<User | undefined> {
    return this.userService.getUser(id)
  }

  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<User> {
    return this.userService.createUser(dto)
  }
}

We use Postman/CURL to call endpoints to ensure the logic works and data is persisted in database

curl "http://localhost:3000/user"
curl "http://localhost:3000/user/1/report"

Step 5: Unit test service and controller

Earlier in the post, I claimed that mocking custom repository is easier than mocking builder methods of TypeORM. Let’s write some test cases to test user service and user controller to show it.

user.service.spec.ts

describe('UserService', () => {
  let service: UserService
  let spyUserRepository: UserRepository

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {},
        },
      ],
    }).compile()

    service = module.get<UserService>(UserService)
    spyUserRepository = module.get<UserRepository>(UserRepository)
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  describe('getUsers', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]
      spyUserRepository.getUsers = jest.fn(() => Promise.resolve(users))
      const result = await service.getUsers()
      expect(result).toEqual(users)
    })
  })

  describe('createUser', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return a new user', async () => {
      const id = v4()
      const newUserDto = {
        name: 'hello',
        lastname: 'world',
        age: 88,
      }

      const newUser: User = { ...newUserDto, id, createdAt: now, updatedAt: now, version: 1, reports: [] }

      spyUserRepository.createUser = jest.fn(() => Promise.resolve(newUser))
      const result = await service.createUser(newUserDto)
      expect(result).toEqual(newUser)
    })
  })
})
user.controller.spec.ts

describe('UserController', () => {
  let controller: UserController
  let userService: UserService

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {},
        },
      ],
    }).compile()

    controller = module.get<UserController>(UserController)
    userService = module.get<UserService>(UserService)
  })

  it('should be defined', () => {
    expect(controller).toBeDefined()
  })

  describe('getUsers', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]

      jest.spyOn(userService, 'getUsers').mockImplementation(() => Promise.resolve(users))
      const result = await controller.getUsers()
      expect(result).toEqual(users)
    })
  })

  describe('createUser', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return a new user', async () => {
      const id = v4()
      const newUserDto = {
        name: 'hello',
        lastname: 'world',
        age: 88,
      }

      const newUser: User = { ...newUserDto, id, createdAt: now, updatedAt: now, version: 1, reports: [] }

      jest.spyOn(userService, 'createUser').mockImplementation(() => Promise.resolve(newUser))
      const result = await controller.createUser(newUserDto)
      expect(result).toEqual(newUser)
    })
  })
})

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version

Replace MomentJS with date-fns in Nestjs

 0 total views

We use NestJS at work and the backend project needs to perform date calculations and formatting in the services. Our team chose momentJS because it is the one of the most popular date-time open source project. After a while, we discovered that the library size is huge (~290kb) and it is not tree-shakable. If I want to use format function to display a date in YYYY-MM-DD format, I must import the entire library into the code.

In Angular Architect Training Course, Bonnie Brennan said moment is so huge that that we should import moment locale instead. I chose a different route and decided to get rid of moment once and for all.

After doing some research, our team decided to replace momentJS with date-fns library, a lightweight date-time library, that has all the functions our project needs.

We made the migration in five steps:

  1. Install and extend eslint-plugin-you-dont-need-momentjs plugin, run eslint on the files to find all momentJS errors
  2. Replace momentJS functions with native JS if possible
  3. Create DateFnsService service in CoreModule that encapsulates the functionality of date-fns/fp submodule
  4. Import CoreModule into other modules of the project
  5. Inject DateFnsService in constructors and replace the remaining momentJS functions with functions of DateFnsService

Step 1: Install and extend eslint-plugin-you-dont-need-momentjs plugin

npm install --save-dev eslint-plugin-you-dont-need-momentjs
"extends" : ["plugin:you-dont-need-momentjs/recommended"],

Executed npm run lint command on terminal, the plugin outputted errors and warnings.

Step 2: Convert momentJS functions to native JS

Follow the examples in https://github.com/you-dont-need/You-Dont-Need-Momentjs#parse to convert momentJS functions to native JS

For example, moment() is replaced with new Date() and isAfter is replaced with

// Before: in moment
moment('2010-10-20').isAfter('2010-10-19') => true

// After: in native JS
new Date(2010, 9, 20) > new Date(2010, 9, 19) => true

Step 3: Create DateFnsService service in CoreModule

The requirement of the service is to provide functionality to format date, and add days, months and years to a given date. It is feasible by using functions defined in date-fns/fp submodule

First, we install date-fns dependency in the project

npm install date-fns --save

Create DateFnsService service in CoreModule

 import { format, addDays, addMonths, addYears } from 'date-fns/fp'

Injectable()
export class DateFnsService {

    format(date: Date | number, format: string): string {
        return format(format)(date)
    }

    addDays(date: Date | number, amount: number): Date {
        return addDays(amount)(date)
    }

    addMonths(date: Date | number, amount: number): Date {
        return addMonths(amount)(date)
    }

    addYears(date: Date | number, amount: number): Date {
        return addYears(amount)(date)
    }
}

Export DateFnsService from CoreModule such that it can be used outside of Core

@Module({
    providers: [DateFnsService],
    exports: [DateFnsService]    
})
export class CoreModule {}

Step 4: Import CoreModule to other modules

import { CoreModule } from '@/core'

@Module({
    imports: [CoreModule],
    providers: [AppController, AppService],
    exports: []    
})
export class AppModule {}

Step 5: Use DateFnsService in place of momentJS

Inject DateFnsService in AppService’s constructor and call its functions to manipulate date inside the function

import { DateFnsService } from '@/core'
// import * as moment from 'moment'

@Inject()
export class AppService {
   constructor (private datefnsService: DateFnsService) {}

   someFunction(): string {
      // Before:  
      // return = moment()
      //    .add(1, 'days')
      //    .add(1, 'months')
      //    .add(1, 'years')
      //    .format('YYYY-MM-DD')

      // After: 
      let mydate = this.datefnsService.addDays(new Date(), 1)
      mydate = this.datefnsService.addMonths(mydate, 1)
      mydate = this.datefnsService.addYears(mydate, 1)
      return this.datefnsService.format(mydate, 'yyyy-MM-dd');  <= '2022-06-19'
   }
}

Step 5 is repeated in all services until npm run lint does not produce any momentJS error. Afterward, we remove moment dependency from package.json and the project saves around 260kb.

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. https://github.com/you-dont-need/You-Dont-Need-Momentjs#days-in-month
  2. https://date-fns.org/v2.21.3/docs/fp/Getting-Started

You probably don’t need Lodash in Nestjs

 0 total views

I completed Angular Architect Training Course from Bonnie Brennan at Angular Nation and lesson 1 is about style and structure. Even though the tips are for Angular application but a couple of them applies to NestJS. One of them is to void fat libraries such as lodash and moment.

In this post, I am going to describe how I limited the import size and usage of lodash library in our NestJS project at work.

Lodash contains a lot of useful functions but it is a rather large library with the size of 70kb. When developers import an lodash function using ES6 import, they are in fact import the entire library into the file that is unexpected.

After reading few blog posts, I found out that both statements are equivalent and import the whole library to file

  • import _ from ‘lodash’
  • import { sort } from ‘lodash’

Even though NestJS app resides in server side and bundle size is not a criterial factor, I wish to import lodash functions that the project is using only and replace other lodash functions with native JS.

you-dont-use-lodash-underscore plugin

NestJS app uses eslint to lint files; therefore, I installed eslint plugin, eslint-plugin-you-dont-need-lodash-underscore, and extended it in eslintrc.js

npm install --save-dev eslint-plugin-you-dont-need-lodash-underscore
"extends" : ["plugin:you-dont-need-lodash-underscore/compatible"]

Execute npm run lint command on terminal and the plugin outputted lodash errors. For example, uniq, flatten and omit can be replaced with native JS.

I overrided @typescript-eslint/no-unused-vars rule such that eslint does not complain unused variable(s) when rest spread operator is used

"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
// Replace omit with rest spread operator
const something = omit(object, ['p1'])

const { p1, ...something } = object
// Replace uniq with Array destructuring and Set 
const u = uniq([1,2,3,1,1,1])

const u = [...new Set<number>([1,2,3,1,1,1])]
// Replace flatten with Array.flat if ES2019 is used
const f = flatten([1,[2,3], 4)

const f = [1,[2,3], 4].flat()

When all lodash errors were resolved, re-run npm run lint and it produced zero errors

Correct way to import lodash

I tried

import pick from 'lodash/pick' 

but the compiler complained. When I used

 import pick = require('lodash/pick')

it worked perfectly.

These are the steps I used to replace lodash functions with light-weight alternatives and import the rest one by one.

Resources:

  1. https://www.blazemeter.com/blog/the-correct-way-to-import-lodash-libraries-a-benchmark
  2. https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore

My Spanish app in PANNG stack

 2 total views

Intention: I am learning Spanish from a Spanish teacher and I cannot read my own notes due to my horrible handwriting. My resolution is to build a full-stack app to store new Spanish vocabulary.

Stack: PANNG (Prisma, Angular, NestJS, Nx, GraphQL)

Repo: https://github.com/railsstudent/nx-apollo-angular-course

Characteristics:

  • Nx Monorepo
  • GraphQL module in Nest
  • Apollo GraphQL
  • Angular Apollo
  • TypeScript GraphQL code generation to generate GraphQL services
  • Tailwind CSS

Features:

  • Cursor-based pagination in course, lesson and sentence lists respectively
  • Add and retrieve course
  • Add and retrieve lesson
  • Add, delete and retrieve sentence
  • Add, delete and retrieve translation
  • Speak Spanish texts

Local graphQL playground

http://localhost:3333/graphql

Use Github Action to deploy React app to Surge

 0 total views

  • Generate a personal access token and create ACCESS_TOKEN variable under Settings -> Secrets.
  • Keep the personal access token in a safe place and do not lose it

npm script commands

  • Add script commands “build” and “clean” to build application and generate artifacts in dist/ directory
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html",
    "clean": "rm -rf ./dist/"
  }
  • Run “npm run build” to build static application before deployment to surge.sh

Surge setup

  • npm install surge globally
npm install -g surge
  • Login or create surge.sh account in command line
surge
email: <email address>
password: <password>

project:  <project directory>/dist
domain: <custom domain>.surge.sh
  • After the site is published, verify http://<custom domain>.surge.sh can be browsed

Surge Token

  • Generate surge token in command line
surge token
XXXXXXXXX        <-- a surge token is issued by Surge
  • In Github repo, create SURGE_TOKEN variable under Settings -> Secrets.
  • Keep surge token in safe location such that it can be reused to deploy other applications to Surge.

Create Github Action workflow file

  • Go to Actions tab of the github repo
  • Create surge.yml under .github/workflow

Paste the follow code in the yaml file

name: Deploy to surge.sh
on:
  push:
   branches:
    - master
jobs:
   build:
     name: Deploying to surge
     runs-on: ubuntu-latest
     steps:
       - name: Setup Node.js for use with actions
         uses: actions/setup-node@v1.1.0
         with:
           version:  12.x
      
       - name: Checkout branch
         uses: actions/checkout@v2
 
       - name: Clean install dependencies
         run: npm ci
 
       - name: Build app
         run: npm run build

       - name: Rename index.html to 200.html
         run: mv ./dist/index.html ./dist/200.html

       - name: Install Surge
         run: npm install -g surge
        
       - name: Deploy to Surge
         run:  surge ./dist https://<custom domain>.surge.sh --token ${{secrets.SURGE_TOKEN}}
  • Replace <custom domain> with actual surge domain name

Add 200.html page for Client-side routing

  • When page is refreshed in Surge, the url does not reach our Reach Router and default Surge 404 page is returned.
  • The solution is to rename dist/index.html to dist/200.html before deployment to Surge is carried out.
  • This is done by mv ./dist/index.html ./dist/200.html in surge.yaml
  • The reason is to load the app when 200 response is resulted. Then the app loads the appropriate component by matching the path of reach router to the url

References:

Use Github Action to deploy React app to Netlify

 1 total views

  • Generate a personal access token and create ACCESS_TOKEN variable under Settings -> Secrets.
  • Keep the personal access token in a safe place and do not lose it

Fix 404 not found when page is refreshed

My React app uses Reach Router library to route user from root / to /countries/:language. When I refreshes page with F5, reach router does not handle redirection and 404 page is returned.

In order to solve the problem in Netlify, I define a redirect rule in netlify.toml to redirect all routes to /index.html with HTTP status code 200.

  • Create netlify.toml in project directory
# Redirects and headers are GLOBAL for all builds – they do not get scoped to
# contexts no matter where you define them in the file.
# For context-specific rules, use _headers or _redirects files, which are
# PER-DEPLOY.

# A basic redirect rule
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Update package.json to create npm script commands that Github Action workflow file depends

  • “npm run clean” deletes all files in dist/ directory
  • “npm run build” builds project and generates artifacts in dist/ directory
  "scripts": {
    "build": "parcel build src/index.html",
    "clean": "rm -rf ./dist/"
  }
  • Go to Actions tab of the github repo
  • Create netlify.yml under .github/workflow

Paste the follow code in the yaml file

# .github/workflows/netlify.yml
name: Build and Deploy to Netlify
on:
  push:
  pull_request:
    types: [opened, synchronize]
jobs:
  build:
    name: Deploying to netlify
    runs-on: ubuntu-latest
    steps:
      - name: Setup Node.js for use with actions
        uses: actions/setup-node@v1.1.0
        with:
          version:  12.x
      
      - name: Checkout branch
        uses: actions/checkout@v2
 
      # ( Build to ./dist or other directory... )
      - name: Clean install dependencies
        run: npm ci

      - name: Remove dist
        run: npm run clean

      - name: Build app
        run: npm run build
 
      - name: Deploy to Netlify
        uses: nwtgck/actions-netlify@v1.1.10
        with:
          publish-dir: './dist'
          netlify-config-path: './netlify.toml'
          production-branch: master
          github-token: ${{ secrets.ACCESS_SECRET }}
          deploy-message: "Deploy from GitHub Actions"
          enable-pull-request-comment: false
          enable-commit-comment: true
          overwrites-pull-request-comment: true
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
        timeout-minutes: 1

netlify-config-path indicates the configuration path to Netlify platform.

Netlify environment variables

Login to Netlify to look up NETIFY_SITE_ID and NETIFY_AUTH_TOKEN.

Go to Team > Site > Settings > Site Information > API ID to copy down the NETLIFY_SITE_ID

Go to User Settings > Applications > Personal access token and clicks New Access Token button to generate a token. The token is your NETLIFY_AUTH_TOKEN and it should be kept in a safe location.

Now, you are ready to create NETLIFY_SITE_ID and NETLIFY_AUTH_TOKEN variables under Settings -> Secrets in Github.

We are done! There is no need to write custom deployment script and travis configuration file to automate the CI/CD process. Github notifies Netlify to automate the build process and publish the site when latest changes are committed.

References:

Add apollo-client to a React project

 1 total views

Set up Apollo Client for React

  • npm install apollo/client and graphql
npm install @apollo/client graphql
  • Create client.tsx file to store configuration of apollo-client react
  • import ApolloClient, InMemoryCache and HttpLink from ‘@apollo/client’
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
  • Initialize and export an instance of ApolloClient
const uri = 'https://countries-274616.ew.r.appspot.com';
const link = new HttpLink({ uri });

const client = new ApolloClient({
    cache: new InMemoryCache(),
    link
});

export default client;
  • Test client instance in client.tsx by executing a GraphQL query
  • Create a new folder named graphql and add get-languages.tsx in it
  • graphql/get-languages.tsx defines a GraphQL query that returns countries where official language is English.
graphql/get-languages.tsx

import { gql } from '@apollo/client';

export const GET_LANGUAGES = gql`
    query getLanguages {
        languages: Language(
            filter: { name_in: ["English"] },   
            orderBy: [name_asc]) {
                id: _id
                name
                nativeName
                countries {
                    id: _id
                    name
                    flag {
                        emoji
                    }
                }
        }
    }
`;
  • Verify Apollo client can retrieve the results of GET_LANGUAGES and output to console
In client.tsx

import { GET_LANGUAGES } from './graphql/queries';

client.query({
  query: GET_LANGUAGES
}).then(console.log);

Connect Apollo Client to React

  • Import ApolloProvider component and client instance to App.tsx. The client instance is initialized and exported from client.tsx
  • Wrap ApolloProvider around top level element in App function
import { ApolloProvider } from "@apollo/client";
import client from './client';

const App = () => {
  return (
    <ApolloProvider client={client}>
      <React.StrictMode>
        <p>Hello World!</p>
      </React.StrictMode>
    </ApolloProvider>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
  • Up to this point, the application can take advantage of Apollo Client to execute queries and mutations on client side

Set up a React and TypeScript project from scratch

 0 total views

  • Create a folder ts-graphql-country and change to directory
mkdir ts-graphql-country
cd ts-graphql-country
  • Generate package.json
  • Add “browserslist”: [ “last 2 Chrome versions” ] in package.json
npm init
"browserslist": [
   "last 2 Chrome versions"
]
  • Install react, react-dom and @reach/router
  • Install parcel bundler and prettier
npm install react react-dom @reach/router
npm install -D parcel-bundler prettier
  • Add dev script in package.json to launch web site at http://localhost:1234
  • Create a blank index.html in src folder
"scripts" {
  "dev": "parcel src/index.html"
}
  • Create .prettierrc file to store prettier configurations
{
    "trailingComma": "all",
    "tabWidth": 2,
    "semi": true,
    "singleQuote": true
}
  • Install @babel/core, @babel/preset-env and @babel/react
npm install -D @babel/core @babel/preset-env @babel/preset-react
  • Create .babelrc file and include babel presets as follows:
{
    "presets": ["@babel/preset-react", "@babel/preset-env"]
}
  • Install typescript, @types/react, @types/react-dom and @types/reacth__router
  • Install tslint, tslint-react and tslint-config-prettier
  • Run npx tsc –init to generate tsconfig.json
npm install -D typescript
npm install -D @types/react @types/react-dom @types/reach__router
npm install -D tslint tslint-react tslint-config-prettier
npx tsc --init
  • Open tsconfig.json. Update target to “ES2018”, uncomment “jsx” field and replace the value to “react”
  • Generate tslint.json and add the following
{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "rules": {
    "ordered-imports": false,
    "object-literal-sort-keys": false,
    "member-ordering": false,
    "no-console": false,
    "jsx-no-lambda": false
  }
}
  • Add “lint”: “tslint –project .” script in package.json
"scripts": {
...
    "lint": "tslint --project ."
}
  • Install node-sass
npm install node-sass
  • Create blank scss file, style.scss, in src folder
  • Create an App component in App.tsx in src folder and write simple JavaScript code to render a h1 tag
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
    return (
        <h1>Hello</h1>  
    );
}

ReactDOM.render(<App />, document.getElementById('root'));
  • Import style.scss and App.tsx to index.html to render App component
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="./style.scss" />
    <title>TS GraphQL Country List</title>
  </head>

  <body>
    <div id="root">not rendered</div>
    <script src="./App.tsx"></script>
  </body>
</html>
  • In terminal, type npm run dev to start web application.
  • In browser, the following is displayed

Use CSS to draw diagonal line across of square

 2 total views

  1. Create two divs and convert them into flexboxes.
.square {
   position: relative;
   width: 130px;
   height: 130px;
   border: 1px solid black;
   background: red;

   display: flex;
   justify-content: center;
   align-items: center;
}

<div class="square left-diag"></div>
<div class="square right-diag"></div>

2. Add .left-diag:after pseudo element to draw a black dialog line that draws from bottom left to upper right. Sass style is shown below:

.square {
   &.left-diag:after {
     content: "";
     position: absolute;
     z-index: 1;
     border: 1px solid black;
     height: 140%;
     transform: rotate(-45deg);
   }
}

3. Add .right-diag:after pseudo element to draw a black dialog line that draws from bottom right to upper left. Sass style is shown below:

.square {
   &.right-diag:after {
     content: "";
     position: absolute;
     z-index: 1;
     border: 1px solid black;
     height: 140%;
     transform: rotate(45deg);
   }
}

4. Lets refactor the sass styles and group common css properties in a mixin function

@mixin strikethroughDiagonal($rotation) {
  content: "";
  position: absolute;
  z-index: 1;
  border: 1px solid black;
  height: 140%;
  transform: rotate($rotation);
}

.square {
   &.left-diag:after {
     @include strikethroughDiagonal(-45deg); 
   }

   &.right-diag:after {
     @include strikethroughDiagonal(45deg); 
   }
}

Stackblitz repo:

https://stackblitz.com/edit/js-4s7qhi