Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated May 6, 2024

Connect to external Amazon DynamoDB data sources

The a.model() data model allows you to define a GraphQL schema for an AWS AppSync API where models are backed by DynamoDB Tables managed by Amplify. The generated schema also provides queries and mutations to the Amplify Data client. However, you may want to connect to an external DynamoDB table and execute custom business logic against it instead.

Using an external DynamoDB table as a data source may be useful if you need to leverage patterns such as single table design.

In the following sections, we walk through the steps to add and use an external DynamoDB table as a data source for your API:

  1. Set up your Amazon DynamoDB table
  2. Add your Amazon DynamoDB table as a data source
  3. Define custom queries and mutations
  4. Configure custom business logic handler code
  5. Invoke custom queries or mutations

Step 1 - Set up your Amazon DynamoDB table

For the purpose of this guide we will define a Post type and create an external DynamoDB table that will store records for it. In Amplify Gen 2, customType adds a type to the schema that is not backed by an Amplify-generated DynamoDB table.

With the Post type defined, it can then be referenced as the return type when defining your custom queries and mutations.

First, add the Post custom type to your schema:

amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Todo: a
.model({
content: a.string(),
})
.authorization(allow => [allow.publicApiKey()]),
Post: a.customType({
id: a.id().required(),
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
ups: a.integer(),
downs: a.integer(),
version: a.integer(),
}),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});

NOTE: To comply with the GraphQL spec, at least one query is required for a schema to be valid. Otherwise, deployments will fail with a schema error. The Amplify Data schema is auto-generated with a Todo model and corresponding queries under the hood. You can leave the Todo model in the schema until you add the first custom query to the schema in the next steps.

Once the deployment successfully completes, navigate to the AppSync console and select your Amplify-generated API. Follow these steps to create a new DynamoDB table:

  1. On the Schema page, choose Create Resources.

AWS AppSync console showing navigation pane with "AWS AppSync" expanded and "APIs" > "TestAPI" selected. Main content displays "Schema Info" section with a "Create Resources" button.

  1. Choose Use existing type, then choose the Post type.

AWS AppSync console, "Create Resources" page. A prominent heading reads "Create Resources". Radio buttons are presented for either defining a new type or selecting an existing type for the table creation.

  1. Set the Primary key to id and the Sort key to None.

  2. Disable Automatically generate GraphQL. In this example, we'll create the resolver ourselves.

AWS AppSync console, "Create a table to hold Post objects" page. A table structure is shown with columns and values of "Table name": "PostTable", "Primary Key": "id", and "Sort key": "None". Below the table, there is an option to "Automatically generate GraphQL" which is disabled.

  1. Choose Create.

You now have a new DynamoDB table named PostTable, which you can see by visiting Data sources in the side tab. You will use this table as the data source for your custom queries and mutations to your Amazon DynamoDB table.

AWS AppSync console, 'Data sources' page. The page shows a list of existing data sources connected to an API. The data sources include an Amazon DynamoDB table named 'PostTable' and another table named 'Todo'.*

Step 2 - Add your Amazon DynamoDB table as a data source

In your amplify/backend.ts file, add your DynamoDB table as a data source for your API:

amplify/backend.ts
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { aws_dynamodb } from "aws-cdk-lib";
export const backend = defineBackend({
auth,
data,
});
const externalDataSourcesStack = backend.createStack("MyExternalDataSources");
const externalTable = aws_dynamodb.Table.fromTableName(
externalDataSourcesStack,
"MyExternalPostTable",
"PostTable"
);
backend.data.addDynamoDbDataSource(
"ExternalPostTableDataSource",
externalTable
);

Step 3 - Define custom queries and mutations

Now that your DynamoDB table has been added as a data source, you can reference it in custom queries and mutations using the a.handler.custom() modifier which accepts the name of the data source and an entry point for your resolvers.

Use the following code examples to add addPost, getPost, updatePost, and deletePost as custom queries and mutations to your schema:

amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Post: a.customType({
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
ups: a.integer(),
downs: a.integer(),
version: a.integer(),
}),
addPost: a
.mutation()
.arguments({
id: a.id(),
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
})
.returns(a.ref("Post"))
.authorization(allow => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./addPost.js",
})
),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});
amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Post: a.customType({
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
ups: a.integer(),
downs: a.integer(),
version: a.integer(),
}),
getPost: a
.query()
.arguments({ id: a.id().required() })
.returns(a.ref("Post"))
.authorization(allow => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./getPost.js",
})
),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});
amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Post: a.customType({
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
ups: a.integer(),
downs: a.integer(),
version: a.integer(),
}),
updatePost: a
.mutation()
.arguments({
id: a.id().required(),
author: a.string(),
title: a.string(),
content: a.string(),
url: a.string(),
expectedVersion: a.integer().required(),
})
.returns(a.ref("Post"))
.authorization(allow => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./updatePost.js",
})
),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});
amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Post: a.customType({
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
ups: a.integer(),
downs: a.integer(),
version: a.integer(),
}),
deletePost: a
.mutation()
.arguments({ id: a.id().required(), expectedVersion: a.integer() })
.returns(a.ref("Post"))
.authorization(allow => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./deletePost.js",
})
),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});

