DynamoDB is a NoSQL database, meaning that it does not have a pre-defined structure. With DynamoDB, you can store any data you want without worrying about the data model, making it a powerful tool to use when working with unstructured data.
But NoSQL comes at a cost. Not defining a structure means you must consistently maintain the structure and data types, or risk your application crashing.
Developers have come up with ways to prevent these issues. In this article, we will explore why you should use ORM, or object-relational mapping, with DynamoDB to manage the models of your database. We will also see why Dynamoose is the best ORM tool to use for our purposes. Finally, we’ll integrate Dynamoose with a NestJS application and build a complete CRUD app that you can use as a reference.
To jump ahead in this article:
Let’s get started!
The most common way to interact with DynamoDB from a JavaScript application is by using the DynamoDB client of aws-sdk
. But this comes with several issues.
The aws-sdk
client does not offer a way to manage your application models. This becomes a hassle if your application requires consistency.
This can be a debatable topic because one of the significant benefits of using DynamoDB is the ability to store anything in the database.
But, in reality, we don’t often require that. For example, we don’t put email into the age field, right? So what do we do?
if(name.length > 0 && isEmail(email)){ // store the value }
We have to do the validation ourselves, and for every field, which becomes difficult to maintain.
The syntax for accessing DynamoDB is strange and often depends on string queries, which is inconvenient. Below is an example from the official documentation that queries a table containing information about an episode from a video series.
var params = { ExpressionAttributeValues: { ':s': {N: '2'}, ':e' : {N: '09'}, ':topic' : {S: 'PHRASE'} }, KeyConditionExpression: 'Season = :s and Episode > :e', ProjectionExpression: 'Episode, Title, Subtitle', FilterExpression: 'contains (Subtitle, :topic)', TableName: 'EPISODES_TABLE' }; ddb.query(params, function(err, data) { // handle the error and the other things here });
This can take a long time to understand. And when there are multiple fields to reason with, it becomes a nightmare!
These issues with DynamoDB give developers (including myself) a tough time reasoning with the queries. And because the syntax is unfamiliar, it requires a steep learning curve, which is not desirable for fast-paced projects.
We can solve the previously mentioned issues with ORM, or object-relational mapping. ORM tools make it easy to manage the models of your database, and make accessing the database easier by using simpler syntax.
There are many benefits to using ORM:
There are many ORM tools to choose for DynamoDB, including dynamodb-data-types, dyngodb, and dynasaur.
But none of them are nearly as popular as Dynamoose, which:
Today, we will see how we can use Dynamoose with a NestJS application.
The first step to creating our NestJS project is to install the NestJS CLI.
npm i -g @nestjs/cli
Next, initialize the boilerplate project.
nest new nestjs-dynamoose-integration
Then, install the required dependency.
yarn add dynamoose
And we are ready to start!
In order for Dynamoose to talk to your DynamoDB tables on your AWS account, it needs the proper permission. You can find the different ways to configure that here.
For our purposes, we will set the credentials using the access_key
and the secret_key
inside the application.
Open the main.ts
file, and add the following configuration code:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as dynamoose from 'dynamoose'; async function bootstrap() { // do this before the app is created. dynamoose.aws.sdk.config.update({ accessKeyId: 'YOUR_ACCESS_KEY', secretAccessKey: 'YOUR_SECRET_ACCESS_KEY', region: 'YOUR_PREFERRED_REGION', }); const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
If you are unsure how to get these keys, you can refer to the documentation. Ideally, you should load them using the environment variables, and don’t use them as plain text inside the project.
The nice thing about NestJS is that it automatically creates many boilerplates for you. Today we are going to build a CRUD endpoint using Dynamoose. Let’s create the boilerplate codes for it.
Run the following command on your command line.
nest g resource user
After running the command, you will see that all the necessary codes are auto-generated.
How cool is that?
The next step is to create an entity
class that extends the Document
class provided by Dynamoose, which will define the structure of our entity.
Open up the user.entity.ts
file and paste the following code.
import { Document } from 'dynamoose/dist/Document'; export class User extends Document { Id = 0; Name = ''; }
This structure serves two purposes:
This step is optional, but I would highly recommend it!
Following the footprint of the Mongoose library, Dynamoose also defines the database models using the concept of a schema. A schema is the definition of a data model our DynamoDB table understands.
As discussed earlier, DynamoDB does not restrict the data that we can upload to the database tables. But we can define the desired data shape using the schema, and Dynamoose will make sure that our data conforms to that structure.
Some other benefits include:
required
, which will make sure that field will never be emptytype
of a propertycustom validation
function to validate a field’s valuetimestamps
automaticallyLet’s create a new user.schema.ts
file in the entities directory and paste the following code there.
import * as dynamoose from 'dynamoose'; export const UserSchema = new dynamoose.Schema( { Id: { type: Number, hashKey: true, required: true, }, Name: { type: String, required: true, index: { name: nameIndex, global: true } }, }, { timestamps: true, }, );
Let’s understand what’s going on here:
Id
field is specified as a Number
, so we can’t pass just any key hereName
field is required
, so if it is empty, it will cause an errorName
is String
, so no other data type can go theretimestamps
property, which will automatically generate and maintain the createdAt
and updatedAt
fields for usName
fieldThese things occurred without us writing unnecessary validation functions.
Let’s open up the user.service.ts
file where we will create all the required CRUD operations.
First, ensure we have a local dbInstance
and initialize it with user information.
Keep in mind that, ideally, we want to create a separate user.repository.ts
file to keep our database logic. We usually use service classes for business logic.
For simplicity, we are now putting the DB operations inside the service class.
@Injectable() export class UserService { private dbInstance: Model<User>; constructor() { const tableName = 'users'; this.dbInstance = dynamoose.model<User>(tableName, UserSchema); } //... crud functions }
Here, we are adding a private dbInstance
with the User
model. Inside the constructor, we specify which table we want to use and provide the UserSchema
here.
Now, our dbInstance
knows which DynamoDB table to access, and the expected data types.
To create a user
, we can write the following function.
async create (createUserDto: CreateUserDto) { return await this.dbInstance.create({ Id: createUserDto.Id, Name: createUserDto.Name, }); }
Notice that we are using the CreateUserDto
request object, which NestJS already defined for us.
It will look something like this:
export class CreateUserDto { Id: number; Name: string; }
To get a single user, we can query the database using the primary key like this:
async findOne(id: number) { return await this.dbInstance.get({ Id: id }); }
You can also query using many things, like a partial match, or less than, or greater than operators. You can find more details in the documentation.
To update the user
, we get an update
function provided by Dynamoose.
async update(id: number, updateUserDto: UpdateUserDto) { return await this.dbInstance.update({ Id: id, Name: updateUserDto.Name, }); }
To delete a user by ID, input this code:
async remove(id: number) { return await this.dbInstance.delete({ Id: id }); }
The final version should look like this:
import * as dynamoose from 'dynamoose'; import { Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; import { Model } from 'dynamoose/dist/Model'; import { UserSchema } from './entities/user.schema'; @Injectable() export class UserService { private dbInstance: Model<User>; constructor() { const tableName = 'users'; this.dbInstance = dynamoose.model<User>(tableName, UserSchema); } async create(createUserDto: CreateUserDto) { return await this.dbInstance.create({ Id: createUserDto.Id, Name: createUserDto.Name, }); } async findOne(id: number) { return await this.dbInstance.get({ Id: id }); } async update(id: number, updateUserDto: UpdateUserDto) { return await this.dbInstance.update({ Id: id, Name: updateUserDto.Name, }); } async remove(id: number) { return await this.dbInstance.delete({ Id: id }); } }
Let’s head over to Postman and create a user using the following body.
endpoint: http://localhost:3000/user method: POST body: { "Id": 1, "Name" : "Faisal" }
If you try to get the particular user like this…
endpoint: http://localhost:3000/user/1 method: GET
… it will give you the following response:
{ "Id": 1, "Name": "Faisal", "createdAt": 1660504687981, "updatedAt": 1660504687981 }
You can see there are two extra fields: createdAt
and updatedAt
, which are generated automatically.
Similarly, you can also play with the update
and delete
methods.
Now you have a fully working CRUD application capable of talking to the actual DynamoDB using Dynamoose.
In this article, you learned how to use Dynamoose with a NestJS application. Check out the GitHub repository for this project here. An alternative package called nestjs-dynamoose adds some syntactic sugar around the usage.
I hope you learned something new today. Have a great day!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.