TECH
BLOG

How to build an Azure Functions development environment with NestJS

2020
10

beginning

The source code for this article is available at the following GitHub repository.
https://github.com/nikaera/azure-nestjs-sample

PlayFab of CloudScript Towards Azure Functions It was decided to develop, and initially .NET Azure Functions I was considering adopting it.

However, it was a project where development speed was required, and there were no human resources who could write C#, and they used Mac a lot, and they wanted to use Node.js, so we selected a development tool that seemed good among them.

Results for NestJS Azure Functions HTTP module I decided to adopt it.
In the end, the following were the deciding factors.

  1. As a means of accessing Azure Functions HTTP Trigger Use
  2. Azure Functions can be developed just like web application development
  3. Azure Cosmos DB Heha MongoDB API If you use TypeORM Module It is possible to connect using
  4. Easy to write unit tests and E2E tests

In this article, I will describe the following.

  • Starting with the introduction of NestJS and setting up a development environment
  • Development of Azure Functions involving Azure Cosmos DB and construction of a test environment
  • Building a CI environment using GitHub Actions
  • Steps to managing secret information using Azure KeyVault

operating environment

  • Azure Functions Core Tools
  • Core 2.11.1
  • func 3.0.2931
  • Node.js 10.22.1
  • MongoDB 4.4.1
  • Docker 19.03.13
  • Docker Compose 1.27.4

Deploying Azure Functions Core Tools

When developing Azure Functions, there are situations where you want to complete deployment work from a terminal in order to check operation locally or build a CI environment.

Therefore, first of all, it is a tool for the above Azure Functions Core Tools Official site instructionsInstall it according toIn this article, I'm using the v3.x system.

公式サイトの手順


Azure Functions Core Tools installation instructions on the official website

Installing NestJS and introducing the Azure Functions HTTP module

Official siteAs far as I can see, at the moment (2020/10/23) You need to use 10.x or 12.x as your Node.js version. I used v10.22.1 in this article.

Make sure you have Node.js version 10.x or 12.x installed.

Anyway, since NestJS uses CLI for development, the first step is to install the CLI tool globally.

npm install @nestjs /cli -g

Once the installation is complete, create a NestJS project via CLI and even import the Azure Functions HTTP module.

# Create a NestJS application
Nest new azure-sample
# Importing the Azure Functions HTTP module into the NestJS application
CD AZURE-SAMPLE
nest add @nestjs /azure-func-http

If the command has been successfully executed, the project folder should have the following structure.

Check the project folder structure with the # tree command
tree -I node_modules -L 2. /
. /
├── README.md
├── host.json
├── local.settings.json
├── main
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── node_modules
├── nest-cli.json
├── package-lock.json
├── package.json
├── proxies.json
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.azure.ts
│ └ ── main.ts
├── test
│ ├── app.e2e-spec.ts
│ └ ── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

The development environment for Azure Functions is now ready.
Let's check if it's in a state where operation verification is possible right away.

Launch an Azure Functions environment locally

Since the getHello function already exists in src/app.controller.ts, let's make it accessible with the path called helloworld.

src/app.controller.ts

import {Controller, Get} from '@nestjs /common';
import {AppService} from '. /app.service ';
@Controller ()
export class AppController {
constructor (private readonly AppService: AppService) {}
//By describing helloworld in @Get,
//Can be accessed with an endpoint such as http://localhost:7071/api/helloworld
@Get ('helloworld')
getHello (): string {
return this.appservice.getHello ();
}
}

Once the above modifications are complete, let's run the npm run start:azure command.

If the command is successfully executed, you will be able to access various APIs at http://localhost:7071/api/{*segments}.

Let's quickly access the API we made accessible earlier at http://localhost:7071/api/helloworld.

http://localhost:7071/api/helloworld のアクセス結果


http://localhost:7071/api/helloworld Access results for

Hello World! on the screen If the string is displayed, it is successful.

Prepare for feature development with NestJS

In this article, we'll develop a function to CRUD events.

In NestJS modules It is developed on a unit basis.By dividing and combining programs in modules, we will proceed with the implementation of functions. The criteria for separating modules are delegated to developers and therefore depend on the architecture adopted.

Now let's quickly create a new module with the nest g module events command.

# Create a module
Nest g module events
CREATE src/events/events.module.ts (82 bytes)
UPDATE src/app.module.ts (370 bytes)

Then, at the same time that a new module is created, the description for reading that module is automatically written to src/app.module.ts.

AppModule is called the root module in NestJS.
This is the module that is loaded first when the application is started.

src/app.module.ts

import {Module} from '@nestjs /common';
import {AppController} from '. /app.controller ';
import {AppService} from '. /app.service ';
//load the newly added EventsModule with import
import {eventsModule} from '. /events/events.module ';
@Module ({
//read eventsModule from AppModule which is the root module
imports: [EventsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Next, in order to create an EventsModule entity controllers I will create it.NestJS controllers are responsible for controlling input and output. Specifically, we perform routing and handling according to requests.

Similar to modules, new controllers are created with the nest g controller events command in NestJS.

# Create a controller
Nest g controller events
CREATE src/events/events.controller.spec.ts (485 bytes)
CREATE src/events/events.controller.ts (99 bytes)
UPDATE src/events/events.module.ts (170 bytes)

The*.spec.ts file is a test file for the generated class.
NestJS is a testing framework Jest It has been adopted.

A controller has been created, and the description for reading the controller has been automatically written into the module created earlier.In this way, in NestJS, when scaffolding (generating a group of files necessary for development) with CLI, when specifying a path with the same name, an import description is automatically added.

src/events/events.module.ts

import {Module} from '@nestjs /common';
//load the newly added EventsController with import
import {eventsController} from '. /events.controller ';
@Module ({
//Description for loading EventsController has been added
controllers: [eventsController]
})
export class EventsModule {}

Next, before implementing functions into the controller, new providers I will create it.NestJS providers refer to classes in general that take on the role of the so-called service layer. It indicates repositories, factories, helpers, etc.

This time, I'm going to create a new provider called EventsService and write an implementation to CRUD events. By using EventsService in the controller, CRUD processing can be executed via the controller. Create a new provider with the nest g service events command.

# Create a provider (service)
Nest g service events
CREATE src/events/events.service.spec.ts (453 bytes)
CREATE src/events/events.service.ts (89 bytes)
UPDATE src/events/events.module.ts (247 bytes)

An EventsService is created, and the description for loading the EventsService is automatically written to the EventsModule.

src/events/events.module.ts

import {Module} from '@nestjs /common';
import {eventsController} from '. /events.controller ';
//load the newly added EventsService with import
import {eventsService} from '. /events.service ';
@Module ({
controllers: [eventsController],
//Description for loading EventsService has been added
providers: [eventsService]
})
export class EventsModule {}

Now we're ready to implement the event CRUD functionality as an Azure Functions function.

As mentioned above,In NestJS, the general flow is to repeat the flow of scaffolding (generating a group of files necessary for development) using the nest command and then proceeding with implementation.

Build an environment for Azure Cosmos DB development locally

When implementing the CRUD function for events,Azure Cosmos DB will be used as the database this time, but MongoDB will be used when developing locally.

Therefore, first for development MongoDB Docker images You can use it. When E2E tests are carried out in the future Docker Compose In order to assume that it will be used, I will write a description using MongoDB in devenv/docker-compose.yml.

devenv/docker-compose.yml

version: '3.8'
services:
Mongodb:
image:mongo:latest
ports:
- "27017:27017”
environment:
MONGO_INITDB_ROOT_USERNAME: azure-sample
MONGO_INITDB_ROOT_PASSWORD: azure-sample
MONGO_INITDB_DATABASE: azure-sample
TZ:Asia/Tokyo

After writing devenv/docker-compose.yml, start the MongoDB container in a daemon state with the following command.

# Start a MongoDB container as a daemon with Docker Compose
CD devenv
docker-compose up -d
# Check if the MongoDB container was started (successful if devenv_mongodb_1 is displayed in the NAMES column)
Docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
f78f09cd4f5b mongo: latest “docker-entrypoint.s...” About a minute ago Up About a minute 0.0.0. 0:27017 ->27017/tcp devenv_mongodb_1

If it starts successfully, you should be able to confirm that the container named devenv_mongodb_1 is running with the Docker ps command.

Now the environment for developing with MongoDB is ready, but since I'm not using it yet, I'm going to stop the container with the docker-compose down command.

# Stop containers started with Docker Compose
CD devenv
Docker compose down
Remaining devenv_mongodb_1... done
Remaining devenv_mongodb_1... done
Defaults network devenv_default

Implement CRUD functionality for events using TypeORM

Now let's quickly implement the CRUD function for events with TypeORM.

As a flow, we will first be able to manage database URIs with configurations, and then define a table schema for events. After that, we will implement the CRUD functionality for events.

Manage database connection information with the NestJS Configuration module

First, information for connecting to MongoDB with TypeORM in NestJS Configuration module I will be able to manage it using.

# Install the Configuration module
npm i --save @nestjs /config

The configuration module is internally Dotenv I'm using it. Therefore, the notation is the same as dotenv.

.env

mongoDB_uri=MongoDB: //azure-sample: azure-sample @localhost: 27017/azure-sample? authSource=admin

The connection URI to MongoDB was described as MONGODB_URI in a.env file. The description for reading the above with NestJS will be added to src/app.module.ts.

src/app.module.ts

import {Module} from '@nestjs /common';
import {ConfigModule} from '@nestjs /config';
import {AppController} from '. /app.controller ';
import {AppService} from '. /app.service ';
import {eventsModule} from '. /events/events.module ';
@Module ({
imports: [
//By adding the ConfigModule description, .env variables will be read automatically
configModule.forRoot (),
eventsModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Since it is read by the root module, variables described in an.env file read by ConfigModule can be used in all modules. I'll try adding console.log to the EventsModule to check if it seems to load properly.

src/app.controller.ts

import {Controller, Get} from '@nestjs /common';
import {AppService} from '. /app.service ';
@Controller ()
export class AppController {
constructor (private readonly AppService: AppService) {}
@Get ('helloworld')
getHello (): string {
return this.appservice.getHello ();
}
//Added an API to check the value of a variable when accessing http://localhost:7071/api/database-uri
@Get ('database-uri')
getDatabaseURI (): string {
return process.env.mongodb_uri;
}
}

# Start the Azure Functions local environment
npm run start: Azure

The result of rewriting src/app.controller.ts and accessing http://localhost:7071/api/database-uri after running npm run start: Azure should be as follows.

http://localhost:7071/api/database-uri のアクセス結果


http://localhost:7071/api/database-uri Access results for

Once you have confirmed that the variables seem to have been read safely, let's delete the getDatabaseURI function. There are security issues if this API remains in production environments.

Now we have confirmed that the information for connecting to MongoDB with TypeORM can be managed with ConfigModule.

Introduce NestJS's TypeORM Module to define tables

Next, NestJS's TypeORM Module install. Also, in order to connect to MongoDB this time Mongoose I'll also install it at the same time.

Basically, any database supported by TypeORM can be used with NestJS's TypeORM Module. For example, it seems that PostgreSQL, MySQL, SQLite, etc. are supported in addition to MongoDB.

# Install the libraries required to use the TypeORM Module in Mongoose with NestJS
npm i --save @nestjs /typeorm typeorm @nestjs /mongoose mongoose
# Install additional Mongoose type information for use with TypeScript
npm i --save-dev @types /mongoose

From describing how to connect to MongoDB with TypeORM to defining event tablesOfficial page instructionsProceed along the way. First, the process of connecting to MongoDB is described in src/app.module.ts.

src/app.module.ts

import {Module} from '@nestjs /common';
import {ConfigModule} from '@nestjs /config';
import {MongooseModule} from '@nestjs /mongoose';
import {AppController} from '. /app.controller ';
import {AppService} from '. /app.service ';
import {eventsModule} from '. /events/events.module ';
@Module ({
imports: [
configModule.forRoot (),
//use MongoSeModule to connect to MongoDB
mongooseModule.forRoot (process.env.mongodb_URI),
eventsModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

I'll write the event table definition in src/events/events.schema.ts. There is only a name attribute in the event table.

src/events/events.schema.ts

import {Prop, Schema, SchemaFactory} from '@nestjs /mongoose';
import {Document} from 'mongoose';
//define the event table
@Schema ()
export class Event extends Document {
//has only the required field holding the event name
@Prop ({required: true})
name: string;
}
export const eventSchema = schemaFactory.createForClass (Event);

Next, we will add a description for using EventSchema to the module EventsModule that handles event table operations.

src/events/events.module.ts

import {Module} from '@nestjs /common';
import {MongooseModule} from '@nestjs /mongoose';
import {eventsController} from '. /events.controller ';
import {eventsService} from '. /events.service ';
import {Event, EventSchema} from '. /events.schema '
@Module ({
//import the EventSchema defined earlier with MongooseModule
//Events can now be used in Controllers and Providers
//This time it is assumed that it will be used with EventsService, which is a provider
imports: [
mongooseModule.forfeature ([
{name: event.name, schema: EventSchema},
]),
],
controllers: [eventsController],
providers: [eventsService]
})
export class EventsModule {}

Implement CRUD functionality for events with TypeORM

Using the class Event, which defines an event table, we will first implement CRUD in EventsService.

src/events/events.service.ts

import {Injectable} from '@nestjs /common';
import {injectModel} from '@nestjs /mongoose';
import {Model} from 'mongoose';
import {Event} from '. /events.schema '
@Injectable ()
export class EventsService {
<Event>//Constructor injection creates a Model imported by EventModule
Constructor (
//If imported with MongooseModule, it's prepared by @nestjs /mongoose
//The @InjectModel decorator requires a name to be defined during injection
@InjectModel (event.name)
<Event>private eventModel: Model,
) {}
//create an Event
<Event>async create (name: string): Promise {
const createDEvent = new this.eventModel ({name: name});
return createdEvent.save ();
}
//read the event with the specified Id
<Event>async readOne (id: string): Promise {
return this.eventModel.findById (id) .exe ();
}
//read all events
<Event [] >async readAll (): Promise {
return this.EventModel.find () .exe ();
}
//update the event specified by Id
<Event>async update (id: string, name: string): Promise {
//When updating data, specify {new: true} as an option to return the updated data
//If {new: true} is not specified, the data before the update will be returned
return this.eventModel.findByIdAndUpdate (
id, {name: name}, {new: true}
) .exe ();
}
//delete the event specified by Id
<Event>async delete (id: string): Promise {
return this.eventModel.findByIdAndDelete (id) .exe ();
}
}

Next, I'd like to implement EventsController, but before that, I'll create various DTOs that also serve as data validation for requests and responses.

src/events/events.dto.ts

//Body Request definition for POST events
export interface createRequest {
name: string
}
//Body Request definition for PATCH events/: id
export interface updateEventDTO {
name: string
}

Once you've created a DTO for the request parameters, implement EventsController.

src/events/events.controller.ts

import {Param, Body, Controller, Get, Post, Patch, Delete} from '@nestjs /common';
import {eventsService} from '. /events.service '
import {Event} from '. /events.schema '
import {
createRequest,
idParams,
UpdateRequest
} from '. /events.request '
@Controller ('events')
export class EventsController {
//create an EventsService with constructor injection and use it with EventsController
constructor (private readonly eventsService: eventsService) {}
//function called when POST events are accessed
@Post ()
<Event>async create (@Body () request: createEventDTO): Promise {
return this.eventsservice.create (request.name)
}
//GET events/: function called when id is accessed
@Get (':id')
<Event>async readOne (@Param ('id') id: string): Promise {
return this.eventsservice.readOne (id)
}
//function called when GET events are accessed
@Get ()
<Event [] >async readAll (): Promise {
return this.eventsService.readAll ()
}
//PATCH events/: function called when id is accessed
@Patch (':id')
<Event>async update (@Param ('id') id: string, @Body () request: updateEventDTO): Promise {
return this.eventsservice.update (id, request.name)
}
//DELETE events/: function called when id is accessed
@Delete (':id')
<Event>async delete (@Param ('id') id: string): Promise {
return this.eventsservice.delete (id)
}
}

In NestJS, when defined as events/ :id, the contents of :id can obtain values from arguments by using the @Param decorator.

Verify the normal operation of various APIs with curl

Let's check the normal operation of the various APIs with curl.

#1. Launch an Azure Functions local environment
npm run start: Azure
#2. Start a MongoDB instance with Docker Compose
CD devenv
docker-compose up -d
#3. Create 2 Events
curl -X POST -H “Content-Type: application/json” -d '{"name”: “test-event-1"}' http://localhost:7071/api/events
{"_id” :"5f945260a503fa9ceae111ae”, "name” :"test-event-1", "__v” :0}
curl -X POST -H “Content-Type: application/json” -d '{"name”: “test-event-2"}' http://localhost:7071/api/events
{"_id” :"5f945278a503fa9ceae111af”, "name” :"test-event-2", "__v” :0}
#4. Load a specific Event
curl -X GET http://localhost:7071/api/events/5f945260a503fa9ceae111ae
{"_id” :"5f945260a503fa9ceae111ae”, "name” :"test-event-1", "__v” :0}
#5. Load all registered events
curl -X GET http://localhost:7071/api/events
[{"_id” :"5f945260a503fa9ceae111ae”, "name” :"test-event-1", "__v” :0}, {"_id” :"5f945278a503fa9ceae111af”, "name” :"test-event-2", "__v” :0}]
#6. Update specific events
curl -X PATCH -H “Content-Type: application/json” -d '{"name”: “test-event-3"}' http://localhost:7071/api/events/5f945260a503fa9ceae111ae
{"_id” :"5f945260a503fa9ceae111ae”, "name” :"test-event-3", "__v” :0}
# 6-1 Check if a specific Event has been updated
curl -X GET http://localhost:7071/api/events/5f945260a503fa9ceae111ae
# I was able to confirm that the value that was previously test-event-1 has been updated to test-event-3
{"_id” :"5f945260a503fa9ceae111ae”, "name” :"test-event-3", "__v” :0}
#7. Delete a specific Event
curl -X DELETE http://localhost:7071/api/events/5f945260a503fa9ceae111ae
{"_id” :"5f945260a503fa9ceae111ae”, "name” :"test-event-3", "__v” :0}
# 7-1 Check if a specific Event has been deleted
curl -X GET http://localhost:7071/api/events
# When all events are obtained, only 1 out of 2 registrations will be returned, 7. I was able to confirm that data including the ID deleted by will not be returned
[{"_id” :"5f945278a503fa9ceae111af”, "name” :"test-event-2", "__v” :0}]
#8. Destroy MongoDB instance with Docker Compose
CD devenv
Docker compose down

The implementation and operation verification of the CRUD function of the event has been completed, but since it is troublesome to manually perform these operation verifications every time the function is modified, we will implement E2E tests.

Also, the E2E test environment will be built with Docker Compose.

Implement E2E tests for event CRUD functionality

Now let's write E2E tests for normal and abnormal systems of the CRUD API for events.

In NestJS, the place to write E2E tests is in the test folder.Also, when running tests, environment variables are managed with.env.test instead of.env. This is because I want to keep the local verification environment and the configuration in the test environment separate.

.env.test

# Currently, the same content as.env is also described in.env.test
mongoDB_uri=MongoDB: //azure-sample: azure-sample @localhost: 27017/azure-sample? authSource=admin

app.e2e-spec.ts

import {Test, TestingModule} from '@nestjs /testing';
import {InestApplication} from '@nestjs /common';
import * as request from 'supertest';
import {ConfigModule} from '@nestjs /config';
import {MongooseModule} from '@nestjs /mongoose';
import {eventsModule} from '.. /src/events/events.module ';
import {AppController} from '.. /src/app.controller ';
import {AppService} from '.. /src/app.service ';
import {createRequest, UpdateRequest} from '.. /src/events/events.request ';
import {Event} from '.. /src/events/events.schema '
describe ('AppController (e2e) ', () => {
let app: inestApplication;
beforeEach (async () => {
const ModuleFixture: testingModule = await test.createTestingModule ({
imports: [
//load .env.test, which is a dotenv file for testing, as a config
ConfigModule.forRoot ({
envFilePath: '.env.test',
}),
mongooseModule.forRoot (process.env.mongodb_URI),
eventsModule,
],
controllers: [AppController],
providers: [AppService],
}) .compile ();
app = ModuleFixture.createNestApplication ();
await app.init ();
});
//function for retrieving all events
<Event>const getEventAll = async (): Promise <Array >=> {
const res = await request (app.getHttpServer ()) .get ('/events');
expect (res.status) .toEqual (200);
<Event>return res.body as Array;
}
describe ('EventsController (e2e) ', () => {
//Test the event's Create API
describe ('Create API of Event', () => {
//API execution succeeds
it ('OK /events (POST) ', async () => {
const body: createRequest = {
name: “test-event”
}
const res = await request (app.getHttpServer ())
.post ('/events')
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (201);
const eventResponse = res.body as Event;
expect (eventResponse) .toHaveProperty ('_id');
expect (EventResponse.name) .toEqual (body.name);
});
//API execution fails due to incorrect parameters
it ('NG /events (POST) :observed parameters', async () => {
const body = {
name: “test-event”
}
const res = await request (app.getHttpServer ())
.post ('/events')
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (400);
});
//API execution fails with empty parameters
it ('NG /events (POST): Empty parameters. ', async () => {
const body = {}
const res = await request (app.getHttpServer ())
.post ('/events')
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (400);
});
});
//Test the Read API for events
describe ('Read API of Event', () => {
//API execution succeeds
it ('OK /events (GET) ', async () => {
const eventsResponse = await getEventAll ();
expect (EventsResponse.length) .toEqual (1);
});
//API execution succeeds
it ('OK /events/:id (GET) ', async () => {
const eventsResponse = await getEventAll ();
const res = await request (app.getHttpServer ())
.get (`/events/$ {eventsResponse [0]. _id} `);
expect (res.status) .toEqual (200);
const eventResponse = res.body as Event;
expect (eventResponse) .toHaveProperty ('_id');
expect (eventResponse.name) .toEqual ('test-event');
});
//API execution fails with an incorrect id
it ('NG /events/:id (GET) :Invalid id. ', async () => {
const res = await request (app.getHttpServer ())
.get ('/events/xxxxxxxxxxx');
expect (res.status) .toEqual (400);
});
//API execution fails with an id that doesn't exist
it ('NG /events/:id (GET) :id that doesn't exist. ', async () => {
const res = await request (app.getHttpServer ())
.get ('/events/5349b4ddd2781d08c09890f4');
expect (res.status) .toEqual (404);
});
});
//testing the event update API
describe ('Update API of Event', () => {
//API execution succeeds
it ('OK /events/:id (PATCH) ', async () => {
const eventsResponse = await getEventAll ();
const body: updateRequest = {
name: “new-test-event”
}
const res = await request (app.getHttpServer ())
.patch (`/events/$ {eventsResponse [0]. _id} `)
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (200);
const eventResponse = res.body as Event;
expect (eventResponse) .toHaveProperty ('_id');
expect (EventResponse.name) .toEqual (body.name);
});
//API execution fails with an incorrect id
it ('NG /events/:id (PATCH) :Attentive Parameters', async () => {
const eventsResponse = await getEventAll ();
const body = {
name: “new-test-event”
}
const res = await request (app.getHttpServer ())
.patch (`/events/$ {eventsResponse [0]. _id} `)
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (400);
});
//API execution fails with empty parameters
it ('NG /events/:id (PATCH) :Empty parameters. ', async () => {
const eventsResponse = await getEventAll ();
const body = {}
const res = await request (app.getHttpServer ())
.patch (`/events/$ {eventsResponse [0]. _id} `)
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (400);
});
//API execution fails with an empty id
it ('NG /events/:id (PATCH) :Empty id. ', async () => {
const eventsResponse = await getEventAll ();
const body = {
name: “new-test-event”
}
const res = await request (app.getHttpServer ())
.patch ('/events')
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (404);
});
//API execution fails with an incorrect id
it ('NG /events/:id (PATCH) :Invalid id. ', async () => {
const body: updateRequest = {
name: “new-test-event”
}
const res = await request (app.getHttpServer ())
.patch ('/events/xxxxxxxxxxx')
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (400);
});
//API execution fails with an id that doesn't exist
it ('NG /events/:id (PATCH) :id that doesn't exist. ', async () => {
const body = {}
const res = await request (app.getHttpServer ())
.patch ('/events/5349b4ddd2781d08c09890f4')
.set ('Accept', 'application/json')
.send (body);
expect (res.status) .toEqual (404);
});
});
//Test the event's Delete API
describe ('Delete API of Event', () => {
//API execution succeeds
it ('OK /events/:id (DELETE) ', async () => {
const eventsResponse = await getEventAll ();
const res = await request (app.getHttpServer ())
.delete (`/events/$ {eventsResponse [0]. _id} `);
expect (res.status) .toEqual (200);
});
//API execution fails with an empty id
it ('NG /events/:id (DELETE) :Empty id. ', async () => {
const res = await request (app.getHttpServer ())
.delete ('/events')
expect (res.status) .toEqual (404);
});
//API execution fails with an incorrect id
it ('NG /events/:id (DELETE) :Invalid id. ', async () => {
const res = await request (app.getHttpServer ())
.delete ('/events/xxxxxxxxxxx')
expect (res.status) .toEqual (400);
});
//API execution fails with an id that doesn't exist
it ('ng /events/:id (DELETE) :id that doesn't exist. ', async () => {
const res = await request (app.getHttpServer ())
.delete ('/events/5349b4ddd2781d08c09890f4');
expect (res.status) .toEqual (404);
});
});
})
//Destroy the app after each test run (the test cannot be completed without freeing the DB connection)
afterEach (async () => {
await app.close ();
});
});

Let's run the E2E test with the above details. Since I wrote the ideal results for the E2E test, the normal system should be as successful as when manually checked, but the test for the abnormal system should have mostly failed.

#1. Start MongoDB with Docker Compose
CD devenv
docker-compose up -d
#2. Run E2E tests
npm run test: e2e
#3. Destroying MongoDB with Docker Compose
CD devenv
Docker compose down

E2E テストの実行結果


E2E Test Execution Results (Failure)

Modify the event's CRUD function so that E2E tests pass

When adding descriptions around exception handling in order to pass the test, NestJS's Exception filters I will continue to use it.By using NestJS's Exception filters, it is possible to easily implement exception handling, and at the same time, it is possible to set the scope of application of these within an appropriate range.

This time, I want to mainly filter errors around Mongoose, so I'll create an ExceptionFilter related to Mongoose.

src/mongoose.exception.filter.ts

import {ArgumentsHost, Catch, ExceptionFilter, HttpStatus} from '@nestjs /common';
import {Error} from 'mongoose';
@Catch (Error)
export class MongooseExceptionFilter implements exceptionFilter {
catch (exception: Error, host: argumentsHost) {
const response = host.switchToHttp () .getResponse ();
switch (exception.name) {
//If a Mongoose validation error occurs, an HTTP BadRequest error is returned
case error.validationerror.name:
case error.casterror.name:
response.status (httpstatus.bad_request) .json (null);
break;
//Returns an HTTP NotFound error when no data is found in Mongoose
case error.documentNotFounderror.name:
response.status (httpStatus.NOT_FOUND) .json (null);
break;
}
}
}

Apply the MongooseExceptionFilter created earlier to EventsController. This allows exception handling for Mongo DB queries to be set collectively for the entire controller.

src/events/events.controller.ts

import {Param, Body, Controller, Get, Post, Patch, Delete, useFilters} from '@nestjs /common';
import {eventsService} from '. /events.service '
import {Event} from '. /events.schema '
import {
createEventDTO,
updateEventDTO
} from '. /events.dto '
import {MongooseExceptionFilter} from '.. /mongoose.exception.filter ';
@Controller ('events')
//I want to apply it to all functions, so use the @UseFilters decorator on the class definition
//Apply MongooseExceptionFilter to all controller functions
@UseFilters (MongooseExceptionFilter)
export class EventsController {
constructor (private readonly eventsService: eventsService) {}
@Post ()
<Event>async create (@Body () request: createEventDTO): Promise {
return this.eventsservice.create (request.name)
}
@Get (':id')
<Event>async readOne (@Param ('id') id: string): Promise {
return this.eventsservice.readOne (id)
}
@Get ()
<Event [] >async readAll (): Promise {
return this.eventsService.readAll ()
}
@Patch (':id')
<Event>async update (@Param ('id') id: string, @Body () request: updateEventDTO): Promise {
return this.eventsservice.update (id, request.name)
}
@Delete (':id')
<Event>async delete (@Param ('id') id: string): Promise {
return this.eventsservice.delete (id)
}
}

Also, since I want to check the name for null when updating, I will modify the EventsService. When null, Mongoose Errors are explicitly created and thrown to supplement MongooseExceptionFilter with exceptions.

src/events/events.service.ts

import {Injectable} from '@nestjs /common';
import {injectModel} from '@nestjs /mongoose';
import {Model, Error} from 'mongoose';
import {Event} from '. /events.schema '
@Injectable ()
export class EventsService {
Constructor (
@InjectModel (event.name)
<Event>private eventModel: Model,
) {}
<Event>async create (name: string): Promise {
const createDEvent = new this.eventModel ({name: name});
return createdEvent.save ();
}
<Event>async readOne (id: string): Promise {
return this.eventModel.findById (id) .orFail () .exe ();
}
<Event [] >async readAll (): Promise {
return this.EventModel.find () .exe ();
}
<Event>async update (id: string, name: string): Promise {
//do a null check for the name
//https://github.com/Automattic/mongoose/issues/6161#issuecomment-368242099
if (name == null) {
const validationError = new error.validationError (null);
validationError.addError ('docField', new error.ValidatorError ({message: 'Empty name. '})));
throw ValidationError;
}
return this.eventModel.findByIdAndUpdate (
id, {name: name}, {new: true}
) .orFail () .exe ();
}
<Event>async delete (id: string): Promise {
return this.eventModel.findByIdAndDelete (id) .orFail () .exe ();
}
}

Let's run the E2E test again. You should now be able to confirm that all E2E tests have passed successfully.

#1. Start MongoDB with Docker Compose
CD devenv
docker-compose up -d
#2. Run E2E tests
npm run test: e2e
#3. Destroying MongoDB with Docker Compose
CD devenv
Docker compose down

E2E テストの実行結果


E2E Test Execution Results (Success)

However, in the current situation, the flow of manually starting and closing MongoDB with Docker Compose is sandwiched between them, so it is a situation where several manual tasks are required to run the E2E test.

As a matter of fact, running E2E tests is troublesome, and since it cannot be run on CI, the NestJS application itself will also be put on Docker Compose. This creates an environment where E2E tests can be executed by automatically starting MongoDB and NestJS applications simultaneously.

Make it possible to run E2E tests with Docker Compose

First, we'll write a Dockerfile in order to put the NestJS application on Docker Compose. Also, since I don't want to include unnecessary files in the Docker image, I'll also write.dockerignore.

Dockerfile

# Use the Docker image you use with Azure Functions
FROM mcr.Microsoft. /azure-functions/node:2.0-node10
# Instructions for including node_modules in the Docker image build cache
WORKDIR /azure-sample
COPY package*.json /azure-sample/
RUN npm install
# Add NestJS application code to Docker image
ADD.// /azure-sample
# Run E2E tests by default when starting the Docker container
CMD npm run test: e2e

.dockerignore

# Ref: https://www.toptal.com/developers/gitignore?templates=node
# Logs
Logs
*.log
# Runtime data
Pids
*.pid
*.seed
# Directory for instrumented libs generated by jsCoverage/jscover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
README.md
.env*
local.settings.json
coverage
dist

Next, we'll modify devenv/docker-compose.yml to define a NestJS application.

devenv/docker-compose.yml

version: '3.8'
services:
# Add NestJS application definition
App:
build:../.
env_file:
-.. /.env.test
links:
- Mongodb
Mongodb:
image:mongo:latest
ports:
- "27017:27017”
environment:
MONGO_INITDB_ROOT_USERNAME: azure-sample
MONGO_INITDB_ROOT_PASSWORD: azure-sample
MONGO_INITDB_DATABASE: azure-sample
TZ:Asia/Tokyo

Also, since I want to run E2E tests with Docker Compose from now on,.env.test will be modified on the assumption that it will run with Docker Compose.

.env.test

# Change the localhost part to mongodb defined by docker-compose.yml
# Note that E2E tests can only be executed on Docker Compose
mongoDB_uri=MongoDB: //azure-sample: azure-sample @mongodb: 27017/azure-sample? authSource=admin

If you start Docker Compose in this state, the E2E test of the NestJS application should run after starting MongoDB.

Now let's start Docker Compose as a test.

The reason we added the --abort-on-container-exit option at startup is because we want to drop Docker Compose when testing the NestJS application is finished.

# Once the E2E test is completed in the NestJS application and the container is finished, I want to stop Docker, including the Mongo DB container, so specify the --abort-on-container-exit option
# Otherwise, the docker-compose command won't end unless you explicitly stop it (I want the command to end as soon as the test is over)
CD devenv
docker-compose up --abort-on-container-exit

If the following standard output is confirmed during execution, it's OK.

Docker Compose での E2E テストの実行結果


Results of running E2E tests with Docker Compose (successful)

Run E2E tests with GitHub Actions

Since source code management was performed on GitHub in the project that adopted the content of this article, GitHub Actions was adopted as the CI environment.

Therefore, this time we will be able to run Docker Compose E2E tests with GitHub Actions.

.gitub/workflows/build-and-test.yml

# Run a pull request every time it's updated
on:
pull_request:
types: [opened, synchronize]
jobs:
Build-and-test:
Name: Build and Test
runs-on: ubuntu-latest
Strategy:
matrices:
node: ['10.22.1']
steps:
# Use the source code of the latest commit of the branch that submitted the pull request
- Name: checkout commit
uses: actions/checkout @v2
with:
ref: $ {{gitub.event.pull_request.head.sha}}
- name: Use Node.js $ {{matrix.node}}
uses: actions/setup-node @v1
with:
node-version: $ {{matrix.node}}
# Verify that the Node.js build passes
- Name: npm install, build.
run: |
npm install
npm run build --if-present
# Run E2E tests with Docker Compose
- Name: run test on docker-compose
run: |
docker-compose build
docker-compose up --abort-on-container-exit
working-directory:. /devenv

After creating the above file, if you cut the branch appropriately, commit it, and then push it to the remote repository to submit a PR, you should be able to confirm that GitHub Actions is working as shown below.

GitHub Actions で E2E テストが動いているか確認する


Check if the E2E test is working on GitHub Actions

Create an Azure Funtions deployment destination in the Azure portal

We will proceed on the assumption that an Azure account has already been created. First, the Azure portal'sTop pageGo to the Azure Functions (function app) page, and then perform the following tasks.

  1. New function app creation page Create a function app from
1. 関数アプリの新規作成ページから関数アプリを作成する
  1. Confirm that the function app has been successfully created and various resources have been deployed
2. 関数アプリの作成が成功して各種リソースがデプロイされたことを確認する

Set up an Azure Functions environment

After creating an Azure Cosmos DB account, use MONGODB_URI as a secret Azure KeyVault Set it to. The value set in Azure KeyVault can be used by setting it as an environment variable in Azure Functinos (function app).

Create an Azure Cosmos DB account

  1. Azure Cosmos DB account creation pageCreate an account from Select MongoDB~ for the API.
1. Azure Cosmos DB アカウントの作成ページから Azure Cosmos DB アカウントを作成する。API には `MongoDB~` を選択する
  1. Retrieve the primary connection string (MongoDB URI) for the Azure Cosmos DB account you created and make a note of it
2. Azure Cosmos DB の MongoDB URI を取得して控えておく

Register your primary connection string as a secret with Azure KeyVault

The primary connection string for Azure Cosmos DB is secure information and is managed in Azure KeyVault (key container).

  1. Key container creation pageCreate a key container from
1. キーコンテナーの作成ページからキーコンテナーを作成する
  1. Go to the secret registration screen for the key container created in
2. プライマリ文字列を登録するため、1. で作成したキーコンテナーのシークレット登録画面に遷移する
  1. Register the Azure Cosmos DB primary string (MongoDB URI) that you have kept as a secret
3. 控えておいた Azure Cosmos DB のプライマリ文字列 (MongoDB URI) をシークレットに登録する
  1. Note the registered secret identifier in order to set the value in the function application later
4. 後に関数アプリで値を設定するため、登録したシークレットの識別子を控えておく

Make Azure KeyVault visible from Azure Functions

In Azure Functions (function app), it is possible to set the value of Azure KeyVault as an environment variable.Official page instructionsWe will proceed with the setup process in accordance with

  1. Turn on system-assigned states for function apps
1. 関数アプリのシステム割り当て済みの状態をオンにする
  1. Key container list page Move to the access policy addition screen for the corresponding key container from
2. キーコンテナーの一覧ページから該当するキーコンテナーのアクセスポリシー追加画面に遷移する
  1. Set an access policy for the appropriate function app
3. 該当する関数アプリにアクセスポリシーを設定する

There are application settings approved when setting the access policy, but don't set anything here. This is because once set, KeyVault application settings cannot be read from the function app.

View the values registered in Azure KeyVault from Azure Functions

  1. Move to the application setting addition screen for the corresponding function application.
    <KeyVault で値を設定した時に取得可能なシークレット識別子>When adding key container secrets from application settings, the value format is @Microsoft .keyVault (secReturi=).
1. 関数アプリのアプリケーション設定追加画面に遷移する
  1. Add a key container secret to the application settings for the corresponding function app
2. 関数アプリのアプリケーション設定に KeyVault のシークレットを追加する
  1. Apply changes to the application settings of the corresponding function app to the function app
3. 該当する関数アプリのアプリケーション設定の変更内容を関数アプリに反映させる

Deploy to Azure Functions with GitHub Actions

In order to be able to deploy to Azure Functions with GitHub Actions, download the publishing profile for the function app required when deploying to Azure Functions from GitHub Actions below.

関数アプリの発行プロファイルを取得する


Download publishing profiles for function apps

Once you have obtained the publication profile, copy and paste the contents of the downloaded file to GitHub Secrets.

2. GitHub リポジトリの Secrets の登録画面に遷移する


2. Go to the GitHub Repository Secrets registration screen

3. GitHub リポジトリの Secrets に発行プロファイルを登録する


3. Register a publishing profile in the GitHub repository Secrets

Once the above is complete, create a GitHub Actions workflow file.GitHub/workflows/deploy-azure.yml to deploy to Azure Functions and push it to the main branch.

.gitub/workflows/deploy-azure.yml

# Run when some commit is added to the main branch
on:
push:
Branches:
- main
env:
AZURE_FUNCTIONAPP_NAME: azure-nestjs-sample
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'
NODE_VERSION: '10.x'
jobs:
Build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout @master
- name: Setup Node $ {{env.NODE_VERSION}} Environment
uses: actions/setup-node @v1
with:
node-version: $ {{env.NODE_VERSION}}
- name: 'Resolve Project Dependencies Using Npm'
shell: bash
run: |
pushd './$ {{env.azure_functionApp_package_path}} '
npm install
npm run build --if-present
Popd
- name: 'Run Azure Functions Action'
uses: azure/functions-action @v1
id:fa
with:
app-name: $ {{env.azure_functionApp_name}}
package: $ {{env.azure_functionApp_package_path}}
publish-profile: $ {{secrets.azure_functionApp_publish_profile}}
# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples

After pushing to the main branch, check to see if GitHub Actions is actually working from the Actions tab.

main ブランチを更新した後、GitHub Actions を確認する


After the main branch has been updated, check GitHub Actions

If WEBSITE_RUN_FROM_PACKAGE is set in the application settings of the function app, deployment may fail. If that fails, remove the WEBSITE _RUN_FROM_PACKAGE from the application settings and try GitHub Actions again.

I'll check with curl to see if the API I deployed at the end seems to work properly. By default HTTP Trigger AuthLevelSince it is anonymized, you can access it by directly typing the URL.

PlayFab CloudScript When running Azure Functions from, function is recommended as the AuthLevel. Also, when calling Azure Functions from CloudScript, an HTTP request is always made using the POST method.

# I'll check if I can register data with POST events
curl -X POST -H “Content-Type: application/json” -d '{"name”: “test-event"}' https://azure-nestjs-sample.azurewebsites.net/api/events
{"_id” :"5f963d6d2867990052b9bac8", "name” :"test-event”, "__v” :0}

Azure Cosmos DB にデータが正常に登録されていることを確認する


Verify that the data has been successfully registered to Azure Cosmos DB

It's OK if you can confirm that the data has been successfully registered.

concluding

This time, I've summarized the development procedure for Azure Functions using NestJS. There are others Open API It's also compatible, and I'd like to add those articles at some point.

It was my first time to touch Azure-related services, but if I proceeded while referring to the documentation, I was able to build an environment without any particular snags.

I would be very grateful if you could point out any errors or improvements regarding the content of the article.

Thank you for reading to the end.

Reference links

RELATED PROJECT

No items found.