Logo

dev-resources.site

for different kinds of informations.

Converting date by user time zone in "NestJS", and entering and displaying date in "Angular"

Published at
12/29/2024
Categories
angular
timezone
nestjs
fullstack
Author
endykaufman
Categories
4 categories in total
angular
open
timezone
open
nestjs
open
fullstack
open
Author
11 person written this
endykaufman
open
Converting date by user time zone in "NestJS", and entering and displaying date in "Angular"

In this article, I will talk about adding a new field workUntilDate with the type timestamp(6) to the Webhook table of the Webhook database.

On the frontend side (in the Angular application), a convenient calendar with the ability to select a time will be implemented for this field.

Users will be able to set the date and time in their time zone, while the backend (NestJS application) will save the entered data in the database in the UTC+0 format.

In addition, the calendar interface and other elements displaying dates will be adapted to the user's language and time zone.

1. Installing the required libraries

First, let's install the required packages:

Commands

npm install --save @jsverse/transloco-locale @jsverse/transloco-messageformat --prefer-offline --no-audit --progress=false
Enter fullscreen mode Exit fullscreen mode

2. Creating a migration

My migrations are written in a way that they can be re-run.

This is useful in cases where you need to undo the migration and re-run it.

Commands

npm run flyway:create:webhook --args=AddFieldWorkUntilDateToAuthUser
Enter fullscreen mode Exit fullscreen mode

Updating the file libs/feature/webhook/src/migrations/V202412200905__AddFieldWorkUntilDateToAuthUser.sql

DO $$
BEGIN
    ALTER TABLE "Webhook"
        ADD "workUntilDate" timestamp(6);
EXCEPTION
    WHEN duplicate_column THEN
        NULL;
END
$$;


Enter fullscreen mode Exit fullscreen mode

3. Applying migration and updating "Prisma" schemas

Now let's apply the created migration, recreate the Prisma schemas and run the Prisma generators.

Commands

npm run docker-compose:start-prod:server
npm run db:create-and-fill
npm run prisma:pull
npm run generate
Enter fullscreen mode Exit fullscreen mode

After completing these steps, all relevant DTOs will have a new field workUntilDate.

Example of updating DTO file libs/feature/webhook/src/lib/generated/rest/dto/webhook.dto.ts

import { Prisma } from '../../../../../../../../node_modules/@prisma/webhook-client';
import { ApiProperty } from '@nestjs/swagger';

export class WebhookDto {
  // ...
  // updates
  @ApiProperty({
    type: 'string',
    format: 'date-time',
    nullable: true,
  })
  workUntilDate!: Date | null;
}
Enter fullscreen mode Exit fullscreen mode

Example of updating a Prisma schema libs/feature/webhook/src/prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
  engineType = "binary"
  output   = "../../../../../node_modules/@prisma/webhook-client"
  binaryTargets = ["native","linux-musl","debian-openssl-1.1.x","linux-musl-openssl-3.0.x"]
}

// ...

model Webhook {
  id                                         String       @id(map: "PK_WEBHOOK") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  // ...
  workUntilDate                              DateTime?    @db.Timestamp(6) /// <-- updates
}
Enter fullscreen mode Exit fullscreen mode

4. Using "AsyncLocalStorage" to store the user's current timezone

Previously, we used AuthTimezoneInterceptor to convert the output of dates in UTC-0 format to a format that respects the user's timezone.

The conversion of the incoming date from the user's timezone to the UTC-0 date in which it is stored in the database is done in AuthTimezonePipe.

However, in this context, we do not have access to the request data, so it is not possible to determine the user and their timezone.

To solve this problem, we wrap each incoming request in AsyncLocalStorage, which will allow us to obtain the user's timezone information.

Updating the file libs/core/auth/src/lib/interceptors/auth-timezone.interceptor.ts

// ...
import { AsyncLocalStorage } from 'node:async_hooks';
import { AuthAsyncLocalStorageData } from '../types/auth-async-local-storage-data';

