Page updated Apr 19, 2024

Add custom queries and mutations

The a.model() data model provides a solid foundation for querying, mutating, and fetching data. However, you may need additional customizations to meet specific requirements around custom API requests, response formatting, and/or fetching from external data sources.

In the following sections, we walk through the three steps to create a custom query or mutation:

  1. Define a custom query or mutation
  2. Configure custom business logic handler code
  3. Invoke the custom query or mutation

Step 1 - Define a custom query or mutation

TypeWhen to choose
QueryWhen the request only needs to read data and will not modify any backend data
MutationWhen the request will modify backend data

For every custom query or mutation, you need to set a return type and, optionally, arguments. Use a.query() or a.mutation() to define your custom query or mutation in your amplify/data/resource.ts file:

1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 // 1. Define your return type as a custom type
5 EchoResponse: a.customType({
6 content: a.string(),
7 executionDuration: a.float()
8 }),
9
10 // 2. Define your query with the return type and, optionally, arguments
11 echo: a
12 .query()
13 // arguments that this query accepts
14 .arguments({
15 content: a.string()
16 })
17 // return type of the query
18 .returns(a.ref('EchoResponse'))
19 // only allow signed-in users to call this API
20 .authorization(allow => [allow.authenticated()])
21});
22
23export type Schema = ClientSchema<typeof schema>;
24
25export const data = defineData({
26 schema
27});
1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 // 1. Define your return type as a custom type or model
5 Post: a.model({
6 id: a.id(),
7 content: a.string(),
8 likes: a.integer()
9 }),
10
11 // 2. Define your mutation with the return type and, optionally, arguments
12 likePost: a
13 .mutation()
14 // arguments that this query accepts
15 .arguments({
16 postId: a.string()
17 })
18 // return type of the query
19 .returns(a.ref('Post'))
20 // only allow signed-in users to call this API
21 .authorization(allow => [allow.authenticated()])
22});
23
24export type Schema = ClientSchema<typeof schema>;
25
26export const data = defineData({
27 schema
28});

Step 2 - Configure custom business logic handler code

After your query or mutation is defined, you need to author your custom business logic. You can either define it in a function or using a custom resolver powered by AppSync JavaScript resolver.

In your amplify/data/echo-handler/ folder, create a handler.ts file. You can import a utility type for your function handler via the Schema type from your backend resource. This gives you type-safe handler parameters and return values.

amplify/data/echo-handler/handler.ts
1import type { Schema } from '../resource'
2
3export const handler: Schema["echo"]["functionHandler"] = async (event, context) => {
4 const start = performance.now();
5 return {
6 content: `Echoing content: ${event.arguments.content}`,
7 executionDuration: performance.now() - start
8 };
9};

In your amplify/data/resource.ts file, define the function using defineFunction and then reference the function with your query or mutation using a.handler.function() as a handler.

amplify/data/resource.ts
1import {
2 type ClientSchema,
3 a,
4 defineData,
5 defineFunction // 1.Import "defineFunction" to create new functions
6} from '@aws-amplify/backend';
7
8// 2. define a function
9const echoHandler = defineFunction({
10 entry: './echo-handler/handler.ts'
11})
12
13const schema = a.schema({
14 EchoResponse: a.customType({
15 content: a.string(),
16 executionDuration: a.float()
17 }),
18
19 echo: a
20 .query()
21 .arguments({ content: a.string() })
22 .returns(a.ref('EchoResponse'))
23 .authorization(allow => [allow.publicApiKey()])
24 // 3. set the function has the handler
25 .handler(a.handler.function(echoHandler))
26});
27
28export type Schema = ClientSchema<typeof schema>;
29
30export const data = defineData({
31 schema,
32 authorizationModes: {
33 defaultAuthorizationMode: 'apiKey',
34 apiKeyAuthorizationMode: {
35 expiresInDays: 30
36 },
37 },
38});

Custom resolvers work on a "request/response" basis. You choose a data source, map your request to the data source's input parameters, and then map the data source's response back to the query/mutation's return type. Custom resolvers provide the benefit of no cold starts, less infrastructure to manage, and no additional charge for Lambda function invocations. Review Choosing between custom resolver and function.

In your amplify/data/resource.ts file, define a custom handler using a.handler.custom.