Step 4 - Configure custom business logic handler code

Next, create the following files in your amplify/data folder and use the code examples to define custom resolvers for the custom queries and mutations added to your schema from the previous step. These are AppSync JavaScript resolvers

amplify/data/addPost.js
import { util } from "@aws-appsync/utils";
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
const item = { ...ctx.arguments, ups: 1, downs: 0, version: 1 };
const key = { id: ctx.args.id ?? util.autoId() };
return ddb.put({ key, item });
}
export function response(ctx) {
return ctx.result;
}
amplify/data/getPost.js
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
return ddb.get({ key: { id: ctx.args.id } });
}
export const response = (ctx) => ctx.result;
amplify/data/updatePost.js
import { util } from "@aws-appsync/utils";
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
const { id, expectedVersion, ...rest } = ctx.args;
const values = Object.entries(rest).reduce((obj, [key, value]) => {
obj[key] = value ?? ddb.operations.remove();
return obj;
}, {});
return ddb.update({
key: { id },
condition: { version: { eq: expectedVersion } },
update: { ...values, version: ddb.operations.increment(1) },
});
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
util.appendError(error.message, error.type);
}
return result;
}
amplify/data/deletePost.js
import { util } from "@aws-appsync/utils";
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
let condition = null;
if (ctx.args.expectedVersion) {
condition = {
or: [
{ id: { attributeExists: false } },
{ version: { eq: ctx.args.expectedVersion } },
],
};
}
return ddb.remove({ key: { id: ctx.args.id }, condition });
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
util.appendError(error.message, error.type);
}
return result;
}

Step 5 - Invoke custom queries or mutations

From your generated Data client, you can find all your custom queries and mutations under the client.queries. and client.mutations. APIs respectively.

App.tsx
const { data, errors } = await client.mutations.addPost({
title: "My Post",
content: "My Content",
author: "Chris",
});
App.tsx
const { data, errors } = await client.queries.getPost({
id: "<post-id>"
});
App.tsx
const { data, errors } = await client.mutations.updatePost({
id: "<post-id>",
title: "An Updated Post",
expectedVersion: 1,
});
App.tsx
const { data, errors } = await client.mutations.deletePost({
id: "<post-id>",
});

Conclusion

In this guide, you’ve added an external DynamoDB table as a data source to an Amplify GraphQL API and defined custom queries and mutations, handled by AppSync JS resolvers, to manipulate Post items in an external DynamoDB table using the Amplify Gen 2 Data client.

To clean up, you can delete your sandbox by accepting the prompt when terminating the sandbox process in your terminal. Alternatively, you can also use the AWS Amplify console to manage and delete sandbox environments.

To delete your external DynamoDB table, you can navigate to the AppSync console and click on the name of the table in the data sources list. This takes you to the DynamoDB console where you can delete the table.

All DynamoDB operations & example resolvers

GetItem

Reference - The GetItem request lets you tell the AWS AppSync DynamoDB function to make a GetItem request to DynamoDB, and enables you to specify:

  • The key of the item in DynamoDB

  • Whether to use a consistent read or not

Example:

export function request(ctx) {
const {foo, bar} = ctx.args
return {
operation : "GetItem",
key : util.dynamodb.toMapValues({foo, bar}),
consistentRead : true
}
}

PutItem

PutItem - The PutItem request mapping document lets you tell the AWS AppSync DynamoDB function to make a PutItem request to DynamoDB, and enables you to specify the following:

  • The key of the item in DynamoDB

  • The full contents of the item (composed of key and attributeValues)

  • Conditions for the operation to succeed

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { foo, bar, ...values} = ctx.args
return {
operation: 'PutItem',
key: util.dynamodb.toMapValues({foo, bar}),
attributeValues: util.dynamodb.toMapValues(values),
};
}

UpdateItem

UpdateItem - The UpdateItem request enables you to tell the AWS AppSync DynamoDB function to make a UpdateItem request to DynamoDB and allows you to specify the following:

  • The key of the item in DynamoDB

  • An update expression describing how to update the item in DynamoDB

  • Conditions for the operation to succeed

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { id } = ctx.args;
return {
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ id }),
update: {
expression: 'ADD #voteField :plusOne, version :plusOne',
expressionNames: { '#voteField': 'upvotes' },
expressionValues: { ':plusOne': { N: 1 } },
},
};
}

DeleteItem

DeleteItem - The DeleteItem request lets you tell the AWS AppSync DynamoDB function to make a DeleteItem request to DynamoDB, and enables you to specify the following:

  • The key of the item in DynamoDB

  • Conditions for the operation to succeed

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
return {
operation: 'DeleteItem',
key: util.dynamodb.toMapValues({ id: ctx.args.id }),
};
}

Query