@Injectable()
export class AuthTimezoneInterceptor implements NestInterceptor<TData, TData> {
  constructor(
    // ...
    private readonly asyncLocalStorage: AsyncLocalStorage<AuthAsyncLocalStorageData>
  ) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    const req: AuthRequest = getRequestFromExecutionContext(context);
    const userId = req.authUser?.externalUserId;

    if (!this.authEnvironments.useInterceptors) {
      return next.handle();
    }

    if (!userId) {
      return next.handle();
    }

    const run = () => {
      const result = next.handle();

      if (isObservable(result)) {
        return result.pipe(
          concatMap(async (data) => {
            const user = await this.authCacheService.getCachedUserByExternalUserId(userId);
            return this.authTimezoneService.convertObject(data, user?.timezone);
          })
        );
      }
      if (result instanceof Promise && typeof result?.then === 'function') {
        return result.then(async (data) => {
          if (isObservable(data)) {
            return data.pipe(
              concatMap(async (data) => {
                const user = await this.authCacheService.getCachedUserByExternalUserId(userId);
                return this.authTimezoneService.convertObject(data, user?.timezone);
              })
            );
          } else {
            const user = await this.authCacheService.getCachedUserByExternalUserId(userId);
            // need for correct map types with base method of NestInterceptor
            return this.authTimezoneService.convertObject(data, user?.timezone) as Observable<TData>;
          }
        });
      }
      // need for correct map types with base method of NestInterceptor
      return this.authTimezoneService.convertObject(result, req.authUser?.timezone) as Observable<TData>;
    };

    if (!this.authEnvironments.usePipes) {
      return run();
    }

    return this.asyncLocalStorage.run({ authTimezone: req.authUser?.timezone || 0 }, () => run());
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Creating a "Pipe" to transform the input object

We implement a Pipe that will subtract the user's time zone from all fields of the input object that contain date strings.

If the time zone of the backend server itself is different from UTC-0, then we subtract the difference.

Updating the file libs/core/auth/src/lib/pipes/auth-timezone.pipe.ts

import { SERVER_TIMEZONE_OFFSET } from '@nestjs-mod-fullstack/common';
import { Injectable, PipeTransform } from '@nestjs/common';
import { AsyncLocalStorage } from 'node:async_hooks';
import { AuthEnvironments } from '../auth.environments';
import { AuthTimezoneService } from '../services/auth-timezone.service';
import { AuthAsyncLocalStorageData } from '../types/auth-async-local-storage-data';

@Injectable()
export class AuthTimezonePipe implements PipeTransform {
  constructor(private readonly asyncLocalStorage: AsyncLocalStorage<AuthAsyncLocalStorageData>, private readonly authTimezoneService: AuthTimezoneService, private readonly authEnvironments: AuthEnvironments) {}

  transform(value: unknown) {
    if (!this.authEnvironments.usePipes) {
      return value;
    }
    const result = this.authTimezoneService.convertObject(value, -1 * (this.asyncLocalStorage.getStore()?.authTimezone || 0) - SERVER_TIMEZONE_OFFSET);
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Registering an interceptor and a service for storing asynchronous state in the authorization module

Now let's add the created interceptor and a service for storing asynchronous state to the authorization module.

Updating the file libs/core/auth/src/lib/auth.module.ts

// ...
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
// ...
import { AsyncLocalStorage } from 'node:async_hooks';
import { AuthTimezonePipe } from './pipes/auth-timezone.pipe';

export const { AuthModule } = createNestModule({
  // ...
  sharedProviders: [
    {
      provide: AsyncLocalStorage,
      useValue: new AsyncLocalStorage(),
    },
    AuthTimezoneService,
    AuthCacheService,
  ],
  providers: [
    // ...
    { provide: APP_PIPE, useClass: AuthTimezonePipe },
    AuthAuthorizerService,
    AuthAuthorizerBootstrapService,
  ],
  // ...
});
Enter fullscreen mode Exit fullscreen mode

7. Adding a new field type "date-input" for "Formly"

Although the standard HTML input field supports entering and displaying data with the Date type, its appearance differs from the components provided by ng.ant.design.

To keep the interface consistent, we will create a new date-input control for Formly.

Create a file libs/common-angular/src/lib/formly/date-input.component.ts

import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { TranslocoService } from '@jsverse/transloco';
import { FieldType, FieldTypeConfig, FormlyModule } from '@ngx-formly/core';
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
import { map, Observable } from 'rxjs';
import { DATE_INPUT_FORMATS } from '../constants/date-input-formats';
import { ActiveLangService } from '../services/active-lang.service';

@Component({
  selector: 'date-input',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ReactiveFormsModule, FormlyModule, NzDatePickerModule, AsyncPipe],
  template: ` <nz-date-picker [formControl]="formControl" [formlyAttributes]="field" [nzShowTime]="true" [nzFormat]="(format$ | async)!"></nz-date-picker> `,
})
export class DateInputComponent extends FieldType<FieldTypeConfig> {
  format$: Observable<string>;

