# 5. Extras
At this point, you already have a working full-featured serverless API, well done! 🎉
NestJS is a very comprehensive framework, and there could be a lot more use-cases to cover for your specific needs. I encourage you to dive into the NestJS documentation to learn more about the techniques and tools you can use.
If you have more time and feel like it, here are some extras points that I found interesting to cover, especially if you want to build enterprise apps.
Note that each of these extra parts is entirely independent, so you can skip to the one you are the most interested in or do them in any order 😉.
# Add data validation
It is a best practice to check and validate any data received by an API. What do you think would happen if you call your story creation endpoint, but without providing data?
Let's try!
curl http://localhost:7071/api/stories -X POST -d ""
Whoops! A new story is created, but with our entity properties are left empty 😱.
We might want to make sure a new story has its animal
field set and either a description
or an image provided.
Nest.js provides a built-in ValidationPipe
that enforces validation rules for received data payloads, thanks to annotations provided by the class-validator
package.
To use it, you have to create a DTO (Data Transfer Object) class on which you will declare the validations rules using annotations.
First, you need to install the required packages:
npm install class-validator class-transformer
Then create the file src/stories/story.dto.ts
:
export class StoryDto {
@IsNotEmpty()
animal: string;
@IsOptional()
description: string;
@IsOptional()
createdAt: Date;
}
It looks like a lot like our Story
entity, but this time you define only properties that are expected in the request payload. That's why there is no imageUrl
property here: it will be set by the controller only if an image file is uploaded.
The annotations @IsNotEmpty()
and @IsOptional()
describe which property can be omitted and which one can be set in the payload. You can see the complete list of provided decorators here.
Now open src/stories/stories.controller.ts
and change the type of the data
parameter of your POST
function to StoryDto
:
...
async createStory(
@Body()
data: StoryDto,
@UploadedFile()
file: UploadedFileMetadata,
): Promise<Story> {
...
Finally open src/main.azure.ts
and enable ValidationPipe
at the application level, to ensure all endpoints gets data validation:
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.useGlobalPipes(new ValidationPipe());
Start your server with npm run start:azure
and run the previous curl
command again. This time you should properly receive an HTTP error 400
(bad request).
Pro tip
By default, detailed error messages will be automatically generated in case of a validation error. You also specify custom error message in the decorator options, for example:
@IsNotEmpty({ message: 'animal must not be empty' })
animal: string;
You also use special tokens in your error message or use a function for better granularity. See the class-validator
documentation for more details.
What about our other constraint, which is to have either a description
or an image file provided?
Since the imageUrl
information is not directly part of the DTO, we cannot use it for validation. As the imageUrl
property is set in the controller, that's where you have to perform manual validation. You can use the manual validation methods of the class-validator
package for that.
This time, it's your turn to finish the job!
- Ensure that either
description
orimageUrl
is not empty, using manual validation. - Ensure that
description
length is at most 240 characters. - Ensure that
animal
is either set tocat
,doc
orhamster
using annotations. - Ensure that
createdAt
is a date if provided, using annotations.
You can read more on data validation techniques in the NestJS documentation.
# Enable CORS
If you try to access your API inside a web application from your browser, you might encounter an error like that one:
This error occurs because browsers block HTTP requests from scripts to web domains different than the one of the current web page to improve security.
To bypass this restriction, your server must define specific HTTP headers to allow it. This mechanism is called Cross-Origin Resource Sharing (CORS).
CORS is already enabled by default on Azure Functions but you must add your website domain to the list of allowed origins using this command:
# Don't forget to change the name and URL with your own
$ az functionapp cors add \
--name funpets-api \
--resource-group funpets \
--allowed-origins https://yourwebsite.com
If you want to allow any website to use your API, you can replace the website URL by using *
instead. In that case, be careful as Azure Functions will auto-scale to handle the workload if millions of users start using it, but so will your bill!
# Enable authorization
By default, all Azure Functions triggered by HTTP are publicly available. It's useful for a lot of scenarios, but at some point you might want to restrict who can execute your functions, in our case your API.
Open the file main/function.json
. In the functions, bindings, notice that authLevel
is set to anonymous
. It can be set to one of these 3 values:
anonymous
: no API key is required (default).function
: an API key specific to this function is required. If none is defined, thedefault
one will be used.admin
: a host API key is required. It will be shared among all functions from the same app.
Now change authLevel
to function
, and redeploy your function:
# Don't forget to change the name with the one you used previously
func azure functionapp publish <your-funpets-api> --nozip
Then try to invoke again your API:
curl https://<your-funpets-api>.azurewebsites.net/api/stories -i
You should get an HTTP status 401
error (Unauthorized
).
To call a protected function, you need to either provide the key as a query string parameter in the form code=<api_key>
or you can provide it with the HTTP header x-functions-key
.
You can either log in to portal.azure.com and go to your function app, or follow these steps to retrieve your function API keys:
// Retrieve your resource ID
# Don't forget to change the name with the one you used previously
az functionapp show --name <your-funpets-api> \
--resource-group funpets \
--query id
# Use the resource ID from the previous command
az rest --method post --uri "<resource_id>/host/default/listKeys?api-version=2018-11-01"
You should see something like that:
{
"functionKeys": {
"default": "functionApiKey=="
},
"masterKey": "masterApiKey==",
"systemKeys": {}
}
Then try to invoke again your API, this time with the x-functions-key
header set with your function API key:
curl https://<your-funpets-api>.azurewebsites.net/api/stories -i \
-H "x-functions-key: <your_function_api_key>"
This time the call should succeed!
Using authorization level you can restrict who can call your API, this can be useful especially for service-to-service access restrictions.
However, if you need to manage finely who can access your API with an endpoint granularity, you need to implement authentication in your app.
# Write tests
Your API might currently look fine, but how can you ensure it has as little bugs as possible, and that you won't introduce regression in the future?
Writing automated is not the most fun part of development, but it's a fundamental requirement to develop robust software applications. It helps to catch bugs early, preventing regressions and ensuring that production releases meet your quality and performance goals.
The good news is NestJS has you covered to make your testing experience as smooth as possible.
When you bootstrapped the project using the nest
CLI, Jest and SuperTest frameworks have been set up for you.
Each time you run the nest generate
command, unit test files are also created for you with the extension .spec.ts
.
There are 5 NPM scripts dedicated to testing in your package.json
file:
npm test
: runs unit tests once.npm run test:watch
runs unit tests in watch mode, it will automatically re-run tests as you make modifications to the files. It is suited perfectly for TDD.npm run test:cov
runs unit tests and generate coverage report, so you can know which code paths are covered by your tests.npm run test:debug
: runs unit tests with Node.js debugger enabled, so you can add breakpoints in your code editor and debug your tests more easily.npm run test:e2e
: runs your end-to-end tests.
Now run the npm test
command. Oops, it seems that src/stories/stories.controller.spec.ts
test if failing 😱!
# Add module and providers mocks
If you look at the stack trace, you can see that the reason is that @nestjs/typeorm
and AzureStorageModule
services cannot be resolved. It's expected: when running unit tests, you want to isolate the code you are testing as much as possible, and for that you can see that each test file provides its own module definition:
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [StoriesController],
}).compile();
controller = module.get<StoriesController>(StoriesController);
});
The module created with Test.createTestingModule
does not import AzureTableStorageModule
and AzureStorageModule
, so that's why their providers cannot be resolved. Instead of importing them right away to fix the issue, we should write mocks for the providers we use instead.
# Mock @nestjs/azure-storage
Let's start with mocking what we use in @nestjs/azure-storage
module, using jest.mock(<module>)
helper function. Add this code just after the imports:
jest.mock('@nestjs/azure-storage', () => ({
// Use Jest automatic mock generation
...jest.genMockFromModule('@nestjs/azure-storage'),
// Mock interceptor
AzureStorageFileInterceptor: () => ({
intercept: jest.fn((context, next) => next.handle())
})
}));
For simple modules, using jest.mock(<module>)
would be enough to generate mocks automatically according to the module interface.
But in our case, AzureStorageFileInterceptor
needs to be mocked manually as it is a bit trickier: it must returns an object with a method intercept(context, next)
that needs to call next.handle()
to not break the chain of interceptors calls.
So we provide our own version of the @nestjs/azure-storage
module mock, using jest.genMockFromModule(<module>)
helper to automatically generates mocks for everything except AzureStorageFileInterceptor
.
For AzureStorageFileInterceptor
we manually reproduce a minimal implementation. Using jest.fn()
method here creates a mock function. Thanks to that, we can later change its implementation in a specific test if needed.
Then add AzureStorageService
to the testting module providers
list:
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [StoriesController],
providers: [AzureStorageService]
}).compile();
controller = module.get<StoriesController>(StoriesController);
});
And complete the missing import:
import { AzureStorageService } from '@nestjs/azure-storage';
# Mock @nestjs/typeorm
We also need to mock the storiesRepository
service injected in our controller using @InjectRepository(Story)
, but how to do that?
This time we do not need to mock the entire module, but only this specific service. We can still use Jest automatic mock generation:
// Add this code after the imports
const mockRepository = jest.genMockFromModule<any>('typeorm').MongoRepository;
Its injection token is generated dynamically, so we need to add a custom provider to our testing module to reproduce the same behavior:
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [StoriesController],
providers: [
AzureStorageService,
{ provide: getRepositoryToken(Story), useValue: mockRepository },
],
}).compile();
controller = module.get<StoriesController>(StoriesController);
});
Pro tip
We had to look at the implementation @InjectRepository()
annotation to find out that it uses the method getRepositoryToken()
internally. Unfortunately, that's something you have to do sometimes to be able to mock modules properly.
Don't forget to add missing imports:
import { getRepositoryToken } from '@nestjs/typeorm';
import { Story } from './story.entity';
Now run npm test
again, this time the tests should succeed!
# Complete test suite
Hold on, now that we have solved the mock issue, it's time to write more tests 😃!
Try to add:
- Unit tests for your controller in
src/stories/stories.controller.ts
. - An end-to-end for of your endpoints in
tests/app.e2e-spec.ts
.
Take also a look a the report generated by npm run test:cov
to see your test coverage.
If you are not familiar with Jest you might want to take a look at the documentation. For end-to-end tests, HTTP assertions are made using the SuperTest library.
You can also find examples and more information in the NestJS documentation.
Solution: see the code for extras