Query - The Query request object lets you tell the AWS AppSync DynamoDB resolver to make a Query request to DynamoDB, and enables you to specify the following:

  • Key expression

  • Which index to use

  • Any additional filter

  • How many items to return

  • Whether to use consistent reads

  • query direction (forward or backward)

  • Pagination token

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { owner } = ctx.args;
return {
operation: 'Query',
query: {
expression: 'ownerId = :ownerId',
expressionValues: util.dynamodb.toMapValues({ ':ownerId': owner }),
},
index: 'owner-index',
};
}

Scan

Scan - The Scan request lets you tell the AWS AppSync DynamoDB function to make a Scan request to DynamoDB, and enables you to specify the following:

  • A filter to exclude results

  • Which index to use

  • How many items to return

  • Whether to use consistent reads

  • Pagination token

  • Parallel scans

Example:

export function request(ctx) {
return { operation: 'Scan' };
}

Sync

Sync - The Sync request object lets you retrieve all the results from a DynamoDB table and then receive only the data altered since your last query (the delta updates). Sync requests can only be made to versioned DynamoDB data sources. You can specify the following:

  • A filter to exclude results

  • How many items to return

  • Pagination Token

  • When your last Sync operation was started

Example:

export function request(ctx) {
const { nextToken, lastSync } = ctx.args;
return { operation: 'Sync', limit: 100, nextToken, lastSync };
}

BatchGetItem

BatchGetItem - The BatchGetItem request object lets you tell the AWS AppSync DynamoDB function to make a BatchGetItem request to DynamoDB to retrieve multiple items, potentially across multiple tables. For this request object, you must specify the following:

  • The table names where to retrieve the items from

  • The keys of the items to retrieve from each table

The DynamoDB BatchGetItem limits apply and no condition expression can be provided.

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { authorId, postId } = ctx.args;
return {
operation: 'BatchGetItem',
tables: {
authors: [util.dynamodb.toMapValues({ authorId })],
posts: [util.dynamodb.toMapValues({ authorId, postId })],
},
};
}

BatchDeleteItem

BatchDeleteItem - The BatchDeleteItem request object lets you tell the AWS AppSync DynamoDB function to make a BatchWriteItem request to DynamoDB to delete multiple items, potentially across multiple tables. For this request object, you must specify the following:

  • The table names where to delete the items from

  • The keys of the items to delete from each table

The DynamoDB BatchWriteItem limits apply and no condition expression can be provided.

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { authorId, postId } = ctx.args;
return {
operation: 'BatchDeleteItem',
tables: {
authors: [util.dynamodb.toMapValues({ authorId })],
posts: [util.dynamodb.toMapValues({ authorId, postId })],
},
};
}

BatchPutItem

BatchPutItem - The BatchPutItem request object lets you tell the AWS AppSync DynamoDB function to make a BatchWriteItem request to DynamoDB to put multiple items, potentially across multiple tables. For this request object, you must specify the following:

  • The table names where to put the items in

  • The full items to put in each table

The DynamoDB BatchWriteItem limits apply and no condition expression can be provided.

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { authorId, postId, name, title } = ctx.args;
return {
operation: 'BatchPutItem',
tables: {
authors: [util.dynamodb.toMapValues({ authorId, name })],
posts: [util.dynamodb.toMapValues({ authorId, postId, title })],
},
};
}

TransactGetItems

TransactGetItems - The TransactGetItems request object lets you to tell the AWS AppSync DynamoDB function to make a TransactGetItems request to DynamoDB to retrieve multiple items, potentially across multiple tables. For this request object, you must specify the following:

  • The table name of each request item where to retrieve the item from

  • The key of each request item to retrieve from each table

The DynamoDB TransactGetItems limits apply and no condition expression can be provided.

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { authorId, postId } = ctx.args;
return {
operation: 'TransactGetItems',
transactItems: [
{
table: 'posts',
key: util.dynamodb.toMapValues({ postId }),
},
{
table: 'authors',
key: util.dynamodb.toMapValues({ authorId }),
},
],
};
}

TransactWriteItems

TransactWriteItems - The TransactWriteItems request object lets you tell the AWS AppSync DynamoDB function to make a TransactWriteItems request to DynamoDB to write multiple items, potentially to multiple tables. For this request object, you must specify the following:

  • The destination table name of each request item

  • The operation of each request item to perform. There are four types of operations that are supported: PutItem, UpdateItem, DeleteItem, and ConditionCheck

  • The key of each request item to write

The DynamoDB TransactWriteItems limits apply.

Example:

import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { authorId, postId, title, description, oldTitle, authorName } = ctx.args;
return {
operation: 'TransactWriteItems',
transactItems: [
{
table: 'posts',
operation: 'PutItem',
key: util.dynamodb.toMapValues({ postId }),
attributeValues: util.dynamodb.toMapValues({ title, description }),
condition: util.transform.toDynamoDBConditionExpression({
title: { eq: oldTitle },
}),
},
{
table: 'authors',
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ authorId }),
update: {
expression: 'SET authorName = :name',
expressionValues: util.dynamodb.toMapValues({ ':name': authorName }),
},
},
],
};
}