  constructor(private readonly translocoService: TranslocoService, private readonly activeLangService: ActiveLangService) {
    super();
    this.format$ = translocoService.langChanges$.pipe(
      map((lang) => {
        const { locale } = this.activeLangService.normalizeLangKey(lang);
        return DATE_INPUT_FORMATS[locale] ? DATE_INPUT_FORMATS[locale] : DATE_INPUT_FORMATS['en-US'];
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The calendar now correctly displays buttons in the selected locale, but the content of the input field itself remains unchanged.

To solve this problem, let's create a list of main locales and output formats and set the format to be set as the date output in input.

Create a file libs/common-angular/src/lib/constants/date-input-formats.ts

export const DATE_INPUT_FORMATS = {
  'en-US': 'MM/dd/yyyy HH:mm:ss',
  'en-GB': 'dd/MM/yyyy HH:mm:ss',
  'ar-SA': 'dd/MM/yyyy Ω‡Ω‡:sΨ³',
  'bg-BG': 'd.M.yyyy H:m:s Ρ‡.',
  'ca-ES': 'dd/MM/yyyy H:mm:ss',
  'cs-CZ': 'd.M.yyyy H:mm:ss',
  'da-DK': 'dd-MM-yyyy HH:mm:ss',
  'de-DE': 'dd.MM.yyyy HH:mm:ss',
  'el-GR': 'd/M/yyyy h:mm:ss πμ|ΞΌΞΌ',
  'es-MX': 'dd/MM/yyyy H:mm:ss',
  'fi-FI': 'd.M.yyyy klo H.mm.ss',
  'fr-FR': 'dd/MM/yyyy HH:mm:ss',
  'he-IL': 'dd/MM/yyyy HH:mm:ss',
  'hi-IN': 'dd-MM-yyyy hh:mm:ss ΰ€¬ΰ€œΰ₯‡',
  'hr-HR': 'd.M.yyyy. H:mm:ss',
  'hu-HU': 'yyyy.MM.dd. H:mm:ss',
  'id-ID': 'dd/MM/yyyy HH:mm:ss',
  'is-IS': 'd.M.yyyy kl. HH:mm:ss',
  'it-IT': 'dd/MM/yyyy HH:mm:ss',
  'ja-JP': 'yyyy/MM/dd HH:mm:ss',
  'ko-KR': 'yyyyλ…„ MMμ›” dd일 HHμ‹œ mmλΆ„ ss초',
  'lt-LT': 'yyyy.MM.dd. HH:mm:ss',
  'lv-LV': 'yyyy.gada MM.mΔ“nesis dd.diena HH:mm:ss',
  'ms-MY': 'dd/MM/yyyy HH:mm:ss',
  'nl-NL': 'dd-MM-yyyy HH:mm:ss',
  'no-NO': 'dd.MM.yyyy HH:mm:ss',
  'pl-PL': 'dd.MM.yyyy HH:mm:ss',
  'pt-BR': 'dd/MM/yyyy HH:mm:ss',
  'ro-RO': 'dd.MM.yyyy HH:mm:ss',
  'ru-RU': 'dd.MM.yyyy HH:mm:ss',
  'sk-SK': 'd. M. yyyy H:mm:ss',
  'sl-SI': 'd.M.yyyy H:mm:ss',
  'sr-RS': 'dd.MM.yyyy. HH:mm:ss',
  'sv-SE': 'yyyy-MM-dd HH:mm:ss',
  'th-TH': 'ΰΈ§ΰΈ±ΰΈ™ΰΈ—ΰΈ΅ΰΉˆ d ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™ M ΰΈ›ΰΈ΅ yyyy ΰΉ€ΰΈ§ΰΈ₯ΰΈ² H:mm:ss',
  'tr-TR': 'dd.MM.yyyy HH:mm:ss',
  'uk-UA': 'dd.MM.yyyy HH:mm:ss',
  'vi-VN': 'dd/MM/yyyy HH:mm:ss',
  'zh-CN': 'yyyyεΉ΄MM月ddζ—₯ HHζ—Άmmεˆ†ssη§’',
  'zh-TW': 'yyyyεΉ΄MM月ddζ—₯ HHζ™‚mmεˆ†ssη§’',
};
Enter fullscreen mode Exit fullscreen mode

Let's define new types in a variable, which we will later include in the application configuration.

Create a file libs/common-angular/src/lib/formly/formly-fields.ts

import { TypeOption } from '@ngx-formly/core/lib/models';
import { DateInputComponent } from './date-input.component';

export const COMMON_FORMLY_FIELDS: TypeOption[] = [
  {
    name: 'date-input',
    component: DateInputComponent,
    extends: 'input',
  },
];
Enter fullscreen mode Exit fullscreen mode

8. Developing a service for changing locale in different components of a frontend application

Since different components use their own unique mechanisms for changing the language, we will combine them into a single service and method.

Create a file libs/common-angular/src/lib/services/active-lang.service.ts

import { Inject, Injectable } from '@angular/core';
import { toCamelCase, TranslocoService } from '@jsverse/transloco';
import { LangToLocaleMapping, TRANSLOCO_LOCALE_LANG_MAPPING, TranslocoLocaleService } from '@jsverse/transloco-locale';
import * as dateFnsLocales from 'date-fns/locale';
import * as ngZorroLocales from 'ng-zorro-antd/i18n';
import { NzI18nService } from 'ng-zorro-antd/i18n';

@Injectable({ providedIn: 'root' })
export class ActiveLangService {
  constructor(
    private readonly translocoService: TranslocoService,
    private readonly translocoLocaleService: TranslocoLocaleService,
    private readonly nzI18nService: NzI18nService,
    @Inject(TRANSLOCO_LOCALE_LANG_MAPPING)
    readonly langToLocaleMapping: LangToLocaleMapping
  ) {}

  applyActiveLang(lang: string) {
    const { locale, localeInSnakeCase, localeInCamelCase } = this.normalizeLangKey(lang);

    this.translocoService.setActiveLang(lang);
    this.translocoLocaleService.setLocale(locale);

    if (ngZorroLocales[localeInSnakeCase]) {
      this.nzI18nService.setLocale(ngZorroLocales[localeInSnakeCase]);
    }

    if (dateFnsLocales[lang]) {
      this.nzI18nService.setDateLocale(dateFnsLocales[lang]);
    }
    if (dateFnsLocales[localeInCamelCase]) {
      this.nzI18nService.setDateLocale(dateFnsLocales[localeInCamelCase]);
    }
  }

  normalizeLangKey(lang: string) {
    const locale = this.langToLocaleMapping[lang];
    const localeInCamelCase = toCamelCase(locale);
    const localeInSnakeCase = locale.split('-').join('_');
    return { locale, localeInSnakeCase, localeInCamelCase };
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Connecting the necessary elements to the application configuration for switching the locale in components working with dates

Now we will connect everything necessary to the configuration of our application to ensure correct switching of the locale in components for working with dates.

Updating the file apps/client/src/app/app.config.ts

import { provideTranslocoMessageformat } from '@jsverse/transloco-messageformat';

// ...

import { COMMON_FORMLY_FIELDS } from '@nestjs-mod-fullstack/common-angular';
import { FILES_FORMLY_FIELDS } from '@nestjs-mod-fullstack/files-angular';

// ...

export const appConfig = ({ authorizerURL, minioURL }: { authorizerURL: string; minioURL: string }): ApplicationConfig => {
  return {
    providers: [
      // ...
      importProvidersFrom(
        // ...
        FormlyModule.forRoot({
          // <--updates
          types: [...FILES_FORMLY_FIELDS, ...COMMON_FORMLY_FIELDS],
        })
      ),
      // ...
      provideTranslocoLocale({
        // <--updates
        defaultLocale: 'en-US',
        langToLocaleMapping: {
          en: 'en-US',
          ru: 'ru-RU',
        },
      }),
      provideTranslocoMessageformat({
        // <--updates
        locales: ['en-US', 'ru-RU'],
      }),
      // ...
    ],
  };
};
Enter fullscreen mode Exit fullscreen mode

10. Adding a new input field on the front end in the "Webhook" module

The new form field can function as a standard type=input element with the props.type=datetime-local type, or as a custom type=date-input field.

Updating the file libs/feature/webhook-angular/src/lib/services/webhook-form.service.ts

import { Injectable } from '@angular/core';
import { TranslocoService } from '@jsverse/transloco';
import { UpdateWebhookDtoInterface, ValidationErrorMetadataInterface, WebhookEventInterface, WebhookScalarFieldEnumInterface } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { ValidationService } from '@nestjs-mod-fullstack/common-angular';
import { UntilDestroy } from '@ngneat/until-destroy';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { tap } from 'rxjs';
import { WebhookEventsService } from './webhook-events.service';

@UntilDestroy()
@Injectable({ providedIn: 'root' })
export class WebhookFormService {
  protected events: WebhookEventInterface[] = [];

  constructor(protected readonly webhookEventsService: WebhookEventsService, protected readonly translocoService: TranslocoService, protected readonly validationService: ValidationService) {}

  init() {
    return this.webhookEventsService.findMany().pipe(
      tap((events) => {
        this.events = events;
      })
    );
  }

  getFormlyFields(options?: {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    data?: UpdateWebhookDtoInterface;
    errors?: ValidationErrorMetadataInterface[];
  }): FormlyFieldConfig[] {
    return this.validationService.appendServerErrorsAsValidatorsToFields(
      [
        {
          key: WebhookScalarFieldEnumInterface.enabled,
          type: 'checkbox',
          validation: {
            show: true,
          },
          props: {
            label: this.translocoService.translate(`webhook.form.fields.enabled`),
            placeholder: 'enabled',
            required: true,
          },
        },
        {
          key: WebhookScalarFieldEnumInterface.endpoint,
          type: 'input',
          validation: {
            show: true,
          },
          props: {
            label: this.translocoService.translate(`webhook.form.fields.endpoint`),
            placeholder: 'endpoint',
            required: true,
          },
        },
        {
          key: WebhookScalarFieldEnumInterface.eventName,
          type: 'select',
          validation: {
            show: true,
          },
          props: {
            label: this.translocoService.translate(`webhook.form.fields.event-name`),
            placeholder: 'eventName',
            required: true,
            options: (this.events || []).map((e) => ({
              value: e.eventName,
              label: `${e.eventName} - ${e.description}`,
            })),
          },
        },
        {
          key: WebhookScalarFieldEnumInterface.headers,
          type: 'textarea',
          validation: {
            show: true,
          },
          props: {
            label: this.translocoService.translate(`webhook.form.fields.headers`),
            placeholder: 'headers',
          },
        },
        {
          key: WebhookScalarFieldEnumInterface.requestTimeout,
          type: 'input',
          validation: {
            show: true,
          },
          props: {
            type: 'number',
            label: this.translocoService.translate(`webhook.form.fields.request-timeout`),
            placeholder: 'requestTimeout',
            required: false,
          },
        },
        {
          key: WebhookScalarFieldEnumInterface.workUntilDate, // <-- updates
          type: 'date-input',
          validation: {
            show: true,
          },
          props: {
            type: 'datetime-local',
            label: this.translocoService.translate(`webhook.form.fields.work-until-date`),
            placeholder: 'workUntilDate',
            required: false,
          },
        },
      ],
      options?.errors || []
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

To convert incoming and outgoing data on the client side, you will need to create mappers, which we will describe in a specialized service.

Considering the possible offset of the user's browser time zone, when converting a string with a date received from the server into a browser date object, it is necessary to take into account the offset of the browser time zone.

Create a file libs/feature/webhook-angular/src/lib/services/webhook-mapper.service.ts

import { Injectable } from '@angular/core';
import { WebhookInterface } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { BROWSER_TIMEZONE_OFFSET, safeParseJson } from '@nestjs-mod-fullstack/common-angular';
import { addHours, format } from 'date-fns';

export interface WebhookModel extends Partial<Omit<WebhookInterface, 'workUntilDate' | 'createdAt' | 'updatedAt' | 'headers'>> {
  headers?: string | null;
  createdAt?: Date | null;
  updatedAt?: Date | null;
  workUntilDate?: Date | null;
}

@Injectable({ providedIn: 'root' })
export class WebhookMapperService {
  toModel(item?: WebhookInterface): WebhookModel {
    return {
      ...item,
      headers: item?.headers ? JSON.stringify(item.headers) : '',
      requestTimeout: item?.requestTimeout ? +item.requestTimeout : null,
      workUntilDate: item?.workUntilDate ? addHours(new Date(item.workUntilDate), BROWSER_TIMEZONE_OFFSET) : null,
      createdAt: item?.createdAt ? addHours(new Date(item.createdAt), BROWSER_TIMEZONE_OFFSET) : null,
      updatedAt: item?.updatedAt ? addHours(new Date(item.updatedAt), BROWSER_TIMEZONE_OFFSET) : null,
    };
  }

  toForm(model: WebhookModel) {
    return {
      ...model,
      requestTimeout: model.requestTimeout ? model.requestTimeout : '',
      workUntilDate: model.workUntilDate ? format(model.workUntilDate, 'yyyy-MM-dd HH:mm:ss') : null,
    };
  }

  toJson(data: WebhookModel) {
    return {
      enabled: data.enabled === true,
      endpoint: data.endpoint || '',
      eventName: data.eventName || '',
      headers: data.headers ? safeParseJson(data.headers) : null,
      requestTimeout: data.requestTimeout ? +data.requestTimeout : null,
      workUntilDate: data.workUntilDate ? format(new Date(data.workUntilDate), 'yyyy-MM-dd HH:mm:ss') : undefined,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

11. Connecting a localization pipe to display dates on the front

In all places where we display the date, we should add processing via a pipe.

Example of adding a pipe apps/client/src/app/app.component.html

<nz-layout class="layout">
  <!-- ... -->
  <nz-footer class="flex justify-between">
    <!-- ... -->
    <div id="serverTime">{{ (serverTime$ | async)! | translocoDate : { dateStyle: 'medium', timeStyle: 'medium' } }}</div>
  </nz-footer>
</nz-layout>
Enter fullscreen mode Exit fullscreen mode

12. Adaptation of tests related to interface localization

Until now, in the interface, we displayed dates in the format received from the backend.

Now, thanks to the implementation of real-time localization, all data with dates is automatically adapted to the user's settings.

Accordingly, all our tests that check the output data containing dates stopped working correctly.

The number of necessary changes is large, but the principle of adaptation is the same everywhere.

Example of updating the test apps/client-e2e/src/ru-example.spec.ts

import { expect, Page, test } from '@playwright/test';
import { join } from 'path';
import { setTimeout } from 'timers/promises';

test.describe('basic usage (ru)', () => {
  // ...

  // <-- updates
  test('has serverTime format should be equal to "21 Π΄Π΅ΠΊ. 2024 Π³., 13:56:00" without "13:56:00"', async () => {
    await page.goto('/', {
      timeout: 7000,
    });

    await setTimeout(4000);

    const serverTime = await page.locator('#serverTime').innerText();
    expect(
      serverTime
        .split(' ')
        .filter((p, i) => i !== 4)
        .join(' ')
    ).toEqual(
      new Intl.DateTimeFormat('ru-RU', {
        dateStyle: 'medium',
        timeStyle: 'medium',
      })
        .format(new Date())
        .split(' ')
        .filter((p, i) => i !== 4)
        .join(' ')
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

13. Generating additional files, updating dictionaries and launching the infrastructure in development mode

Now we will start generating additional files, update dictionaries and activate the application infrastructure in development mode.

After that, we will perform a final functionality check through E2E tests.

Commands

npm run manual:prepare
npm run translates
npm run pm2-full:dev:start
npm run pm2-full:dev:test:e2e
Enter fullscreen mode Exit fullscreen mode

Conclusion

Although my goal was to change the code as little as possible, it turned out to be quite a large update again, despite adding only one field with the Date type.

New field types rarely need to be added, because before starting a project, a thorough analysis of future tasks is usually carried out and the main types of objects are determined, for which the corresponding input and output components are developed.

At the moment, the project provides examples of working with various data types: string values, numbers, dictionaries, switches, files and date-time.

These types are quite sufficient for creating a small CRM system.

If additional customization is needed, you can look at the implementation of custom components for file and date-time.

Plans

The main aspects of writing typical REST code have already been covered in previous articles.

I intentionally did not touch upon the issues of queue integration and working with microservices, as they deserve separate series of articles not related to the current REST boilerplate.

To date, the production part of the project is tightly linked to the creation of Docker images and deployment to Kubernetes, which can be excessively complex for many frontend and backend developers.

In the next article, I will try to describe a simplified approach to DevOps, focused on free or shareware cloud solutions.

Links

P.S.

Happy New Year 2025! I wish you all health, love and good luck! πŸ₯³πŸ₯³πŸ₯³

fullstack Article's
30 articles in total
Favicon
Getting Started with MERN Stack: A Beginner's Guide
Favicon
Understanding Microservices Architecture in Full-Stack Applications
Favicon
Looking for Middle Frontend Platform/Fullstack Engineer
Favicon
Bootcamp vs. Self-Taught: Which path is the best?
Favicon
System.out.println()
Favicon
Angular Signals and Their Benefits
Favicon
Converting date by user time zone in "NestJS", and entering and displaying date in "Angular"
Favicon
AI and the Full Stack Developer in 2025
Favicon
Should I look for a new job?
Favicon
System.out.println()
Favicon
Constructor
Favicon
lowCalAlt_update 6
Favicon
7 Practical Ways to Build Backends Much Faster as a Developer
Favicon
Dumriya Live - AWS Oriented Full Stack Application Infrastructure Overview
Favicon
Hello,help review my fullstack website stack : nestjs,mongodb and reactjs. https://events-org-siiv.vercel.app/
Favicon
Full stack development
Favicon
Title: "My Journey as an Aspiring Full-Stack Developer" Introduction: Hello, dev.to community! My name is Shoeb Ahmad, and I'm an aspiring full-stack developer. My journey into web development began with a passion for creating dynamic and responsive web
Favicon
Java Constructors
Favicon
Best Leading Software Development Company In Lucknow
Favicon
What is Microfrontend, and How to Implement It?
Favicon
Building REST APIs vs GraphQL: Which One is Right for Your Project?
Favicon
https://dev.to/hanzla-baig/full-stack-developers-roadmap-dch
Favicon
Full-Stack Next.js 15 Development Using Zod, Typescript, tRPC, react-query, and Sequelize ORM
Favicon
Complete Full-Stack Web App Development Roadmap
Favicon
πŸš€ Developers, Are You Betting on the Right Framework? πŸš€
Favicon
Want to be a better full-stack developer? Know the web and HTTP principles
Favicon
DTO & DAO in Software Development
Favicon
Advanced JavaScript concepts that every Javascript devloper should know!
Favicon
Mastering Full Stack Development: Your Gateway to a Dynamic Career
Favicon
Navigating the Web Development Landscape: Finding Your Role

Featured ones: