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

Page updated Sep 17, 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:

import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
const schema = a.schema({
// 1. Define your return type as a custom type
EchoResponse: a.customType({
content: a.string(),
executionDuration: a.float()
}),
// 2. Define your query with the return type and, optionally, arguments
echo: a
.query()
// arguments that this query accepts
.arguments({
content: a.string()
})
// return type of the query
.returns(a.ref('EchoResponse'))
// only allow signed-in users to call this API
.authorization(allow => [allow.authenticated()])
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema
});
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
const schema = a.schema({
// 1. Define your return type as a custom type or model
Post: a.model({
id: a.id(),
content: a.string(),
likes: a.integer()
}),
// 2. Define your mutation with the return type and, optionally, arguments
likePost: a
.mutation()
// arguments that this query accepts
.arguments({
postId: a.string()
})
// return type of the query
.returns(a.ref('Post'))
// only allow signed-in users to call this API
.authorization(allow => [allow.authenticated()])
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema
});

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
import type { Schema } from '../resource'
export const handler: Schema["echo"]["functionHandler"] = async (event, context) => {
const start = performance.now();
return {
content: `Echoing content: ${event.arguments.content}`,
executionDuration: performance.now() - start
};
};

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
import {
type ClientSchema,
a,
defineData,
defineFunction // 1.Import "defineFunction" to create new functions
} from '@aws-amplify/backend';
// 2. define a function
const echoHandler = defineFunction({
entry: './echo-handler/handler.ts'
})
const schema = a.schema({
EchoResponse: a.customType({
content: a.string(),
executionDuration: a.float()
}),
echo: a
.query()
.arguments({ content: a.string() })
.returns(a.ref('EchoResponse'))
.authorization(allow => [allow.publicApiKey()])
// 3. set the function has the handler
.handler(a.handler.function(echoHandler))
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30
},
},
});

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
import {
type ClientSchema,
a,
defineData,
} from '@aws-amplify/backend';
const schema = a.schema({
Post: a.model({
content: a.string(),
likes: a.integer()
.authorization(allow => [allow.authenticated().to(['read'])])
}).authorization(allow => [
allow.owner(),
allow.authenticated().to(['read'])
]),
likePost: a
.mutation()
.arguments({ postId: a.id() })
.returns(a.ref('Post'))
.authorization(allow => [allow.authenticated()])
.handler(a.handler.custom({
dataSource: a.ref('Post'),
entry: './increment-like.js'
}))
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30
}
},
});
amplify/data/increment-like.js
import { util } from '@aws-appsync/utils';
export function request(ctx) {
return {
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ id: ctx.args.postId}),
update: {
expression: 'ADD likes :plusOne',
expressionValues: { ':plusOne': { N: 1 } },
}
}
}
export function response(ctx) {
return ctx.result
}

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
import * as dynamoDb from 'aws-cdk-lib/aws-dynamodb'
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
export const backend = defineBackend({
auth,
data,
});
const externalDataSourcesStack = backend.createStack("MyExternalDataSources")
const externalTable = dynamoDb.Table.fromTableName(externalDataSourcesStack, "MyTable", "MyExternalTable")
backend.data.addDynamoDbDataSource(
"ExternalTableDataSource",
externalTable)

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

amplify/data/resource.ts
import {
type ClientSchema,
a,
defineData,
} from '@aws-amplify/backend';
const schema = a.schema({
Post: a.model({
content: a.string(),
likes: a.integer()
.authorization(allow => [allow.authenticated().to(['read'])])
}).authorization(allow => [
allow.owner(),
allow.authenticated().to(['read'])
]),
likePost: a
.mutation()
.arguments({ postId: a.id() })
.returns(a.ref('Post'))
.authorization(allow => [allow.authenticated()])
.handler(a.handler.custom({
dataSource: "ExternalTableDataSource",
entry: './increment-like.js'
}))
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: {
expiresInDays: 30
}
},
});

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

struct EchoResponse: Codable {
public let echo: Echo
struct Echo: Codable {
public let content: String
public let executionDuration: Float
}
}
let document = """
query EchoQuery($content: String!) {
echo(content: $content) {
content
executionDuration
}
}
"""
let result = try await Amplify.API.query(request: GraphQLRequest<EchoResponse>(
document: document,
variables: [
"content": "hello world!!!"
],
responseType: EchoResponse.self
))
switch result {
case .success(let response):
print(response.echo)
case .failure(let error):
print(error)
}

Async function handlers

Async function handlers allow you to execute long-running operations asynchronously, improving the responsiveness of your API. This is particularly useful for tasks that don't require an immediate response, such as batch processing, putting messages in a queue, and initiating a generative AI model inference.

Usage

To define an async function handler, use the .async() method when defining your handler:

amplify/data/resource.ts
const signUpForNewsletter = defineFunction({
entry: './sign-up-for-newsletter/handler.ts'
});
const schema = a.schema({
someAsyncOperation: a.mutation()
.arguments({
email: a.email().required()
})
.handler(a.handler.function(signUpForNewsletter).async())
.authorization((allow) => allow.guest()),
})

Key Characteristics

  1. Single Return Type: Async handlers return a static type EventInvocationResponse and don't support specifying a return type. The .returns() method is not available for operations using async handlers.

  2. Fire and Forget: The client is informed whether the invocation was successfully queued, but doesn't receive data from the Lambda function execution.

  3. Pipeline Support: Async handlers can be used in function pipelines. If the final handler is an async function, the return type of the query or mutation is EventInvocationResponse.