amplify/data/resource.ts
1import {
2 type ClientSchema,
3 a,
4 defineData,
5} from '@aws-amplify/backend';
6
7const schema = a.schema({
8 Post: a.model({
9 content: a.string(),
10 likes: a.integer()
11 .authorization(allow => [allow.authenticated().to(['read'])])
12 }).authorization(allow => [
13 allow.owner(),
14 allow.authenticated().to(['read'])
15 ]),
16
17 likePost: a
18 .mutation()
19 .arguments({ postId: a.id() })
20 .returns(a.ref('Post'))
21 .authorization(allow => [allow.authenticated()])
22 .handler(a.handler.custom({
23 dataSource: a.ref('Post'),
24 entry: './increment-like.js'
25 }))
26});
27
28export type Schema = ClientSchema<typeof schema>;
29
30export const data = defineData({
31 schema,
32 authorizationModes: {
33 defaultAuthorizationMode: 'apiKey',
34 apiKeyAuthorizationMode: {
35 expiresInDays: 30
36 }
37 },
38});
amplify/data/increment-like.js
1export function request(ctx) {
2 return {
3 operation: 'UpdateItem',
4 key: util.dynamodb.toMapValues({ id: ctx.args.postId}),
5 update: {
6 expression: 'ADD likes :plusOne',
7 expressionValues: { ':plusOne': { N: 1 } },
8 }
9 }
10}
11
12export function response(ctx) {
13 return ctx.result
14}

By default, you'll be able to access any existing database tables (powered by Amazon DynamoDB) using a.ref('MODEL_NAME'). But you can also reference any other external data source from within your AWS account, by adding them to your backend definition.

The supported data sources are:

  • Amazon DynamoDB
  • AWS Lambda
  • Amazon RDS databases with Data API
  • Amazon EventBridge
  • OpenSearch
  • HTTP endpoints

You can add these additional data sources via our amplify/backend.ts file:

amplify/backend.ts
1import * as dynamoDb from 'aws-cdk-lib/aws-dynamodb'
2import { defineBackend } from '@aws-amplify/backend';
3import { auth } from './auth/resource';
4import { data } from './data/resource';
5
6export const backend = defineBackend({
7 auth,
8 data,
9});
10
11const externalDataSourcesStack = backend.createStack("MyExternalDataSources")
12
13const externalTable = dynamoDb.Table.fromTableName(externalDataSourcesStack, "MyTable", "MyExternalTable")
14
15backend.data.addDynamoDbDataSource(
16 "ExternalTableDataSource",
17 externalTable)

In your schema you can then reference these additional data sources based on their name:

amplify/data/resource.ts
1import {
2 type ClientSchema,
3 a,
4 defineData,
5} from '@aws-amplify/backend';
6
7const schema = a.schema({
8 Post: a.model({
9 content: a.string(),
10 likes: a.integer()
11 .authorization(allow => [allow.authenticated().to(['read'])])
12 }).authorization(allow => [
13 allow.owner(),
14 allow.authenticated().to(['read'])
15 ]),
16
17 likePost: a
18 .mutation()
19 .arguments({ postId: a.id() })
20 .returns(a.ref('Post'))
21 .authorization(allow => [allow.authenticated()])
22 .handler(a.handler.custom({
23 dataSource: "ExternalTableDataSource",
24 entry: './increment-like.js'
25 }))
26});
27
28export type Schema = ClientSchema<typeof schema>;
29
30export const data = defineData({
31 schema,
32 authorizationModes: {
33 defaultAuthorizationMode: 'apiKey',
34 apiKeyAuthorizationMode: {
35 expiresInDays: 30
36 }
37 },
38});

The .handler() accepts an array of handlers that will run in a pipeline. For now, in the developer preview, you can only use a.handler.custom() and a.handler.function(). All other handlers, such as a.handler.inlineSql() are under active development.

All handlers must be of the same type. For example, you can't mix and match a.handler.function with a.handler.custom within a single .handler() modifier.

Step 3 - Invoke the custom query or mutation

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

1const { data, errors } = await client.queries.echo({
2 content: 'hello'
3});
1const { data, errors } = await client.mutations.likePost({
2 postId: 'hello'
3});