View on GitHub

Povio Engineering Guidelines

These are guidelines for the technologies we are using.

Nest.js Guidelines

Overview

The guidelines represent an intersection among several Povio developer opinions that already have experiences with production ready NestJS apps.

There is no need to take these guidelines as something that you must use in Povio projects but rather as a reference to which you can always return when you’re looking for ideas related to NestJS code architecture, patterns or even reusable code snippets.

Because NestJS is relatively young framework, we can expect new features that will affecting these guidelines in the future. So if you feel something is outdated or needs to be included, please make a PR and share your knowledge.

Project structure

Here is a recommended project structure:

Top level folder stricture looks like:

src
  ├── assets        // static assets
  ├── common        // Common helpers or some high-level base classes (eq. base entity)
  ├── constants     // global constants and enums
  ├── decorators    // application decorators
  ├── entities      // data model entities (typeorm builds db from them)
  ├── guards        // custom NestJs guards
  ├── middleware    // custom middlewares
  ├── interfaces    // global interfaces
  ├── dtos          // general purposes DTOs
  ├── migrations    // migrations generated with typeorm migrations command
  ├── modules       // Business logic of application
  └── services      // 3rd-party integration services or general use services

Closer look to application module structure:

modules
├── address
   ├── address.controller.ts
   ├── address.module.ts
   ├── address.repository.ts
   ├── address.service.spec.ts
   ├── address.service.ts
   ├── dto
   └── enums

Closer look to application services structure. If it’s possible, use domain driven development approach, where each domain of interest has its dedicated folder. So if we change 3rd party service inside the domain folder, the new one will still be in the same folder.

services
├── email
   ├── email.module.ts
   ├── MailGunService.service.ts
   ├── dto
   └── enums

DTOs

Strive to create DTOs based on request types. Naming follows the pattern of create-x.dto.ts (CreateXDto), update-x.dto.ts (UpdateXDto), get-x.dto.ts (GetXDto), where x is entity name. Place them in entity module folder, next to controller in a dto folder. If DTO doesn’t have a direct link to the entity, then place it in dtos folder that exists on top level.

Working with Exceptions

It’s considered as a good practice to create a custom exception class for a particular service which extends built-in HttpException class. Each custom exception should have an exception filter which is then used at controller, controller’s method or global level.

Custom exception class (service.exception.ts)

A general custom exception:

import { HttpException } from '@nestjs/common/exceptions';

export class ServiceException extends HttpException {
  constructor(protected readonly errorMessage: any, protected readonly statusCode: number, readonly source: string) {
    super(errorMessage, statusCode);
    this.source = source;
  }
}

If you have to extend a known built in exception use the appropriate Nest class as your base (e.g. extend the NotFoundException if you need a custom 404).

Exception filters (service-exception.filter.ts)

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';

@Catch(ServiceException)
export class ServiceExceptionFilter implements ExceptionFilter<ServiceException> {
  catch(exception: ServiceException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();
    const message = exception.message;

    const { headers, url, method, body } = request;
    // You can add log the exeption here using application logger, sentry or other 3rd party service

    response.status(status).json({
      source: exception.source,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      errorCode: `service_exception/${exception.name}`,
      exception: {
        message: exception.message,
      },
    });
  }
}

Even though NestJS has built-in exceptions layer which is responsible for processing all unhandled exceptions across an application, there are situations when some exceptions will get swallowed and leave no trace. As well described here, it’s fine to include these process listeners in the main.ts file:

process.on('unhandledRejection', (reason, p) => {
  // I just caught an unhandled promise rejection, since we already have fallback handler for unhandled errors (see below), let throw and let him handle that
  throw reason;
});
process.on('uncaughtException', (error) => {
  // I just received an error that was never handled, time to handle it and then decide whether a restart is needed
  errorManagement.handler.handleError(error);
  if (!errorManagement.handler.isTrustedError(error))
    process.exit(1);
});

Configuration files

Local environment variables should be specified in .env files. There should be an example.env file which every developer should use to create an .env file which should be gitignored.

Sometimes environments are saved inside the repository (development.env, staging.env, production.env, etc.) - this is a bad practice and should be avoided.

For better readability variables should be grouped based on what they’re for. For instance database connection info variables should be grouped together, JWT settings variables together, etc. Before each group there is preferably a description or group name.

Then environment variables are loaded through Config module:

(config.module.ts)

import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()
@Module({
  providers: [
    {
      provide: ConfigService,
      useValue: new ConfigService(`.${process.env.NODE_ENV}.env`),
    },
  ],
  exports: [ConfigService],
})
export class ConfigModule {}

(config.service.ts)

import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable } from '@nestjs/common';

@Injectable()
export class ConfigService {
  private readonly envConfig: { [key: string]: string };

  constructor(filePath: string) {
    const configFile = fs.existsSync(`${filePath}.local`) ? `${filePath}.local` : filePath;
    this.envConfig = fs.existsSync(configFile) ? dotenv.parse(fs.readFileSync(configFile)) : process.env;
  }

Here is also an idea of how various config data could be organized through config module services.

Custom decorators

It’s highly recommended that whenever you feel a custom decorator could be a good fit for abstracting away some boilerplate code, create one.

Some of your colleagues already made some custom decorators which make working with Swagger documentation in NestJS project a bit more plesant. You can find them here.