Making DynamoDB Access Easy in NodeJS with ORM

Mohammad Faisal - Dec 3 '23 - - Dev Community

To read more articles like this, visit my blog

Currently, we are using aws-sdk to interact with the DynamoDB.

As DynamoDB is a NoSQL key-value storage, it presents many issues when working in a team. There is a lot of scope for errors in the syntax of how we interact with the database.

We needed a consistent way of doing things. And today, we will see how we did it with an awesome library named. Dynamoose

Problem with Current Approach

Even after working with dynamodb for quite some time now, I find it hard to get the hang of it. Especially some special properties we have to use to update a record.

The ExpressionAttributeNames and ExpressionAttributeValues are another pain to understand and work with.

And guess what? They are not even type-safe!

Let’s take the following example. This code is taken from an actual project where we update a model named member. The details are not important. Just look at the syntax!

const params = {
      TableName: this.tableName,
      Key: {
          Customer: team.Customer,
          Id: team.Id
      },
      UpdateExpression:
                'set \
                        #n= :name,\
                        Description = :description,\
                        Active= :active,\
                        UpdateDate= :updateDate,\
                        UpdatedBy= :updatedBy',
      ExpressionAttributeValues: {
                ':active': team.Active ?? false,
                ':name': team.Name,
                ':description': team.Description,
                ':updateDate': Date.now(),
                ':updatedBy': updatedBy
      },
      ExpressionAttributeNames: { '#i': 'Id', '#n': 'Name' },
      ConditionExpression: 'attribute_exists(Customer) AND attribute_exists(#i)',
      ReturnValues: 'ALL_NEW'
};
Enter fullscreen mode Exit fullscreen mode

There are several problems with this code.

  • The UpdateExpression and ConditionExpression is just a plain string. So if we miss a single character, we are writing a wrong DB query, which will be a nightmare to find out.

  • On top of that, we don’t have any kind of validation for the models we are pushing into the database.

  • Also, we don’t have any autocomplete

  • The API is hard to understand

What’s the solution?

There are multiple libraries to solve this exact problem (Because we are not the first ones to feel this pain), and after some digging, I think the best library is dynamoose .

Why is Dynamoose a great choice?

  • Typescript support

  • Validation support

  • Automatic object transformation

  • Very intuitive API (Using familiar words like get, put etc)

This library follows the API style of a hugely popular mongoose library(for MongoDB). Maybe you already guessed it from the name :P

Let’s re-write our queries to find out how dynamose to improve our code's quality.

Step -1: First, install the dependency

Let’s install the dependency first

npm i dynamoose
Enter fullscreen mode Exit fullscreen mode

Step -2: Create a Model Class

Let’s create a model for our data that will live in the dynamoDB. This is optional. But doing this will produce typescript errors that will keep you safe from unwanted typos.

import { Document } from 'dynamoose/dist/Document';

export class ExampleModel extends Document {
    Id = '';
    Module = '';
    Description = '';
}
Enter fullscreen mode Exit fullscreen mode

Pretty simple right?

Step- 3: Create the schema

Schema means the database specification of the model.

Here we can specify various properties for individual fields. They will later be used for validation and ensure that no dirty data will go into the database.

Some of them are…

required → if the property is required or not

default → default database value for the property

validate → Validation of the field

getter and setter → How do we want to get back the value of a field

timestamp → Automatic CreatedAt and UpdatedAt value for the DB model (which we use regularly)

So in our case the ExampleSchema will be

import * as dynamoose from 'dynamoose';

export const ExampleSchema = new dynamoose.Schema(
    {
        Id: {
            type: String,
            hashKey: true,
            required: true
        },
        Module: {
            type: String,
            rangeKey: true,
            required: true
        },
        Description: {
            type: String,
            required: false,
            default: ""
        }
    },
    {
        timestamps: {
            createdAt: 'CreateDate',
            updatedAt: 'UpdateDate' 
        }
    }
);
Enter fullscreen mode Exit fullscreen mode

Notice at the bottom of the schema, we have specified the timestamps property. This will automatically generate timestamp for us.

Step 4: Create a Repository

The library follows the MongoDB naming so it identifies its repository as a model. We will use the model but in a smarter way.

We will create a repository to separate all the database logic. Below is an example of how to create functions for GET, CREATE, and UPDATE operations

import * as dynamoose from 'dynamoose';
import { ExampleModel } from './ExampleModel';
import { getTableName } from '@amagroup.io/amag-corelib';
import { Model } from 'dynamoose/dist/Model';
import { ExampleSchema } from './ExampleSchema';
import { CreateExampleRequest } from './create-survey/CreateSurveyRequest';
import { UpdateExampleRequest } from './update-survey/UpdateSurveyRequest';

export default class ExampleRepository {

    private dbInstance: Model<ExampleModel>;

    constructor(environment: string) {
        const tableName = getTableName(environment, 'Example');
        this.dbInstance = dynamoose.model<ExampleModel>(tableName, ExampleSchema);
    }

    createExample = async (request: CreateExampleRequest) => {
        return await this.dbInstance.create({
            Id: request.Id,
            Module: request.Module
        });
    };

    updateExample = async (request: UpdateExampleRequest) => {
        return await this.dbInstance.update({
            Id: request.Id,
            Module: request.Module,
            Description: request.Description
        });
    };

    getExampleById = async (id: string, moduleName: string) => {
        return await this.dbInstance.get({ Id: id, Module: moduleName });
    };
}
Enter fullscreen mode Exit fullscreen mode

So all of our database logic is encapsulated now. and also look at the updateExample function. Which is now just like any other function.

You just provide the primary key and sort key along with the fields that you want to get updated. And you are done!

And if you try to pass any unknown key that is not defined in your model, you will get a typescript error on compile time!

Final Step: Use it inside your code

Now we will use this repository to interact with the database

const repository = new ExampleRepository('dev');

const response = await repository.createExample(request);

Enter fullscreen mode Exit fullscreen mode

So now we have a CRUD application where we can take advantage of the dynamoose to eliminate the database query pain.

That’s it for today. Have a great day! :D

Get in touch with me via LinkedIn or my Personal Website.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .