Table of Contents

  1. Introduction
  2. Setup
  3. ParametersModule
  4. Authorization Guards
  5. Azure Function and CosmosDB
  6. Conclusion
  7. Resources

Introduction

When I asked a colleague to validate my code structure for this blog, he asked me “Why would one use TypeScript in the backend at all?”. He’s a Java programmer and didn’t know TypeScript’s properties very well. An introduction: TypeScript is an asynchronous, functional programming language which compiles down to plain JavaScript. It supports interfaces, classes and access modifiers like private, protected and public.

When first using TypeScript, it felt less easy than using Spring Boot, which I had used prior during Java programming. This is where NestJS comes in, a NodeJS framework built for the backend with Object Oriented Programming in mind. If you have worked with a framework like Spring before, NestJS will be quite easy for you to understand. It requires a modular way of working, which makes sure the application stays well organised.

In this blog, we take a dive into using NestJS in a serverless application hosted in an Azure Function and connect it with a CosmosDB. Of course, NestJS can be integrated with other serverless services like AWS Lambda and Cognito. However, I found the process of converting this application to a serverless function to be a very smooth solution requiring only one(!) command.

Setup

I made a little application which can save and return three parameters of your body: weight, fat percentage and muscle percentage. The app is automatically deployed to an Azure function using Azure Pipelines and saves those parameters to a CosmosDB. I made the code available on GitHub for you to learn from, as we won’t touch on everything in the repository in this blog post.

The user can send new data to the application, which will keep the history of the three parameters in a CosmosDB. Apart from the main AppModule, I only added two modules, the ParametersModule and the LoggerModule to the application. This blog post won’t explain building the LoggerModule, as it only serves to create a custom logger. The NestJS documentation provides a very clear explanation of how to use a custom logger.

The application has two (basic) guards set up for the HTTP calls, one for authorization and one for role-based access. For this guard, we use a simple bearer token, however, integration with eg. Cognito or JWT tokens is available.

ParametersModule

A Module in NestJS provides a clear way of organizing the project and enabling clear dependency injection. There are four important things you can mark within a module:

  • controllers: classes which capture incoming HTTP calls
  • providers: classes marked with NestJS’s @Injectable(), made available for dependency injection
  • imports: modules that need to be imported, again for dependency injection
  • exports: subset of the providers that need to be exported for use in other modules

The ParametersModule imports three other modules: the previously explained LoggerModule, the ConfigModule and the AzureCosmosDBModule. The ConfigModule is used to be able to access environment variables from a .env file or from the configuration of the Azure Function. Note that this is also possible with a package such as dotenv, however this isn’t very ideal as we would have to access process.env directly every time. Of course, pushing those environment files to Git is bad practice. You can find a .env-sample file in the repository, which is used to show which variables need to be filled in the .env file.

@Module({
  controllers: [ParametersController],
  providers: [
      ParametersService, 
      ParametersRepository,
      {
        provide: APP_GUARD,
        useClass: RolesGuard,
      },
  ],
  imports: [
      LoggerModule,
      ConfigModule,
      AzureCosmosDbModule.forFeature([{dto: ParametersEntity}])
  ]
})
export class ParametersModule {}

The first step when receiving an HTTP request to the application is the ParametersController as shown below. This controller will catch all requests on the ‘parameters’ endpoint. Using annotations, you can:

  • Make a check for the type of incoming request and divide the traffic accordingly. This is similar to the way annotations work in the Spring Framework (eg. @PostMapping).
  • Customise which HTTP code you want to return on successful calls, as I did with the createParameters method.
  • Use the @Res() from express to send a completely customised response, however I did not use that here.
  • Execute Guards before being able to activate the method

Let’s focus on the Post() method. It first asks the parametersService to check if there is an object with the given userName present in the database. We could ask the parametersRepository for this information directly, however, having this layer of abstraction is essential for having cleaner code. If there is no object present yet, it will create a new one, otherwise, it will update the existing one. For this update, it will map the new info to the existing object. Again, we use the parametersService for its abstraction layer, the controller should only be used for methods capturing HTTP calls.

@Controller('parameters')
export class ParametersController {
    constructor(private readonly parametersService: ParametersService, private readonly loggerSerivce: LoggerService) {
        this.loggerService.setContext('ParametersController');
    }

    @Post()
    @HttpCode(HttpStatus.CREATED)
    @UseGuards(AuthGuard)
    async createParameters(@Body() parametersDto: ParametersDto): Promise<ParametersEntity> {
        if(!parametersDto || parametersDto.userName || (!parametersDto.bodyWeight && !parametersDto.fatPercentage && !parametersDto.musclePercentage)) {
            // could implement @Res() from express to send a proper response to say it should at least contain one of the parameters or a userName
            return;
        }

        const existingParams: ParametersEntity = await this.parametersService.getParametersEntityByUserName(parametersDto.userName);

        if (existingParams) {
            const updatedParams: ParametersEntity = this.parametersService.mapDtoToEntity(parametersDto, existingParams);
            return this.parametersService.update(updatedParams);
        } else {
            return this.parametersService.create(parametersDto)
        }
    }

    @Get(':id')
    @UseGuards(AuthGuard)
    @Roles('user')
    async findOne(@Param('userName') userName: string): Promise<ParametersEntity> {
        return this.parametersService.getParametersEntityByUserName(userName);
    }

    @Get()
    @UseGuards(AuthGuard)
    @Roles('admin')
    async findAll(): Promise<ParametersEntity[]> {
        return this.parametersService.getAll();
    }
}

