Originally published on the Trilon Blog on September 17, 2019.
In this article, we'll be looking at how to add Azure Table Storage to our NestJS applications in only a few minutes with the new @nestjs/azure-database
library!
In case you're not familiar with NestJS, it is a TypeScript Node.js framework that helps you build enterprise-grade efficient and scalable Node.js applications.
What's Azure Table Storage?
Azure Table Storage is a NoSQL key-value store using massive semi-structured datasets.
Table Storage allows you to create massively-scalable apps that require a flexible data schema. You can also perform OData-based queries and use JSON to serialize data.
Use Azure Table storage to store petabytes of semi-structured data and keep costs down.
Unlike many data stores—on-premises or cloud-based:
- Table storage lets you scale up without having to manually shard your dataset.
- Availability also isn’t a concern!
- Using geo-redundant storage, stored data is replicated three times within a region—and an additional three times in another region, hundreds of miles away.
Let's dive into how we can use Table Storage for our NestJS applications!
Getting setup
NOTE: In this demonstration, we'll be showcasing a new NestJS application generated by the CLI, but if you prefer to use an existing NestJS application - feel free - and just skip ahead!
Generate a new NestJS Application
For demo purposes, let's make sure we have the latest NestJS CLI installed - and create a New application.
$ npm i -g @nestjs/cli
$ nest new PROJECT_NAME
Now let's cd
into the newly created directory and open up our IDE. At this point, we have a simple generated NestJS Application.
Setting up an Azure Storage account
In order to use Table Storage, we will need to create an Azure Storage account. You can follow this step by step guide.
Once our storage account created, we need to copy the Connection String that we will use for our SDK. In the Azure Portal, go to Dashboard > Storage > your-storage-account:
Note down the "Storage account name" and "Connection string" obtained at Access keys under the Settings tab.
TIP: The connection string should start with
DefaultEndpointsProtocol=
NestJS Azure Storage Installation
Next, we need to install the @nestjs/azure-database
SDK from NPM:
$ npm i --save @nestjs/azure-database dotenv
We also install the
dotenv
package that allows us to deal with environment variables.
We will then create a file called .env with the following content:
AZURE_STORAGE_CONNECTION_STRING="<the connection string we copied from previous step>"
Also very important: we will make sure to add our .env
file to the .gitignore
! The .env
file MUST NOT be versioned on Git.
Once the .env
file created and ready, we will include the following call to the src/main.ts
file:
if (process.env.NODE_ENV !== 'production') require('dotenv').config();
TIP: This line must be added before any other imports in the
src/main.ts
file!
Our setup is now ready. Let's implement the logic of our application.
Preparing our business logic
The Azure Table Storage support in NestJS follows the Object-Relational Mapping (ORM) design pattern which is basically a "structured" way of accessing a database from our code - letting you use an API instead of writing actual SQL code.
In order to implement this design pattern we will need to create the following components, for each feature:
-
DTO
(or data transfer object)- This is the object that will represent our data. DTO's are primarily used to transfer data between application services, such as that between an HTTP service and a browser.
-
Entity
- This is basically a class mapped to the table schema.
-
Repository
- This is the component that is responsible for communicating with the database.
Let's first create a NestJS feature module where we will host our feature business logic. We will use the NestJS CLI to create a Cat
feature:
$ nest generate module cat
NOTE: We will come back to our generated module at the end of the process.
DTO
The first component we need to create for our Cat
feature is a DTO. Inside a file named cat.dto.ts
, we create the following class:
export class CatDTO {
name: string;
age: number;
}
Entity
Next, we need an Entity
. To do so, we create a file called cat.entity.ts
and describe the model using the decorators provided by @nestjs/azure-database
:
Entity | Represents | Required |
---|---|---|
@EntityPartitionKey(value: string) |
The PartitionKey of the entity |
Yes |
@EntityRowKey(value: string) |
The RowKey of the entity |
Yes |
@EntityInt32(value?: string) |
Signed 32-bit integer values | |
@EntityInt64(value?: string) |
Signed 64-bit integer values | |
@EntityBinary(value?: string) |
Binary (blob) data | |
@EntityBoolean(value?: string) |
true or false values |
|
@EntityString(value?: string) |
Character data | |
@EntityDouble(value?: string) |
Floating point numbers with 15 digit precision | |
@EntityDateTime(value?: string) |
Time of day |
Note: The API might slightly change in the stable release.
For instance, the shape of the following entity:
import {
EntityPartitionKey,
EntityRowKey,
EntityString,
EntityIn32
} from '@nestjs/azure-database';
@EntityPartitionKey('CatID')
@EntityRowKey('CatName')
export class Cat {
@EntityString() name: string;
@EntityIn32() age: number;
}
The Cat
entity will be automatically converted to the following schema that is expected by the Azure Table Storage:
{
"PartitionKey": { "_": "CatID", "$": "Edm.String" },
"RowKey": { "_": "CatName", "$": "Edm.String" },
"name": { "_": undefined, "$": "Edm.String" },
"age": { "_": undefined, "$": "Edm.Int32" }
}
Repository
After the DTO and the Entity, we need now to create a Cat
service that will abstract all the CRUD operations related to the Cat
entity. This service will use the Azure Table Storage Repository
.
Let's create a service using the NestJS CLI:
$ nest generate service cat
Inside the created cat.service.ts
, we import the Repository
and provide it with the Cat
entity definition created in the previous step:
import { Injectable } from '@nestjs/common';
import { Repository, InjectRepository } from '@nestjs/azure-database';
import { Cat } from './cat.entity';
@Injectable()
export class CatService {
constructor(
@InjectRepository(Cat)
private readonly catRepository: Repository<Cat>,
) {}
// ... other code ...
The Azure Table Storage Repository
interface provides a bunch of public APIs and types for managing various CRUD (Create
, Read
, Update
and Delete
) operations. Let's see how we can implement each different operation using @nestjs/azure-database
SDK.
The methods we will be invoking are the following:
-
create(entity: T): Promise<T>
to create a new entity. -
findAll(tableQuery?: azure.TableQuery, currentToken?: azure.TableService.TableContinuationToken): Promise<AzureTableStorageResultList<T>>
to find all entities that match the given query (return all entities if no query provided). -
find(rowKey: string, entity: Partial<T>): Promise<T>
to find one entity using its RowKey. -
update(rowKey: string, entity: Partial<T>): Promise<T>
to update an entity. This does a partial update. -
delete(rowKey: string, entity: T): Promise<AzureTableStorageResponse>
to remove an entity using its RowKey.
Here is an example of such implementation:
import { Injectable } from '@nestjs/common';
import { Repository, InjectRepository } from '@nestjs/azure-database';
import { Cat } from './cat.entity';
@Injectable()
export class CatService {
constructor(
@InjectRepository(Cat)
private readonly catRepository: Repository<Cat>,
) {}
// find one cat entitu by its rowKey
async find(rowKey: string, cat: Cat): Promise<Cat> {
return this.catRepository.find(rowKey, cat);
}
// find all cat entities
async findAll(): Promise<AzureTableStorageResultList<Cat>> {
return this.catRepository.findAll();
}
// create a new cat entity
async create(cat: Cat): Promise<Cat> {
return this.catRepository.create(cat);
}
// update the a cat entity by its rowKey
async update(rowKey: string, cat: Partial<Cat>): Promise<Cat> {
return this.catRepository.update(rowKey, cat);
}
// delete a cat entity by its rowKey
async delete(rowKey: string, cat: Cat): Promise<AzureTableStorageResponse> {
return this.catRepository.delete(rowKey, cat);
}
}
Controller
The last step is to implement the NestJS controller that will process the HTTP requests. Let's create such a controller using the NestJS CLI:
$ nest generate controller cat
The implementation of the controller is straightforward and would probably depend on your application business needs. Here is an example of an implementation:
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UnprocessableEntityException,
NotFoundException,
Patch
} from '@nestjs/common';
import { CatDto } from './cat.dto';
import { Cat } from './cat.entity';
import { CatService } from './cat.service';
@Controller('cats')
export class CatController {
constructor(private readonly catService: CatService) {}
@Get()
async getAllCats() {
return await this.catService.findAll();
}
@Get(':rowKey')
async getCat(@Param('rowKey') rowKey) {
try {
return await this.catService.find(rowKey, new Cat());
} catch (error) {
// Entity not found
throw new NotFoundException(error);
}
}
@Post()
async createCat(
@Body()
catData: CatDto,
) {
try {
const cat = new Cat();
// Disclaimer: Assign only the properties you are expecting!
Object.assign(cat, catData);
return await this.catService.create(cat);
} catch (error) {
throw new UnprocessableEntityException(error);
}
}
@Put(':rowKey')
async saveCat(@Param('rowKey') rowKey, @Body() catData: CatDto) {
try {
const cat = new Cat();
// Disclaimer: Assign only the properties you are expecting!
Object.assign(cat, catData);
return await this.catService.update(rowKey, cat);
} catch (error) {
throw new UnprocessableEntityException(error);
}
}
@Patch(':rowKey')
async updateCatDetails(@Param('rowKey') rowKey, @Body() catData: Partial<CatDto>) {
try {
const cat = new Cat();
// Disclaimer: Assign only the properties you are expecting!
Object.assign(cat, catData);
return await this.catService.update(rowKey, cat);
} catch (error) {
throw new UnprocessableEntityException(error);
}
}
@Delete(':rowKey')
async deleteDelete(@Param('rowKey') rowKey) {
try {
const response = await this.catService.delete(rowKey, new Cat());
if (response.statusCode === 204) {
return null;
} else {
throw new UnprocessableEntityException(response);
}
} catch (error) {
throw new UnprocessableEntityException(error);
}
}
}
Putting everything together
We have finished the implementation of our Cat
feature. In this last step, we will need to import the AzureTableStorageModule
inside our Nest feature module cat.module.ts
that we created earlier:
import { Module } from '@nestjs/common';
import { AzureTableStorageModule } from '@nestjs/azure-database';
import { CatController } from './cat.controller';
import { CatService } from './cat.service';
import { Cat } from './cat.entity';
@Module({
imports: [AzureTableStorageModule.forFeature(Cat)],
providers: [CatService],
controllers: [CatController],
})
export class CatModule {}
The AzureTableStorageModule
module takes in few optional arguments:
AzureTableStorageModule.forFeature(Cat, {
table: 'AnotherTableName',
createTableIfNotExists: true,
})
-
table: string
: The name of the table. If not provided, the name of the Cat entity will be used as a table name -
createTableIfNotExists: boolean
: Whether to automatically create the table if it doesn't exist or not:- If
true
the table will be created during the startup of the app. - If
false
the table will not be created. You will have to create the table by yourself before querying it!
- If
In Conclusion
We just implemented a new Cat
feature for our application that uses @nestjs/azure-database
official package to add support for Azure Table Storage. With NestJS's modular system, we are able to install it and set it up with our application almost like a native Nest-feature!
If you're interested in learning more about Serverless NestJS apps with Azure read more here.
nestjs / azure-database
Azure CosmosDB Database module for Nest framework (node.js) ☁️
A progressive Node.js framework for building efficient and scalable server-side applications.
Description
Azure Database (Table Storage, Cosmos DB - NoSQL) module for Nest framework (node.js)
Disclaimer
You are reading the documentation for version 3. If you are looking for version 2 documentation, click here. Please also note that version 2 is no longer maintained and will not receive any updates!
Before Installation
For Cosmos DB (NoSQL ONLY)
- Create a Cosmos DB account and resource (read more)
- Note down the "URI", Database name and the "Primary Key" (or "Secondary Key") - You will need them later
For Table Storage
- Create a Storage account and resource (read more)
- Note down the "Connection string" - You will need it later
Installation
$ npm i --save @nestjs/azure-database
Usage
For Azure Cosmos DB support
- Create or update your existing
.env
file with the following content:
AZURE_COSMOS_DB_NAME=
AZURE_COSMOS_DB_ENDPOINT=
…
Top comments (1)
<3