Replace MomentJS with date-fns in Nestjs

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

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

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

  • 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

  • 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

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

  • 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

  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

Use Github Action to deploy Vue app to Netlify

  • 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
  • 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: Check out branch
        uses: actions/checkout@v2

      # ( Build to ./dist or other directory... )
      - name: Clean install dependencies
        run: npm ci

      - name: Build app
        run: npm run build-netlify

      - name: Deploy to Netlify
        uses: nwtgck/actions-netlify@v1.1
        with:
          publish-dir: './dist'
          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

I deploy the Tic Tac Toe game to both github page and Netlify; therefore, I use NODE_ENV variable to configure the public path in vue.config.js.

// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === "production" ? "/vue-tic-tac-toe/" : "/",
  outputDir: "dist",
  lintOnSave: process.env.NODE_ENV !== "production",
  devServer: {
    overlay: {
      warnings: true,
      errors: true
    }
  }
};

The NODE_ENV variable of github page is “production” by default and the full URL becomes https://<username>.github.io/vue-tic-tac-toe/. For Netlify deployment, I set the NODE_ENV variable to “netlify” and the game is hosted on https://<netlify app>/ .

Open package.json and add a task that builds the project to dist directory and publishes it to Netlify.

{
  ...
  "scripts": {
    ...
    "build-netlify": "NODE_ENV=netlify vue-cli-service build"
  },
  ...
}

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:

Use github action to deploy vue app to github page

  • 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
  • Go to Actions tab of the github repo
  • Create main.yml under .github/workflow
 name: Deploy to github pages
 on:
   push:
    branches:
     - master
 jobs:
    build:
      name: Deploying to gh-pages
      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: deploy
          uses: peaceiris/actions-gh-pages@v3
          with:
           github_token: ${{ secrets.ACCESS_TOKEN }}
           publish_dir: ./dist

There is no need to write custom deployment script and travis configuration file to automate the CI/CD process. Github does it for you when latest changes are pushed to master to start the build process and copy the contents of output folder to gh-pages branch.

References: