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:
- Define a custom query or mutation
- Configure custom business logic handler code
- Invoke the custom query or mutation
Step 1 - Define a custom query or mutation
Type | When to choose |
---|---|
Query | When the request only needs to read data and will not modify any backend data |
Mutation | When 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 type5 EchoResponse: a.customType({6 content: a.string(),7 executionDuration: a.float()8 }),9
10 // 2. Define your query with the return type and, optionally, arguments11 echo: a12 .query()13 // arguments that this query accepts14 .arguments({15 content: a.string()16 })17 // return type of the query18 .returns(a.ref('EchoResponse'))19 // only allow signed-in users to call this API20 .authorization(allow => [allow.authenticated()])21});22
23export type Schema = ClientSchema<typeof schema>;24
25export const data = defineData({26 schema27});
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 model5 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, arguments12 likePost: a13 .mutation()14 // arguments that this query accepts15 .arguments({16 postId: a.string()17 })18 // return type of the query19 .returns(a.ref('Post'))20 // only allow signed-in users to call this API21 .authorization(allow => [allow.authenticated()])22});23
24export type Schema = ClientSchema<typeof schema>;25
26export const data = defineData({27 schema28});
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.
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() - start8 };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.
1import {2 type ClientSchema,3 a,4 defineData,5 defineFunction // 1.Import "defineFunction" to create new functions6} from '@aws-amplify/backend';7
8// 2. define a function9const 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: a20 .query()21 .arguments({ content: a.string() })22 .returns(a.ref('EchoResponse'))23 .authorization(allow => [allow.publicApiKey()])24 // 3. set the function has the handler25 .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: 3036 },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
.
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: a18 .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: 3036 }37 },38});
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.result14}
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:
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(17 externalTable)
In your schema you can then reference these additional data sources based on their name:
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: a18 .mutation()19 .arguments({ postId: a.id() })20 .returns(a.ref('Post'))21 .authorization(allow => [allow.authenticated()])22 .handler(a.handler.custom({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: 3036 }37 },38});
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 world!!!'3});
1const { data, errors } = await client.mutations.likePost({2 postId: 'hello'3});