Moving on to the ParametersService, we’ll only take a glance at the create() function. When receiving an HTTP call, it will contain values for at least one of our three parameters. In this method, we just check the values and add them to the respective array. The update field contains the moment that the value gets updated to track the user’s progress over time. The parameters will then be put into a ParametersEntity (Discussed in Azure Function and CosmosDB) and added to the database using the parametersRepository.

@Injectable()
export class ParametersService {
    constructor(private readonly parametersRepository: ParametersRepository) {
    }

    async create(parametersDto: ParametersDto): Promise<ParametersEntity> {
        let bodyweight: BodyweightDto;
        let fatPercentage: PercentageDto;
        let musclePercentage: PercentageDto;

        if (parametersDto.bodyWeight) {
            bodyweight = {
                weight: Array.from([parametersDto.bodyWeight]),
                update: [new Date()]
            }
        }
        if (parametersDto.fatPercentage) {
            fatPercentage = {
                percentage: Array.from([parametersDto.fatPercentage]),
                update: [new Date()]
            }
        }
        if (parametersDto.musclePercentage) {
            musclePercentage = {
                percentage: Array.from([parametersDto.musclePercentage]),
                update: [new Date()]
            }
        }

        return this.parametersRepository.create(new ParametersEntity(parametersDto.userName, bodyweight, fatPercentage, musclePercentage));
    }

    // More code
}

The create() function in the ParametersRepository adds the date the object was created and adds it to the database. We see a good example of the loggerService here too. First, we set the context to ‘ParametersRepository’, so that, when it logs something, it will show that the log came from this class. This way, logs can easily be retraced to its origin.

@Injectable()
export class ParametersRepository {

    constructor(@InjectModel(ParametersEntity) private readonly container: Container, private loggerService: LoggerService) {
        this.loggerService.setContext('ParametersRepository');
    }

    async create(item: ParametersEntity): Promise<ParametersEntity> {
        item.createdAt = new Date();
        const response = await this.container.items.create(item);
        this.loggerService.verbose(`Create RUs: ${response.requestCharge}`);
        return response.resource;
    }

Authorization Guards

The project uses two guards, an AuthGuard for the authorization, and a RolesGuard to check which roles can access certain resources. A good explanation of both can be found in the NestJS documentation. The RolesGuard is almost an exact copy from the documentation, so let’s take a look at the AuthGuard which doesn’t need to be provided from a module.

The canActivate() method is called before executing the method in the controller. It needs to return true, or the method won’t execute and the application will return a 401 Unauthorized code. In this case, we check if the authorization header has the correct value as configured in the environment variables. Other setups, like OAuth, Cognito or JWT tokens are also possible.

@Injectable()
export class AuthGuard implements CanActivate {
    constructor(private readonly configService: ConfigService) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();
        if (!req.headers.authorization) {
            return false;
        }
        if (req.headers.authorization.split(' ')[0] !== 'Bearer') {
            throw new HttpException('Invalid token', HttpStatus.FORBIDDEN);
        }
        const token = req.headers.authorization.split(' ')[1];

        return token === this.configService.get<string>('BEARER_TOKEN');
    }
}

Azure Function and CosmosDB

To convert this app into an Azure Function and make it serverless, we only need a single command:

nest add @nestjs/azure-func-http

This will add some files and folders, including a main.azure.ts through which your app can be started. It will set a global prefix ‘api’ to all your controllers, the standard for Azure Functions.

export async function createApp(): Promise<INestApplication> {
  const app = await NestFactory.create(AppModule, new AzureHttpRouter());
  app.setGlobalPrefix('api');

  await app.init();
  return app;
}

Now, you can choose to either run your app as a normal Web App or a serverless Azure Function. The only thing left to do is create an Azure Function in the Portal and set up a pipeline, which is also an automatic process (on Azure DevOps). This will generate an azure-pipelines.yml file containing all necessary information and connect it with the function automatically. On every push to the master branch (pull request), it will automatically start a build and deploy process. For the environment variables, they need to be set up within the ‘configuration’ tab of your function, and then, you’re all set.’

Azure Function Creation

Congratulations! You converted your app to a serverless Function! Quite an easy conversion, wasn’t it?

The database connection is just as easy. Again, in the portal, you can create a CosmosDB Account. In that account, go to ‘Data Explorer’ and create a new database and add the necessary variables to the configuration of the Function.

In the end, your app.module.ts should look like this. Notice that we can’t use the ConfigService in this @Module(), as it needs to be initialised before usage.

@Module({
  imports: [
      ConfigModule.forRoot(),
      ParametersModule,
      LoggerModule,
      AzureCosmosDbModule.forRoot({
          dbName: process.env.DATABASE_NAME,
          endpoint: process.env.DATABASE_ENDPOINT,
          key: process.env.DATABASE_KEY,
      })
  ],
  controllers: [
      AppController
  ],
  providers: [
      AppService,
  ],
})
export class AppModule {}

That’s it! Your app is now fully functional!

Conclusion

In this blog post, we made a small application to discover how NestJS can be used in the backend with some of its neat features. Of course, this was a very basic program to show some of the possibilities. For more information on NestJS and its features, check out the very thorough documentation.

Resources

Jasper is a Java Developer with a love for IoT and innovative tech. He has a lot of experience in building chatbots using various platforms and coupling them to different chat clients. He also has a passion for the Agile way of working, Scrum and communication, both inside and outside the team.