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 Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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>

Build an AI assistant with Vercel AI Elements, which provides pre-built React components specifically designed for AI applications.

line-clamp to trim lines of textMaster the CSS line-clamp property. Learn how to truncate text lines, ensure cross-browser compatibility, and avoid hidden UX pitfalls when designing modern web layouts.

Discover seven custom React Hooks that will simplify your web development process and make you a faster, better, more efficient developer.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.
Would you be interested in joining LogRocket's developer community?
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 now